From 32b61c6761f4fbf4b15916a6482e002e66608e9b Mon Sep 17 00:00:00 2001 From: Closed-Book Date: Thu, 14 May 2026 15:20:03 +0800 Subject: [PATCH] fix(security): SSRF guard + ripgrep timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 fetch SSRF 防御(M1)+ ripgrep timeout(M3)。属 nice-to-have 安全纵深,请作者判断是否合并: fetcher/http.ts: - 改 redirect: 'manual' 手动跟随,max=5 跳 - 每跳前 dns.lookup() 检查目标 IP 是否在 RFC 1918 / RFC 6890 私网范围(127/8 / 10/8 / 172.16/12 / 192.168/16 / 169.254/16 / ::1 / fc00::/7),拒则抛 PrivateAddressError - env opt-out LOREKIT_FETCH_ALLOW_PRIVATE=1 跳过私网检查 - 不影响 ollama localhost(在 lib/ollama.ts 另一路径) fetcher/index.ts: - L1 catch 时识别 PrivateAddressError,直接冒泡为 FetchResult error (reason=PRIVATE_ADDRESS_BLOCKED),不退 L2 fallback (否则 playwright 也会绕过 guard 命中私网) search.ts: - spawnSync 加 timeout: 30_000(之前仅 maxBuffer 10M) - SIGTERM 触发时落 fallback + warn 新增 tests/smoke/fetch-ssrf.test.mjs 验证私网拒绝 + opt-out。 Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/cli.js | 123 +++++++++++++++++++++-- dist/cli.js.map | 2 +- src/commands/search.ts | 9 +- src/lib/fetcher/http.ts | 173 ++++++++++++++++++++++++++++++-- src/lib/fetcher/index.ts | 18 +++- tests/smoke/fetch-ssrf.test.mjs | 58 +++++++++++ 6 files changed, 361 insertions(+), 22 deletions(-) create mode 100644 tests/smoke/fetch-ssrf.test.mjs diff --git a/dist/cli.js b/dist/cli.js index 45a8f73..fa8af7e 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -2944,9 +2944,15 @@ function searchWithRipgrep(query, corpus, opts) { args.push(query, searchDir); const result = spawnSync("rg", args, { encoding: "utf-8", - maxBuffer: 10 * 1024 * 1024 + maxBuffer: 10 * 1024 * 1024, + // 30s 上限:规避恶意 / 退化 regex 在巨型 corpus 上拖垮 CLI(PR #5 review M3)。 + // ripgrep 正常扫几 GB markdown 也只要几秒,30s 是宽松值。 + timeout: 3e4 }); if (result.error) { + if (result.signal === "SIGTERM") { + warn(`rg timed out after 30s, falling back to built-in scan`); + } return []; } const results = []; @@ -3242,9 +3248,12 @@ function normalizeDateText(raw) { } // src/lib/fetcher/http.ts +import { lookup } from "dns/promises"; +import { BlockList, isIP } from "net"; var UA_IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; var UA_DESKTOP = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; var HTTP_TIMEOUT_MS = 2e4; +var MAX_REDIRECTS = 5; var ANTIBOT_TRIGGERS = [ "\u73AF\u5883\u5F02\u5E38", "\u8BF7\u5728\u5FAE\u4FE1\u5BA2\u6237\u7AEF\u6253\u5F00", @@ -3252,6 +3261,73 @@ var ANTIBOT_TRIGGERS = [ "Just a moment", "cf-browser-verification" ]; +var PRIVATE_BLOCKS = (() => { + const bl = new BlockList(); + bl.addSubnet("127.0.0.0", 8, "ipv4"); + bl.addSubnet("10.0.0.0", 8, "ipv4"); + bl.addSubnet("172.16.0.0", 12, "ipv4"); + bl.addSubnet("192.168.0.0", 16, "ipv4"); + bl.addSubnet("169.254.0.0", 16, "ipv4"); + bl.addSubnet("0.0.0.0", 8, "ipv4"); + bl.addAddress("::1", "ipv6"); + bl.addSubnet("fc00::", 7, "ipv6"); + bl.addSubnet("fe80::", 10, "ipv6"); + return bl; +})(); +var PrivateAddressError = class extends Error { + constructor(message, host, address) { + super(message); + this.host = host; + this.address = address; + this.name = "PrivateAddressError"; + } + host; + address; + code = "SSRF_PRIVATE_ADDRESS"; +}; +function isPrivateOptOut() { + const v = process.env.LOREKIT_FETCH_ALLOW_PRIVATE; + return v === "1" || v === "true"; +} +async function assertPublicAddress(urlStr) { + if (isPrivateOptOut()) return; + let parsed; + try { + parsed = new URL(urlStr); + } catch { + return; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return; + const rawHost = parsed.hostname; + const host = rawHost.startsWith("[") && rawHost.endsWith("]") ? rawHost.slice(1, -1) : rawHost; + if (!host) return; + const ipVersion = isIP(host); + if (ipVersion === 4 || ipVersion === 6) { + const family2 = ipVersion === 4 ? "ipv4" : "ipv6"; + if (PRIVATE_BLOCKS.check(host, family2)) { + throw new PrivateAddressError( + `refusing to fetch private address ${host} (set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`, + host, + host + ); + } + return; + } + let addr; + try { + addr = await lookup(host); + } catch { + return; + } + const family = addr.family === 6 ? "ipv6" : "ipv4"; + if (PRIVATE_BLOCKS.check(addr.address, family)) { + throw new PrivateAddressError( + `refusing to fetch ${host} which resolves to private address ${addr.address} (set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`, + host, + addr.address + ); + } +} function detectSite(url) { try { const host = new URL(url).hostname.toLowerCase(); @@ -3284,13 +3360,33 @@ async function fetchHtmlL1(url, headers) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS); try { - const res = await fetch(url, { - headers, - redirect: "follow", - signal: controller.signal - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return await res.text(); + let currentUrl = url; + for (let hop = 0; hop <= MAX_REDIRECTS; hop++) { + await assertPublicAddress(currentUrl); + const res = await fetch(currentUrl, { + headers, + redirect: "manual", + signal: controller.signal + }); + if (res.status >= 300 && res.status < 400) { + const loc = res.headers.get("location"); + if (!loc) { + throw new Error(`HTTP ${res.status} without Location header`); + } + if (hop === MAX_REDIRECTS) { + throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${url}`); + } + currentUrl = new URL(loc, currentUrl).toString(); + try { + await res.arrayBuffer(); + } catch { + } + continue; + } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return await res.text(); + } + throw new Error(`unreachable: redirect loop exit ${url}`); } finally { clearTimeout(timer); } @@ -3740,7 +3836,16 @@ async function fetchUrl(url, opts) { if (detectAntibot(html, site)) { html = ""; } - } catch { + } catch (e) { + if (e instanceof PrivateAddressError) { + return { + status: "error", + route: "rich", + url, + reason: "PRIVATE_ADDRESS_BLOCKED", + suggest: `target resolves to private address ${e.address}. Set LOREKIT_FETCH_ALLOW_PRIVATE=1 to allow (local dev only).` + }; + } html = ""; } if (!html) { diff --git a/dist/cli.js.map b/dist/cli.js.map index fb56c7d..ed0d196 100644 --- a/dist/cli.js.map +++ b/dist/cli.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/lib/paths.ts","../src/utils/logger.ts","../src/lib/vectordb/files.ts","../src/lib/vectordb/schema.ts","../src/lib/ollama.ts","../src/lib/chunker.ts","../src/lib/vectordb/sync.ts","../src/lib/vectordb/build-layered-index.ts","../src/lib/vectordb/query-flat.ts","../src/lib/vectordb/query-layered.ts","../src/lib/vectordb/query-bm25.ts","../src/lib/vectordb/query-hybrid.ts","../src/lib/vectordb/status.ts","../src/lib/vectordb/index.ts","../src/cli.ts","../src/lib/corpus.ts","../src/utils/fs.ts","../src/commands/init.ts","../src/commands/doctor.ts","../src/lib/obsidian.ts","../src/lib/integrations/gbrain.ts","../src/lib/integrations/gbrain-status.ts","../src/lib/integrations/process.ts","../src/lib/integrations/gbrain-export.ts","../src/lib/integrations/manifest.ts","../src/commands/stats.ts","../src/commands/lint.ts","../src/commands/audit.ts","../src/lib/date.ts","../src/commands/dir-index.ts","../src/commands/install-skills.ts","../src/commands/snapshot.ts","../src/commands/restore.ts","../src/commands/search.ts","../src/commands/vector.ts","../src/lib/vectordb/prune.ts","../src/commands/fetch.ts","../src/lib/fetcher/index.ts","../src/lib/fetcher/frontmatter.ts","../src/lib/fetcher/helpers.ts","../src/lib/fetcher/http.ts","../src/lib/fetcher/images.ts","../src/lib/fetcher/routes/web.ts","../src/lib/fetcher/routes/weixin.ts","../src/lib/fetcher/routes/gist.ts","../src/lib/fetcher/routes/github.ts","../src/lib/ingest-state.ts","../src/commands/ingest.ts","../src/commands/sync.ts","../src/lib/root-index.ts","../src/commands/obsidian-tune.ts","../src/commands/remove.ts","../src/commands/gbrain.ts"],"sourcesContent":["/**\n * paths.ts — corpus 路径 / 排除规则的单一事实源。\n *\n * 历史背景(LEGACY P1-1):corpus.ts / vectordb.ts / commands/{index,lint,snapshot}\n * 各自维护独立的\"排除目录\"集合,加新顶层目录时容易漏改其中一处。本文件把所有\n * 集合集中起来,下游 import 即可。\n *\n * CONVENTIONS Do Not #11:建 paths.ts 后不许再硬编码新的\"排除目录\"常量。\n *\n * 命名约定:\n * - alwaysExclude* — 全局通用,所有 collect / scan 都该跳过\n * - vectorInclude* — 仅向量库索引时纳入\n * - vectorExclude* — 仅向量库索引时排除\n * - lintSkip* — 仅 lint 时跳过\n * - indexExclude* — 仅 dir-index (`_INDEX.md`) 生成时跳过\n * - snapshotExclude* — 仅 snapshot 时跳过\n *\n * 后续批次(6/7)会继续往本文件追加 set;先生不要在其他文件里\"另起炉灶\"。\n */\n\n/**\n * 全局排除:任何 markdown 收集都该跳过的文件名(不是目录)。\n * - .gitkeep / .DS_Store:环境噪声\n * - _INDEX.md:`lorekit index` 自动生成的目录索引文件,不是用户内容\n */\nexport const alwaysExcludeNames: ReadonlySet = new Set([\n '.gitkeep',\n '.DS_Store',\n '_INDEX.md',\n]);\n\n// ---------------------------------------------------------------------------\n// 向量库索引(vectordb.ts)专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * 向量库纳入索引的目录前缀。任何文件 rel path 必须以下列之一开头才参与 chunk\n * embedding。注意:原料里只索引\"长文\"类(文章 / 书籍 / 会议);剪藏 / 录音\n * 走另外的路径(embed 摘要而非全文)。\n */\nexport const vectorIncludeDirs: readonly string[] = [\n '知识库',\n '每日',\n '写作',\n '原料/文章',\n '原料/书籍',\n '原料/会议',\n];\n\n/**\n * 向量库排除目录前缀。先 exclude 检查再 include 检查 — exclude 规则胜出。\n * - `_工作台` / `_archive` / `_归档`:过渡区 / 冷数据,不该污染向量空间\n * - `原料/录音` / `原料/剪藏`:走摘要 embedding,不索引全文 chunk\n * - `反馈` / `系统` / `.wiki`:流程 / 规范 / 元数据,跟知识检索无关\n * - `输出`:LLM 产物(问答 / 文章 / 幻灯片 / 图表 / 体检报告 / 空缺分析),\n * 是二次加工产物不该回流进向量空间(否则会和原始 wiki 页形成互相加强的回音)\n *\n * 注:`.assets/`(各 markdown 的图片子目录)**不需要在此单独列**——它一定是\n * 某个父目录的子目录,随父目录的 include / exclude 规则自动生效。\n */\nexport const vectorExcludePrefixes: readonly string[] = [\n '_工作台',\n '_archive',\n '_归档',\n '原料/录音',\n '原料/剪藏',\n '反馈',\n '系统',\n '输出',\n '.wiki',\n];\n\n/**\n * 向量库排除文件名(不含 `_INDEX.md` —— 注意跟 alwaysExcludeNames 不同)。\n * `_INDEX.md` 由 buildLayeredIndex 通过 L1 路径单独处理(embed 条目摘要),\n * 不应进 chunk 池;但当前 vectordb.shouldIndex 没有显式排除它,依赖 INCLUDE_DIRS\n * 圈定边界。本集合**严格保留迁移前行为**,不在本批改语义。\n */\nexport const vectorExcludeNames: ReadonlySet = new Set(['.gitkeep', '.DS_Store']);\n\n// ---------------------------------------------------------------------------\n// `lorekit index` 专用规则(生成 _INDEX.md)\n// ---------------------------------------------------------------------------\n\n/**\n * `lorekit index` 不为下列前缀目录生成 `_INDEX.md`。\n * 系统 / 反馈 / 工作台 / 归档 / git / .wiki 都不是用户内容书架。\n */\nexport const indexExcludeDirPrefixes: readonly string[] = [\n '.wiki',\n '.git',\n '_归档',\n '_工作台',\n '系统',\n '反馈',\n];\n\n/**\n * 判断给定 corpus 内相对路径是否落在\"不索引\"前缀里。\n * 对外暴露的小工具(doctor.ts / commands/dir-index.ts 都用)。\n */\nexport function isIndexExcluded(rel: string): boolean {\n for (const prefix of indexExcludeDirPrefixes) {\n if (rel === prefix || rel.startsWith(prefix + '/')) return true;\n }\n return false;\n}\n\n/**\n * 判断目录是否\"目录包装式原料\"——即 `/article.md` 存在。\n * 此类目录在生成 `_INDEX.md` 时被当作\"一个条目\"而非容器,不递归进入。\n *\n * 注:lstatSync 直接 throw 时返回 false(catch 静默是有意——目录不存在 / 权限拒绝\n * 都按\"不是 folder package\" 处理;这是 paths.ts 的小预言式 helper,没有副作用)。\n */\nexport function isFolderPackage(dir: string): boolean {\n const articlePath = pathJoin(dir, 'article.md');\n try {\n return lstatSync(articlePath).isFile();\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// `lorekit lint` 专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * lint 时按 basename 跳过 frontmatter / orphan 检查的文件名(任何位置)。\n * 这些是 schema / 入口文档,不该被当作 wiki 页验证。\n */\nexport const lintSkipFrontmatterBasenames: ReadonlySet = new Set([\n 'README.md',\n 'AGENTS.md',\n 'CLAUDE.md',\n 'MEMORY.md',\n]);\n\n/**\n * 仅在 corpus 根目录跳过的文件名。`index.md` / `log.md` 在子目录里如果有\n * 同名文件,仍按普通 wiki 页校验。\n */\nexport const lintRootOnlySkipBasenames: ReadonlySet = new Set(['index.md', 'log.md']);\n\n/**\n * lint 不参与 orphan 检查的目录前缀(过渡区 / 冷数据 / 系统规范 / 模板区)。\n * `知识库/模板/` 下是页面模板,不是正式 wiki 页,天然无入链,不应报 orphan。\n */\nexport const lintSkipOrphanPrefixes: readonly string[] = [\n '_工作台/',\n '_归档/',\n '系统/',\n '知识库/模板/',\n];\n\n/**\n * lint 不参与 frontmatter 检查的目录前缀(过渡区 / 冷数据)。\n * 注:`系统/` 故意保留 frontmatter 检查(schema 文件应规范)。\n */\nexport const lintSkipFrontmatterPrefixes: readonly string[] = ['_工作台/', '_归档/'];\n\n/**\n * lint 不参与 broken-link 检查的目录前缀。\n * `知识库/模板/` 下模板正文含 `[[知识库/摘要/xxx]]` 这种占位符,让 LLM 建页时替换,\n * 不是真实 wikilink 目标,不该被报 broken。\n */\nexport const lintSkipBrokenLinkPrefixes: readonly string[] = ['知识库/模板/'];\n\n// ---------------------------------------------------------------------------\n// `lorekit snapshot` 专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * snapshot 打包时跳过的目录 / 文件名。\n * - .wiki:corpus 元数据(含向量库),快照里要重建不要拷\n * - .git:corpus 自己的 git 仓库\n * - .DS_Store:环境噪声\n */\nexport const snapshotExcludeNames: ReadonlySet = new Set(['.wiki', '.git', '.DS_Store']);\n\n// ---------------------------------------------------------------------------\n// 内部 helper(用静态 import;放文件末尾以保持顶部导出干净)\n// ---------------------------------------------------------------------------\n\nimport { lstatSync } from 'node:fs';\nimport { join as pathJoin } from 'node:path';\n","/**\n * logger.ts — 全仓库人类输出唯一通道(CONVENTIONS #2 强制)。\n *\n * 通道分流(CONVENTIONS #3):所有人类信息(ok / bad / warn / err / info / debug)\n * 一律 stderr,stdout 只留给机器可读输出(JSON / 数据)。这样\n * `lorekit xxx 2>/dev/null | jq .` 才能正确读到结果。\n *\n * debug:受 `LOREKIT_DEBUG=1` 环境变量控制;不开时静默。\n *\n * 历史:批次 10 之前 `ok` / `bad` 走 stdout,是 CONVENTIONS 已规定但 logger 自己\n * 没落地的 bug(LEGACY P1-3)。本批次修复。\n */\nimport chalk from 'chalk';\n\nconst DEBUG_ENABLED = process.env.LOREKIT_DEBUG === '1';\n\n/** 成功 / 完成提示,绿勾 */\nexport const ok = (msg: string) => console.error(`${chalk.green('✓')} ${msg}`);\n\n/** 失败 / 取消提示,红叉。批次 10 前错误地写 stdout,已修正 */\nexport const bad = (msg: string) => console.error(`${chalk.red('✗')} ${msg}`);\n\n/** 警告:可继续但需注意 */\nexport const warn = (msg: string) => console.error(`${chalk.yellow('lorekit:')} ${msg}`);\n\n/** 错误:致命或已退出条件 */\nexport const err = (msg: string) => console.error(`${chalk.red('lorekit:')} ${msg}`);\n\n/** 一般信息:进度 / 提示 */\nexport const info = (msg: string) => console.error(`${chalk.cyan('ℹ')} ${msg}`);\n\n/** 调试输出:仅当 LOREKIT_DEBUG=1 时打印;其他时候静默 */\nexport const debug = (msg: string) => {\n if (DEBUG_ENABLED) console.error(`${chalk.dim('debug:')} ${msg}`);\n};\n\n/**\n * 原样写一行到 stderr(无前缀 / 无装饰)。\n * 用于 banner / 分隔线 / 空行 / 自定义 chalk 着色的 header 等纯展示内容。\n * 调用方负责自己加 chalk —— 如 `print(chalk.cyan('── 区块 ──'))`。\n */\nexport const print = (msg = '') => console.error(msg);\n\n/**\n * 原样写一行到 **stdout**(机器可读通道)。\n * 用于 JSON / 数据结构 / 任何下游会用 `cmd | jq` 解析的输出。\n * 这是 stdout 的唯一合法 logger 入口(CONVENTIONS Do Not #2)。\n */\nexport const out = (msg: string) => console.log(msg);\n","/**\n * vectordb/files.ts — 文件发现 + 字节工具 + 页摘要抽取\n *\n * 批次 22a strangler fig 第一步:从 src/lib/vectordb.ts copy 出文件层小工具。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - sha256 / float32ToBuffer / distanceToScore:字节层小工具\n * - shouldIndex / collectFiles:基于 paths.ts 规则的 corpus 文件发现\n * - extractPageSummary:从 markdown 文件抽 \"title: intro\" 形式的页摘要\n *\n * 不依赖 schema.ts(无 db 状态),self-contained 除 paths.ts 与 gray-matter。\n */\n\nimport { createHash } from 'node:crypto';\nimport { readFileSync, readdirSync } from 'node:fs';\nimport { basename, join, relative } from 'node:path';\n\nimport matter from 'gray-matter';\n\nimport { vectorIncludeDirs, vectorExcludePrefixes, vectorExcludeNames } from '../paths.js';\n\n// ---------------------------------------------------------------------------\n// 字节层小工具\n// ---------------------------------------------------------------------------\n\n/**\n * 文件 SHA-256 hex digest。用于 sync 时判断 `.path → updated_at` 是否需要\n * 重新计算 embedding(`sha256` 没变就直接复用旧 chunks)。\n */\nexport function sha256(filePath: string): string {\n const data = readFileSync(filePath);\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Float32Array → Buffer 零拷贝转换(共享底层 ArrayBuffer),用于把 embedding\n * 写进 sqlite BLOB 列。\n */\nexport function float32ToBuffer(arr: Float32Array): Buffer {\n return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);\n}\n\n/**\n * Convert sqlite-vec cosine distance to similarity score.\n * sqlite-vec returns sqrt(2*(1 - cos_sim)), so:\n * score = 1 - distance^2 / 2\n */\nexport function distanceToScore(distance: number): number {\n return 1.0 - (distance * distance) / 2.0;\n}\n\n// ---------------------------------------------------------------------------\n// 文件发现\n// ---------------------------------------------------------------------------\n\n/**\n * 判断给定 corpus 相对路径是否应进向量索引。\n *\n * 规则(来自 paths.ts,CONVENTIONS Do Not #11 集中维护):\n * 1. 文件名命中 `vectorExcludeNames`(如 `_INDEX.md` / `.gitkeep`)→ 跳\n * 2. 非 `.md` → 跳\n * 3. 路径前缀命中 `vectorExcludePrefixes`(如 `_工作台/` / `_归档/`)→ 跳\n * 4. 路径前缀命中 `vectorIncludeDirs`(如 `知识库/` / `原料/`)→ 收\n * 5. 都不命中 → 跳(保守策略:未明确包含的目录不索引)\n */\nexport function shouldIndex(rel: string): boolean {\n const parts = rel.split('/');\n if (vectorExcludeNames.has(parts[parts.length - 1])) return false;\n if (!rel.endsWith('.md')) return false;\n for (const prefix of vectorExcludePrefixes) {\n if (rel === prefix || rel.startsWith(prefix + '/')) return false;\n }\n for (const inc of vectorIncludeDirs) {\n if (rel === inc || rel.startsWith(inc + '/')) return true;\n }\n return false;\n}\n\n/**\n * 递归收集 corpus 下所有应进索引的 .md 文件,返回排序后的绝对路径列表。\n * 排序保证多次运行得到稳定的 doc_id 顺序,便于 diff debug。\n */\nexport function collectFiles(corpus: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string) {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n // 目录读不到(权限 / 临时被删)就跳,整体扫描继续。\n return;\n }\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.md')) {\n const rel = relative(corpus, full);\n if (shouldIndex(rel)) {\n results.push(full);\n }\n }\n }\n }\n\n walk(corpus);\n return results.sort();\n}\n\n// ---------------------------------------------------------------------------\n// 页摘要\n// ---------------------------------------------------------------------------\n\n/**\n * 给一个 markdown 文件抽 \"title: intro\" 形式的页摘要,用于 page_summaries 向量。\n *\n * title 优先级:frontmatter.title > 第一个 `# heading` > basename(file, '.md')\n *\n * intro 优先级:`## Compiled Truth` 段(lorekit 知识库页约定的核心摘要节)前 200 字\n * > body 起始前 200 字\n */\nexport function extractPageSummary(filePath: string): string {\n const raw = readFileSync(filePath, 'utf-8');\n const { data: fm, content: body } = matter(raw);\n\n let title = (fm.title as string) || '';\n if (!title) {\n const m = body.match(/^#\\s+(.+)/m);\n title = m ? m[1].trim() : basename(filePath, '.md');\n }\n\n // Try \"## Compiled Truth\" section\n const ctMatch = body.match(/(?:^|\\n)## Compiled Truth\\s*\\n([\\s\\S]*?)(?=\\n## |\\n*$)/);\n const intro = ctMatch ? ctMatch[1].trim().slice(0, 200) : body.trim().slice(0, 200);\n\n return `${title}: ${intro}`;\n}\n","/**\n * vectordb/schema.ts — sqlite schema (DDL) + 数据库打开 + 动态加载 sqlite-vec\n *\n * 批次 22a strangler fig 第一步:从 src/lib/vectordb.ts copy 出 schema 层。\n * 原 vectordb.ts 同名定义仍保留,commands/*.ts 暂未切换,本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 常量:EMBEDDING_DIM / MODE_THRESHOLD_FILES\n * - 类型:Db / StatusInfo / QueryResult\n * - DDL:4 个 SQL 表 + 3 个 vec0 虚表 + 3 个 fts5 虚表\n * - 入口:loadSqlite() 动态加载 better-sqlite3 + sqlite-vec;openDb() 一站建库\n *\n * 不含 sync / query / build-layered(后续子批负责)。\n */\n\nimport { existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport type DatabaseNS from 'better-sqlite3';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface QueryResult {\n file: string;\n chunk: string;\n score: number;\n section: string;\n}\n\nexport interface StatusInfo {\n indexed: boolean;\n total_indexable_files?: number;\n indexed_files?: number;\n chunks?: number;\n layered?: { dirs: number; pages: number };\n embedding_dim?: number;\n last_sync?: string | null;\n model?: string | null;\n backend?: string;\n message?: string;\n /**\n * 检索模式推荐(wiki-query skill 直接读这个字段决定路径,不做数值判断)\n * - \"text\" → 走 Read 三层(corpus/index.md → {dir}/_INDEX.md → 具体文件)\n * - \"vector\" → 走向量 layered 召回\n */\n mode?: 'text' | 'vector';\n mode_threshold?: number;\n mode_reason?: string;\n}\n\n// **23c 改**:原 `Db = any` → `DatabaseNS.Database` 精确类型(来自 @types/better-sqlite3\n// 的 namespace 别名 `BetterSqlite3.Database`)。`import type DatabaseNS` 是类型 only,\n// 不引入 runtime 依赖(runtime 仍走 schema.ts 内的 dynamic import 兜可选 dep 缺失)。\n// **重命名 NS 后缀**:避开 loadSqlite() 内部 `let Database = ...` 局部变量 shadowing。\n// 编辑器对 db.prepare/get/run/exec/pragma/close 的智能补全和参数校验恢复正常。\nexport type Db = DatabaseNS.Database;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 1024;\n\n/**\n * 文本模式 ↔ 向量模式的切换阈值(按 indexed_files 计数,不按 chunks)。\n *\n * 为什么按文档数不按 chunks:chunks 会被单文档长度扭曲(一篇 2 万字可能切出 30+ chunks,\n * 但它仍然只是\"一份材料\")。Karpathy 原文也是按 pages/sources 计数:\n * \"works surprisingly well at moderate scale (~100 sources, ~hundreds of pages)\n * and avoids the need for embedding-based RAG infrastructure.\"\n *\n * 100 = 按 Karpathy 原文的 moderate scale 上界。到这规模之前,Read 三层精度最高;\n * 超过后扁平 Read 太慢,切向量 layered 召回。\n *\n * 先生要调:改这里的数字即可,所有 skill 通过 `lorekit vector status` 读 `mode` 字段\n * 自动跟随。\n */\nexport const MODE_THRESHOLD_FILES = 100;\n\n// 排除 / 包含规则集中在 lib/paths.ts,本文件不再硬编码(CONVENTIONS Do Not #11)。\n\n// ---------------------------------------------------------------------------\n// DDL\n// ---------------------------------------------------------------------------\n\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY,\n path TEXT UNIQUE NOT NULL,\n sha256 TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY,\n doc_id INTEGER NOT NULL,\n section TEXT,\n content TEXT NOT NULL,\n embedding BLOB NOT NULL,\n FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\nCREATE TABLE IF NOT EXISTS dir_summaries (\n id INTEGER PRIMARY KEY,\n dir_path TEXT UNIQUE NOT NULL,\n summary TEXT NOT NULL,\n embedding BLOB NOT NULL,\n slug_list TEXT NOT NULL DEFAULT '[]'\n);\n\nCREATE TABLE IF NOT EXISTS page_summaries (\n id INTEGER PRIMARY KEY,\n doc_id INTEGER NOT NULL REFERENCES documents(id),\n summary TEXT NOT NULL,\n embedding BLOB NOT NULL\n);\n`;\n\n/**\n * 生成 vec0 虚表 DDL。dim 是 embedding 维度(默认 EMBEDDING_DIM=1024 / bge-m3 模型)。\n *\n * 三个虚表平行存在:\n * - vec_chunks:文档切块向量(rowid 对齐 chunks.id)\n * - vec_dirs:目录摘要向量(rowid 对齐 dir_summaries.id)\n * - vec_pages:页摘要向量(rowid 对齐 page_summaries.id)\n *\n * cosine 距离用于召回时按相似度排序。\n */\nexport function vecDdl(dim: number): string {\n return `\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_dirs USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_pages USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n`;\n}\n\n/**\n * FTS5 虚表 DDL:跟 vec_* 平行存在于同一份 vector.sqlite 里。\n * tokenize='trigram':对中文友好(每 3 字符滑动窗口),专有名词和日期精确命中。\n * rowid 跟对应 SQL 表的 id 对齐(chunks.id / dir_summaries.id / page_summaries.id)。\n */\nexport const FTS_DDL = `\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_chunks USING fts5(\n content,\n tokenize='trigram'\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_dirs USING fts5(\n summary,\n tokenize='trigram'\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(\n summary,\n tokenize='trigram'\n);\n`;\n\n// ---------------------------------------------------------------------------\n// Dynamic import helper\n// ---------------------------------------------------------------------------\n\n/**\n * 动态加载 better-sqlite3 + sqlite-vec。两者都是 optionalDependencies,\n * 缺失时抛带安装提示的 Error,由调用方决定怎么报给用户。\n *\n * 单独抽出来是为了让其他模块(query / sync / build-layered)也能复用同一份动态加载,\n * 不重复 try/catch。\n */\nexport async function loadSqlite(): Promise<{\n Database: typeof import('better-sqlite3');\n sqliteVec: { load: (db: Db) => void };\n}> {\n let Database: typeof import('better-sqlite3');\n try {\n Database = (await import('better-sqlite3'))\n .default as unknown as typeof import('better-sqlite3');\n } catch {\n throw new Error(\n 'better-sqlite3 is required for the vector engine.\\n' +\n ' Install it: npm install better-sqlite3',\n );\n }\n\n let sqliteVec: { load: (db: Db) => void };\n try {\n const vecMod = await import('sqlite-vec');\n sqliteVec = vecMod as unknown as { load: (db: Db) => void };\n } catch {\n throw new Error(\n 'sqlite-vec is required for the vector engine.\\n' + ' Install it: npm install sqlite-vec',\n );\n }\n\n return { Database, sqliteVec };\n}\n\n// ---------------------------------------------------------------------------\n// openDb — 一站建库 + DDL 应用 + migration\n// ---------------------------------------------------------------------------\n\n/**\n * 打开(必要时创建)`/.wiki/vector.sqlite`,应用所有 DDL,加载 sqlite-vec。\n *\n * 副作用:\n * - 创建 `/.wiki/` 目录(若不存在)\n * - WAL 模式 + 外键开启\n * - migration:dir_summaries.slug_list 字段(21 之前老库可能缺)补 ALTER TABLE\n *\n * 调用方拿到 Db 之后是同步 better-sqlite3 句柄,可直接 prepare/exec/transaction。\n */\nexport async function openDb(corpus: string, dim = EMBEDDING_DIM): Promise {\n const { Database, sqliteVec } = await loadSqlite();\n\n const wikiDir = join(corpus, '.wiki');\n if (!existsSync(wikiDir)) mkdirSync(wikiDir, { recursive: true });\n\n const dbPath = join(wikiDir, 'vector.sqlite');\n // `Database` 的实际 runtime 值是 `BetterSqlite3.DatabaseConstructor`(可 new 的\n // class);dynamic import 拿回的 `default` 已是构造器本体,直接 new 即可。\n const db = new Database(dbPath);\n sqliteVec.load(db);\n\n db.pragma('journal_mode = WAL');\n db.pragma('foreign_keys = ON');\n db.exec(DDL);\n db.exec(vecDdl(dim));\n db.exec(FTS_DDL);\n\n // Migration: dir_summaries.slug_list 字段是后加的,老库要补。\n // 数据每次 buildLayeredIndex 都会全量重建,ALTER 完留空数组即可。\n const dirCols = db.prepare('PRAGMA table_info(dir_summaries)').all() as {\n name: string;\n }[];\n if (!dirCols.some((c) => c.name === 'slug_list')) {\n db.exec(`ALTER TABLE dir_summaries ADD COLUMN slug_list TEXT NOT NULL DEFAULT '[]'`);\n }\n\n return db;\n}\n","/**\n * Ollama embedding client — calls local ollama /api/embed endpoint.\n */\n\nconst OLLAMA_URL = 'http://localhost:11434/api/embed';\nconst DEFAULT_MODEL = 'bge-m3';\n\nexport async function embed(texts: string[], model = DEFAULT_MODEL): Promise {\n const payload = JSON.stringify({ model, input: texts });\n\n let resp: Response;\n try {\n resp = await fetch(OLLAMA_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: payload,\n signal: AbortSignal.timeout(120_000),\n });\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e);\n throw new Error(\n `Cannot connect to ollama at ${OLLAMA_URL}: ${msg}\\n` +\n ` Make sure ollama is running: ollama serve\\n` +\n ` And the model is pulled: ollama pull ${model}`,\n );\n }\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => '');\n throw new Error(`ollama returned ${resp.status}: ${body}`);\n }\n\n const data = (await resp.json()) as { embeddings?: number[][] };\n const embeddings = data.embeddings ?? [];\n return embeddings.map((e) => new Float32Array(e));\n}\n\nexport async function embedSingle(text: string, model = DEFAULT_MODEL): Promise {\n const results = await embed([text], model);\n return results[0];\n}\n","/**\n * Markdown file chunker — splits .md files into embeddable chunks.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { basename, relative } from 'node:path';\nimport matter from 'gray-matter';\n\nconst MAX_CHUNK_CHARS = 800;\nconst MIN_CHUNK_CHARS = 20;\n\nexport interface Chunk {\n section: string;\n content: string;\n}\n\nexport function chunkFile(filePath: string, corpusRoot: string): Chunk[] {\n const raw = readFileSync(filePath, 'utf-8');\n const { data: fm, content: body } = matter(raw);\n\n let title = (fm.title as string) || '';\n const type = (fm.type as string) || '';\n\n if (!title) {\n const m = body.match(/^#\\s+(.+)/m);\n title = m ? m[1].trim() : basename(filePath, '.md');\n }\n\n // Split body by ## headings\n const parts = body.split(/^(## .+)$/m);\n\n const sections: Array<[string, string]> = [];\n if (parts[0].trim()) {\n sections.push(['_intro', parts[0]]);\n }\n for (let i = 1; i < parts.length - 1; i += 2) {\n const heading = parts[i].replace(/^#+\\s*/, '').trim();\n const secBody = i + 1 < parts.length ? parts[i + 1] : '';\n sections.push([heading, secBody]);\n }\n\n let prefix = '';\n if (title) prefix += `[${title}] `;\n if (type) prefix += `[${type}] `;\n\n const chunks: Chunk[] = [];\n\n for (const [heading, secBody] of sections) {\n const trimmed = secBody.trim();\n if (!trimmed || trimmed.length < MIN_CHUNK_CHARS) continue;\n\n if (trimmed.length > MAX_CHUNK_CHARS) {\n const paragraphs = trimmed.split('\\n\\n');\n let current = '';\n for (const p of paragraphs) {\n if (current.length + p.length > MAX_CHUNK_CHARS && current) {\n chunks.push({ section: heading, content: prefix + current.trim() });\n current = p;\n } else {\n current = current ? current + '\\n\\n' + p : p;\n }\n }\n if (current.trim()) {\n chunks.push({ section: heading, content: prefix + current.trim() });\n }\n } else {\n chunks.push({ section: heading, content: prefix + trimmed });\n }\n }\n\n return chunks;\n}\n","/**\n * vectordb/sync.ts — 单文件增量同步(写路径)\n *\n * 批次 22b strangler fig 第二步:从 src/lib/vectordb.ts copy 出 syncFile。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 拿到一个 corpus 内的 markdown 路径 + db 句柄 + embed 回调,把该文件的 chunks\n * 全量重建(先级联清旧 doc → 重新切块 → embed → 写 documents/chunks/vec_chunks/fts_chunks)\n * - **不**重建 page_summaries / vec_pages / fts_pages(那是 buildLayeredIndex 的活)\n *\n * 调用流程上由 commands/vector.ts 的 sync 子命令在 collectFiles 列表上 batch 调用。\n */\n\nimport { relative } from 'node:path';\n\nimport { sha256, float32ToBuffer } from './files.js';\nimport type { Db } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * embedding 回调签名。inline 定义保持 22b 不依赖未上提的共享类型;22e/22f 收尾时\n * 若 query 系列子批也用到再考虑上提到 schema.ts(当前 grep 仅 sync/build-layered 用到)。\n */\ntype EmbedFn = (texts: string[]) => Promise;\n\n// ---------------------------------------------------------------------------\n// syncFile\n// ---------------------------------------------------------------------------\n\n/**\n * 单文件全量重建:\n * 1. 查 documents.path == rel 的旧 doc,存在则先级联清 chunks/vec_chunks/fts_chunks\n * + page_summaries/vec_pages/fts_pages,再删 documents\n * - **顺序敏感**:page_summaries 有 FK ON documents(id),必须先于 documents 删\n * - vec_* / fts_* 是 sqlite-vec / FTS5 的虚表,rowid 跟对应 SQL 表 id 对齐,\n * 手动 DELETE WHERE rowid = ?;不能依赖外键级联\n * 2. INSERT documents(path / sha256 / updated_at)\n * 3. chunkFile() 切块;空 chunks 直接返回\n * 4. embedFn(texts) 批量 embed\n * 5. 逐 chunk INSERT chunks → 拿 last_insert_rowid → INSERT vec_chunks(rowid) 同 id\n * → INSERT fts_chunks(rowid) 同 id\n *\n * 副作用:仅写入;返回 `{ chunks: number }` 让调用方汇总。\n */\nexport async function syncFile(\n db: Db,\n filePath: string,\n corpus: string,\n embedFn: EmbedFn,\n): Promise<{ chunks: number }> {\n const { chunkFile } = await import('../chunker.js');\n\n const rel = relative(corpus, filePath);\n const sha = sha256(filePath);\n\n // Remove old data — chunks / page_summaries 都引用 documents.id,要级联清;\n // 每张 virtual table(vec_*, fts_*)的 rowid 跟对应 SQL 表的 id 一一对应。\n const old = db.prepare('SELECT id FROM documents WHERE path = ?').get(rel) as\n | { id: number }\n | undefined;\n if (old) {\n // 1. chunks + vec_chunks + fts_chunks\n const chunkIds = db.prepare('SELECT id FROM chunks WHERE doc_id = ?').all(old.id) as {\n id: number;\n }[];\n const delVecChunk = db.prepare('DELETE FROM vec_chunks WHERE rowid = ?');\n const delFtsChunk = db.prepare('DELETE FROM fts_chunks WHERE rowid = ?');\n for (const { id } of chunkIds) {\n delVecChunk.run(id);\n delFtsChunk.run(id);\n }\n db.prepare('DELETE FROM chunks WHERE doc_id = ?').run(old.id);\n\n // 2. page_summaries + vec_pages + fts_pages(外键拦 documents 删除,必须先清)\n const pageIds = db.prepare('SELECT id FROM page_summaries WHERE doc_id = ?').all(old.id) as {\n id: number;\n }[];\n const delVecPage = db.prepare('DELETE FROM vec_pages WHERE rowid = ?');\n const delFtsPage = db.prepare('DELETE FROM fts_pages WHERE rowid = ?');\n for (const { id } of pageIds) {\n delVecPage.run(id);\n delFtsPage.run(id);\n }\n db.prepare('DELETE FROM page_summaries WHERE doc_id = ?').run(old.id);\n\n // 3. 最后删 documents\n db.prepare('DELETE FROM documents WHERE id = ?').run(old.id);\n }\n\n const now = new Date().toISOString();\n db.prepare('INSERT INTO documents (path, sha256, updated_at) VALUES (?, ?, ?)').run(\n rel,\n sha,\n now,\n );\n const docRow = db.prepare('SELECT id FROM documents WHERE path = ?').get(rel) as {\n id: number;\n };\n const docId = docRow.id;\n\n const chunks = chunkFile(filePath, corpus);\n if (chunks.length === 0) return { chunks: 0 };\n\n const texts = chunks.map((c) => c.content);\n const embeddings = await embedFn(texts);\n\n const insertChunk = db.prepare(\n 'INSERT INTO chunks (doc_id, section, content, embedding) VALUES (?, ?, ?, ?)',\n );\n const insertFts = db.prepare('INSERT INTO fts_chunks(rowid, content) VALUES (?, ?)');\n for (let i = 0; i < chunks.length; i++) {\n const blob = float32ToBuffer(embeddings[i]);\n insertChunk.run(docId, chunks[i].section, chunks[i].content, blob);\n const chunkId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n // vec0 doesn't support bound params for rowid — must inline\n db.prepare(`INSERT INTO vec_chunks (rowid, embedding) VALUES (${chunkId}, ?)`).run(blob);\n insertFts.run(chunkId, chunks[i].content);\n }\n\n return { chunks: chunks.length };\n}\n","/**\n * vectordb/build-layered-index.ts — L0/L1 分层索引重建(写路径)\n *\n * 批次 22b strangler fig 第二步:从 src/lib/vectordb.ts copy 出 buildLayeredIndex\n * + 3 个内部 helper(parseIndexSections / parseIndexEntries / findAllIndexFiles)。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - L0: 从 `corpus/index.md` 按 `## section` 切,每段一条 vec_dirs / fts_dirs 行\n * - L1: 从所有 `{dir}/_INDEX.md` 表格行解析,每条目一条 vec_pages / fts_pages 行\n * (summary 入向量;slug + summary 入 FTS)\n * - 重建是\"全量替换\":先 DELETE FROM dir_summaries / vec_dirs / fts_dirs / page_*\n *\n * 依赖 syncFile 已先跑过:因为 L1 用 `slug → doc_id` 映射,doc_id 来自 documents 表\n * (sync 阶段建立)。typical commands/vector.ts sync 流程:collectFiles → syncFile loop\n * → buildLayeredIndex。\n *\n * **23a 改动**:7 处进度提示 `console.log` → `logger.info`(stderr,不污染\n * `lorekit sync | jq` 管道)。findAllIndexFiles 的沉默 catch → `logger.warn` +\n * 注释说明为何可以继续。对应 LEGACY P2-2 / P2-4 vectordb 残留清零。\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\n\nimport matter from 'gray-matter';\n\nimport { vectorExcludePrefixes } from '../paths.js';\nimport * as logger from '../../utils/logger.js';\nimport { float32ToBuffer } from './files.js';\nimport type { Db } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * embedding 回调签名。inline 同 sync.ts;22e/22f 收尾决定是否上提到 schema.ts。\n */\ntype EmbedFn = (texts: string[]) => Promise;\n\n// ---------------------------------------------------------------------------\n// parseIndexSections — 解析 corpus/index.md 的 ## 分区\n// ---------------------------------------------------------------------------\n\n/**\n * 把 `corpus/index.md` 按 `## section name` 切成多段。\n *\n * 仅返回\"含至少一行 `- [[xxx]]` 或 `* [[xxx]]` 条目\"的分区——纯说明性段(如\n * \"## How to use this wiki\" 不含 wikilink 列表)会被滤掉,不进 L0 向量。\n *\n * 每段返回:\n * - `name`:section 标题(去 ## 与首尾空白)\n * - `text`:完整段文本(含 `## name` 行 + 后续所有行 join('\\n').trim())\n * - `slugs`:去重后的主 slug 列表(每行 `- [[slug]]` 或 `* [[slug]]` 取 wikilink 第一个)\n */\nfunction parseIndexSections(\n content: string,\n): Array<{ name: string; text: string; slugs: string[] }> {\n const lines = content.split('\\n');\n const sections: Array<{ name: string; lines: string[] }> = [];\n let current: { name: string; lines: string[] } | null = null;\n\n for (const line of lines) {\n const m = line.match(/^##\\s+(.+?)\\s*$/);\n if (m) {\n if (current) sections.push(current);\n current = { name: m[1].trim(), lines: [line] };\n } else if (current) {\n current.lines.push(line);\n }\n }\n if (current) sections.push(current);\n\n // 每条目主 slug:行首 `- [[slug]]`(或 `* [[slug]]`),只取第一个 wikilink\n const entrySlugRe = /^\\s*[-*]\\s*\\[\\[([^\\]|#]+?)\\]\\]/;\n\n return sections\n .filter((s) => /^\\s*[-*]\\s/m.test(s.lines.slice(1).join('\\n')))\n .map((s) => {\n const slugs: string[] = [];\n for (const line of s.lines.slice(1)) {\n const m = line.match(entrySlugRe);\n if (m) slugs.push(m[1].trim());\n }\n return {\n name: s.name,\n text: s.lines.join('\\n').trim(),\n slugs: [...new Set(slugs)],\n };\n });\n}\n\n// ---------------------------------------------------------------------------\n// parseIndexEntries — 解析 {dir}/_INDEX.md 的表格行\n// ---------------------------------------------------------------------------\n\n/**\n * 解析 `{dir}/_INDEX.md` 表格行:`| [[slug]] | summary | updated |`\n * 跳过表头(含\"条目\"二字)和分隔行(全是 - 和 |)\n */\nfunction parseIndexEntries(content: string): Array<{ slug: string; summary: string }> {\n const lines = content.split('\\n');\n const entries: Array<{ slug: string; summary: string }> = [];\n\n for (const line of lines) {\n if (/^\\|\\s*条目\\s*\\|/.test(line)) continue;\n if (/^\\|[\\s\\-|]+\\|?\\s*$/.test(line)) continue;\n\n const m = line.match(/^\\|\\s*\\[\\[([^\\]|#]+?)\\]\\]\\s*\\|\\s*(.*?)\\s*\\|\\s*(.*?)\\s*\\|/);\n if (!m) continue;\n const slug = m[1].trim();\n const summary = m[2].replace(/\\\\\\|/g, '|').trim();\n entries.push({ slug, summary });\n }\n\n return entries;\n}\n\n// ---------------------------------------------------------------------------\n// findAllIndexFiles — 递归找所有 _INDEX.md\n// ---------------------------------------------------------------------------\n\n/**\n * 递归扫 corpus 找所有 `_INDEX.md`,复用 paths.ts 的 vectorExcludePrefixes 排除规则。\n * 同时跳过 `.` 起头的目录(.git / .wiki / .obsidian 等约定隐藏目录)。\n */\nfunction findAllIndexFiles(corpus: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string) {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch (e) {\n // 目录读不到(权限 / 临时被删 / 扫到 symlink 循环)就跳,整体扫描继续。\n logger.warn(`findAllIndexFiles: skip ${dir} (${(e as Error).message})`);\n return;\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue;\n const full = join(dir, entry.name);\n const rel = relative(corpus, full);\n if (vectorExcludePrefixes.some((p) => rel === p || rel.startsWith(p + '/'))) continue;\n\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name === '_INDEX.md') {\n results.push(full);\n }\n }\n }\n walk(corpus);\n return results.sort();\n}\n\n// ---------------------------------------------------------------------------\n// buildLayeredIndex — L0 + L1 全量重建\n// ---------------------------------------------------------------------------\n\nexport async function buildLayeredIndex(db: Db, corpus: string, embedFn: EmbedFn): Promise {\n // --- L0: 从 corpus/index.md 按 ## 分区切,每区一条向量 + 一条 FTS ---\n db.prepare('DELETE FROM dir_summaries').run();\n db.prepare('DELETE FROM vec_dirs').run();\n db.prepare('DELETE FROM fts_dirs').run();\n\n const indexPath = join(corpus, 'index.md');\n if (!existsSync(indexPath)) {\n logger.info(' L0: corpus/index.md not found, skipped');\n } else {\n const raw = readFileSync(indexPath, 'utf-8');\n const { content } = matter(raw);\n const sections = parseIndexSections(content);\n\n if (sections.length === 0) {\n logger.info(' L0: no sections with entries in index.md, skipped');\n } else {\n const texts = sections.map((s) => s.text);\n const embeddings = await embedFn(texts);\n\n const insertDir = db.prepare(\n 'INSERT INTO dir_summaries (dir_path, summary, embedding, slug_list) VALUES (?, ?, ?, ?)',\n );\n const insertFtsDir = db.prepare('INSERT INTO fts_dirs(rowid, summary) VALUES (?, ?)');\n for (let i = 0; i < sections.length; i++) {\n const blob = float32ToBuffer(embeddings[i]);\n const slugListJson = JSON.stringify(sections[i].slugs);\n insertDir.run(sections[i].name, sections[i].text, blob, slugListJson);\n const dirId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n db.prepare(`INSERT INTO vec_dirs (rowid, embedding) VALUES (${dirId}, ?)`).run(blob);\n insertFtsDir.run(dirId, sections[i].text);\n }\n const totalSlugs = sections.reduce((a, s) => a + s.slugs.length, 0);\n logger.info(\n ` L0: indexed ${sections.length} sections from index.md (${totalSlugs} slugs tracked)`,\n );\n }\n }\n\n // --- L1: 从各 _INDEX.md 的每行条目,每条一条向量 + 一条 FTS ---\n db.prepare('DELETE FROM page_summaries').run();\n db.prepare('DELETE FROM vec_pages').run();\n db.prepare('DELETE FROM fts_pages').run();\n\n const indexFiles = findAllIndexFiles(corpus);\n if (indexFiles.length === 0) {\n logger.info(' L1: no _INDEX.md found, skipped');\n return;\n }\n\n const allEntries: Array<{ slug: string; summary: string }> = [];\n for (const f of indexFiles) {\n const raw = readFileSync(f, 'utf-8');\n allEntries.push(...parseIndexEntries(raw));\n }\n\n if (allEntries.length === 0) {\n logger.info(' L1: no entries parsed from _INDEX.md, skipped');\n return;\n }\n\n // 建 slug → doc_id 映射(兼容目录包装式和去/不去 .md 后缀)\n const docRows = db.prepare('SELECT id, path FROM documents').all() as {\n id: number;\n path: string;\n }[];\n const slugToDocId = new Map();\n for (const { id, path } of docRows) {\n slugToDocId.set(path, id);\n slugToDocId.set(path.replace(/\\.md$/, ''), id);\n if (path.endsWith('/article.md')) {\n slugToDocId.set(path.replace(/\\/article\\.md$/, ''), id);\n }\n }\n\n const matched: Array<{ docId: number; text: string; slug: string }> = [];\n let unmatched = 0;\n for (const e of allEntries) {\n const docId = slugToDocId.get(e.slug);\n if (docId === undefined) {\n unmatched++;\n continue;\n }\n // 向量输入用 summary;summary 缺失时退回 slug(至少有语义路径)\n const text =\n e.summary && e.summary !== '—' && e.summary !== '(缺少 frontmatter)' ? e.summary : e.slug;\n matched.push({ docId, text, slug: e.slug });\n }\n\n if (matched.length === 0) {\n logger.info(' L1: no _INDEX.md entries matched documents, skipped');\n return;\n }\n\n const BATCH = 64;\n const insertPage = db.prepare(\n 'INSERT INTO page_summaries (doc_id, summary, embedding) VALUES (?, ?, ?)',\n );\n const insertFtsPage = db.prepare('INSERT INTO fts_pages(rowid, summary) VALUES (?, ?)');\n for (let i = 0; i < matched.length; i += BATCH) {\n const batch = matched.slice(i, i + BATCH);\n const texts = batch.map((m) => m.text);\n const embeddings = await embedFn(texts);\n for (let j = 0; j < batch.length; j++) {\n const blob = float32ToBuffer(embeddings[j]);\n insertPage.run(batch[j].docId, batch[j].text, blob);\n const pageId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n db.prepare(`INSERT INTO vec_pages (rowid, embedding) VALUES (${pageId}, ?)`).run(blob);\n // FTS 索引内容 = slug + summary,让 BM25 也能通过路径(含实体名)命中;\n // 向量只索引 summary,保持语义纯净不被路径噪声污染。\n insertFtsPage.run(pageId, `${batch[j].slug} ${batch[j].text}`);\n }\n }\n\n let msg = ` L1: indexed ${matched.length} entries from ${indexFiles.length} _INDEX.md`;\n if (unmatched > 0) msg += ` (${unmatched} unmatched slug, skipped)`;\n logger.info(msg);\n}\n","/**\n * vectordb/query-flat.ts — 单层向量召回(最简实现,对照 layered 的 baseline)\n *\n * 批次 22c strangler fig 第三步:从 src/lib/vectordb.ts copy 出 queryFlat。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 单一 vec_chunks 表 ANN top-K 召回\n * - threshold 过滤后 JOIN documents 拿 file path 与 section 元数据\n * - 不做分层、不做 parent boost、不做 BM25 融合(那些在 query-layered / query-bm25 /\n * query-hybrid 各自负责)\n *\n * 适用:调试 / 小语料 / 不想要 layered 复杂逻辑时;commands/vector.ts 的\n * `vector query` 默认走 queryHybrid,flat 通过显式 flag 触发。\n */\n\nimport { distanceToScore, float32ToBuffer } from './files.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// queryFlat\n// ---------------------------------------------------------------------------\n\n/**\n * 单层向量 query:\n * - vec_chunks MATCH ? ORDER BY distance LIMIT topK\n * - 距离 → 相似度(distanceToScore:1 - d²/2)\n * - 低于 threshold 的 chunk 丢弃\n * - 用 chunks.id JOIN documents 拿 path / section\n *\n * 返回的 QueryResult.score 保留 4 位小数(`Math.round(score * 10000) / 10000`),\n * 跟 layered / bm25 / hybrid 的输出保持一致便于下游对比。\n */\nexport function queryFlat(\n db: Db,\n embedding: Float32Array,\n topK: number,\n threshold: number,\n): QueryResult[] {\n const blob = float32ToBuffer(embedding);\n\n const rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_chunks v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, topK) as { id: number; distance: number }[];\n\n const results: QueryResult[] = [];\n const getChunk = db.prepare(\n `SELECT c.content, c.section, d.path\n FROM chunks c JOIN documents d ON c.doc_id = d.id\n WHERE c.id = ?`,\n );\n\n for (const row of rows) {\n const score = distanceToScore(row.distance);\n if (score < threshold) continue;\n const cr = getChunk.get(row.id) as\n | { content: string; section: string; path: string }\n | undefined;\n if (cr) {\n results.push({\n file: cr.path,\n chunk: cr.content,\n score: Math.round(score * 10000) / 10000,\n section: cr.section,\n });\n }\n }\n\n return results;\n}\n","/**\n * vectordb/query-layered.ts — L0/L1/L2 三层向量召回\n *\n * 批次 22c strangler fig 第三步:从 src/lib/vectordb.ts copy 出 queryLayered。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 三层结构(基于 buildLayeredIndex 建立的索引):\n *\n * - **L0** vec_dirs:top-3 sections(index.md 的 ## 分区向量)\n * → 用 dir_summaries.slug_list 收集\"覆盖到的所有候选 slug\"\n *\n * - **L1** vec_pages:在 L0 候选 doc_id 范围内召回 top-5 pages\n * → searchK = min(候选页数, 50);vec0 只能限 K 不能限范围,先全召回再 filter\n *\n * - **L2** vec_chunks:在 L1 命中页的 chunks 范围内召回 topK\n * → searchK2 = min(候选 chunks 数, topK*5);同样先召回再 filter\n *\n * 每层任意阶段命中为空 → 短路返回 `[]`(不退化到全库 flat 召回)。\n *\n * 设计取舍:分层把\"语义召回\"分阶段约束在\"结构相关\"的子集,避免大库 ANN\n * 误命中冷门文档。代价是命中阈值越低 / 库规模越小时,可能比 flat 漏召。\n */\n\nimport { distanceToScore, float32ToBuffer } from './files.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// queryLayered\n// ---------------------------------------------------------------------------\n\nexport function queryLayered(\n db: Db,\n embedding: Float32Array,\n topK: number,\n threshold: number,\n): QueryResult[] {\n const blob = float32ToBuffer(embedding);\n\n // **23c 改:cap 联动 topK** —— 3/5 是 Karpathy 原文 baseline(小 topK 精度优先),\n // topK 上来时按比例放大避免过早收窄。topK=10 默认:L0_K=3, L1_CAP=5(向后兼容)。\n // topK=100 时:L0_K=10, L1_CAP=20,让 L0 覆盖更多分区 / L1 保留更多候选页。\n const L0_K = Math.max(3, Math.ceil(topK / 10));\n const L1_CAP = Math.max(5, Math.ceil(topK / 5));\n\n // L0: top-L0_K sections(分区向量,每个分区 = index.md 里一个 ## 区块)\n const l0Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_dirs v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, L0_K) as { id: number; distance: number }[];\n\n if (l0Rows.length === 0) return [];\n\n // 从 dir_summaries.slug_list 拿 L0 命中分区覆盖的所有 slug\n const dirIds = l0Rows.map((r) => r.id);\n const dirRows = db\n .prepare(`SELECT slug_list FROM dir_summaries WHERE id IN (${dirIds.map(() => '?').join(',')})`)\n .all(...dirIds) as { slug_list: string }[];\n\n const candidateSlugs = new Set();\n for (const row of dirRows) {\n try {\n const list = JSON.parse(row.slug_list) as string[];\n for (const s of list) candidateSlugs.add(s);\n } catch {\n // slug_list 异常(老库未迁移时可能是空串)→ 跳过\n }\n }\n\n if (candidateSlugs.size === 0) return [];\n\n // 把 slug 映射成 doc_id(兼容目录包装式 slug、去/不去 .md 后缀)\n // **23c 改:变量 rename** `candidateDocIds` → `L0CandidateDocIds` 按层语义命名\n const docRows = db.prepare('SELECT id, path FROM documents').all() as {\n id: number;\n path: string;\n }[];\n const L0CandidateDocIds = new Set();\n for (const { id, path } of docRows) {\n const stem = path.replace(/\\.md$/, '');\n const folderSlug = path.endsWith('/article.md') ? path.replace(/\\/article\\.md$/, '') : null;\n if (candidateSlugs.has(path) || candidateSlugs.has(stem)) {\n L0CandidateDocIds.add(id);\n } else if (folderSlug && candidateSlugs.has(folderSlug)) {\n L0CandidateDocIds.add(id);\n }\n }\n\n if (L0CandidateDocIds.size === 0) return [];\n\n // L1: top-L1_CAP pages,候选限定在 L0 命中分区覆盖的 doc_id\n // **23c 改:变量 rename** `docIdArr` → `L0CandidateDocIdArr`(L0 doc_id 的 array view)\n const L0CandidateDocIdArr = [...L0CandidateDocIds];\n const candidatePageIds = db\n .prepare(\n `SELECT id FROM page_summaries WHERE doc_id IN (${L0CandidateDocIdArr.map(() => '?').join(',')})`,\n )\n .all(...L0CandidateDocIdArr) as { id: number }[];\n\n if (candidatePageIds.length === 0) return [];\n\n const searchK = Math.min(candidatePageIds.length, 50);\n const l1Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_pages v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, searchK) as { id: number; distance: number }[];\n\n const candidateSet = new Set(candidatePageIds.map((r) => r.id));\n const l1Filtered = l1Rows.filter((r) => candidateSet.has(r.id)).slice(0, L1_CAP);\n\n if (l1Filtered.length === 0) return [];\n\n // Get doc_ids from matched page summaries\n // **23c 改:变量 rename** `docIds` → `L1CandidateDocIds`(L1 命中页对应的 doc_id 对象数组)\n const pageIds = l1Filtered.map((r) => r.id);\n const L1CandidateDocIds = db\n .prepare(\n `SELECT DISTINCT doc_id FROM page_summaries WHERE id IN (${pageIds.map(() => '?').join(',')})`,\n )\n .all(...pageIds) as { doc_id: number }[];\n\n if (L1CandidateDocIds.length === 0) return [];\n\n // L2: chunks within matched docs\n // **23c 改:变量 rename** `docIdList` → `L1CandidateDocIdList`(plain number[] 形式)\n const L1CandidateDocIdList = L1CandidateDocIds.map((r) => r.doc_id);\n const candidateChunkIds = db\n .prepare(\n `SELECT id FROM chunks WHERE doc_id IN (${L1CandidateDocIdList.map(() => '?').join(',')})`,\n )\n .all(...L1CandidateDocIdList) as { id: number }[];\n\n if (candidateChunkIds.length === 0) return [];\n\n const searchK2 = Math.min(candidateChunkIds.length, topK * 5);\n const l2Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_chunks v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, searchK2) as { id: number; distance: number }[];\n\n const chunkSet = new Set(candidateChunkIds.map((r) => r.id));\n const l2Filtered = l2Rows.filter((r) => chunkSet.has(r.id)).slice(0, topK);\n\n const results: QueryResult[] = [];\n const getChunk = db.prepare(\n `SELECT c.content, c.section, d.path\n FROM chunks c JOIN documents d ON c.doc_id = d.id\n WHERE c.id = ?`,\n );\n\n for (const row of l2Filtered) {\n const score = distanceToScore(row.distance);\n if (score < threshold) continue;\n const cr = getChunk.get(row.id) as\n | { content: string; section: string; path: string }\n | undefined;\n if (cr) {\n results.push({\n file: cr.path,\n chunk: cr.content,\n score: Math.round(score * 10000) / 10000,\n section: cr.section,\n });\n }\n }\n\n return results;\n}\n","/**\n * vectordb/query-bm25.ts — BM25(FTS5)chunk 层直查\n *\n * **批次 24-fix(2026-04-19)重写**:原 layered 三层 (fts_dirs → fts_pages →\n * fts_chunks) 在 21 引入时设计错误——L0 的 dir 摘要只含目录标题 + wikilink 列表,\n * **不含正文**,用户关键词几乎永不命中 L0;一旦 L0 空集整条链路 `return []`,\n * 导致 `lorekit vector query --bm25 --text \"browser-use\"` 这种最朴素的调用\n * 返回空。这个缺陷隐藏在 hybrid 融合后(向量路补救),22 系列 byte-level 拆分\n * 验证没抓到,22f 真实 ingest 验收时先生发现真实 corpus 里 BM25 永远空才暴露。\n *\n * 方案 X(规划方批准):BM25 不分层,直接 fts_chunks MATCH + rank 排 topK。\n * 依据:\n * - BM25 本身就用 rank 排精度,不需要 L0/L1 预先缩候选集\n * - dir / page 摘要不含正文是架构事实(buildLayeredIndex 写入语义),不是 bug\n * - 向量路的 queryLayered 保留 L0 gate(向量相似度下 L0 能做语义 gate)\n *\n * 函数名 / 签名 / 返回类型全部保留,commands / query-hybrid 无需改 import。\n *\n * 适用:精确实体名 / 日期 / 专有名词召回(\"Harness\" / \"Anthropic\" / \"2026-04-15\")。\n * 中英混合复合短语建议走向量或 hybrid。\n */\n\nimport * as logger from '../../utils/logger.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// sanitizeFtsQuery — FTS5 查询字符串清洗\n// ---------------------------------------------------------------------------\n\n/**\n * FTS5 查询字符串清洗:\n * - 去掉 FTS5 运算符字符(\" * : ^ ( ))和保留关键字(OR/AND/NOT/NEAR)\n * - trigram tokenizer 下短于 3 字符的 token 无法命中,过滤掉\n * - 多 token 之间用空格连接(FTS5 默认 AND 语义)\n *\n * 为什么不用短语搜索(\"...\"):短语要求整条 trigram 序列完全连续匹配,中英混合\n * 或带空格的 query 几乎永远命中不上。默认 AND 更宽松,跟 BM25 的\"精确关键词\"\n * 定位一致——先生查\"Harness\"/\"Anthropic\"这种精确实体名能命中;查\"Harness 五版\n * 演化\"这种复合短语 BM25 空是合理的(语义匹配应该走向量或 Hybrid)。\n *\n * **23b 修**:`\\d{4}-\\d{2}-\\d{2}` 完整 ISO 日期 protect-and-restore,避免被\n * `-` 拆 token 退化为 `2026`。流程:\n * 1. 提取所有 ISO 日期,用 `__DATE0__` / `__DATE1__` 占位符替换(前后空格保证分词)\n * 2. 跑现有 sanitize(占位符 `_` 不在 FTS5 运算符 set 里,整串 9 字符 > 3 通过过滤)\n * 3. 把占位符还原为 `\"YYYY-MM-DD\"`(双引号包裹让 FTS5 当 phrase token,\n * 避免 `-` 被解析成 NOT 前缀)\n *\n * 不识别 `2026/04/15`(`/` 不在原 sanitize 拆分字符里,本来就 OK 不需要保护)。\n * 不识别 `2026-4-15`(年月日不补 0 的非标准格式,避免误识别行内 hyphenated 词如\n * `self-hosted`)。\n */\nfunction sanitizeFtsQuery(q: string): string {\n // 1. protect ISO dates\n const dates: string[] = [];\n const protectedQ = q.replace(/\\d{4}-\\d{2}-\\d{2}/g, (m) => {\n const i = dates.length;\n dates.push(m);\n return ` __DATE${i}__ `;\n });\n\n // 2. 现有 sanitize 流程\n // FTS5 运算符:\" * : ^ ( ) - +(`-` 前缀是 NOT,内部的 `-` 也会让日期类 query 失效)\n let s = protectedQ.replace(/[\"*:^()\\-+]/g, ' ');\n s = s.replace(/\\b(OR|AND|NOT|NEAR)\\b/gi, ' ');\n s = s.replace(/\\s+/g, ' ').trim();\n if (!s) return '';\n const tokens = s.split(' ').filter((t) => t.length >= 3);\n if (tokens.length === 0) return '';\n\n // 3. 还原占位符为 quoted 完整日期(FTS5 phrase syntax)\n const restored = tokens.map((t) => {\n const m = t.match(/^__DATE(\\d+)__$/);\n return m ? `\"${dates[Number(m[1])]}\"` : t;\n });\n return restored.join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// queryBM25Layered — BM25 chunk 层直查(名字保留,语义改为单层)\n// ---------------------------------------------------------------------------\n\n/**\n * BM25 召回:fts_chunks MATCH + rank 排序,topK 截断。\n *\n * **不再走 layered 三层**——L0/L1 的 dir/page 摘要不含正文,永不命中用户关键词\n * (详见文件头注释)。改为对 fts_chunks 直接 MATCH,rank 列就是 BM25 负数,\n * 越小越相关;返回时归一为正数。\n *\n * 命名保留 `queryBM25Layered` 是为了兼容现有 import(query-hybrid / commands/vector /\n * lib/vectordb/index)。后续批次若要改名,整个 import 图一起改。\n *\n * 失败路径:FTS5 对 sanitizeFtsQuery 后的 query 仍可能因边界 token 抛错(纯 trigram\n * 不可分串),catch 后返回 `[]` 让上层 hybrid 优雅降级到纯向量;失败原因走 stderr\n * 便于 debug。\n */\nexport function queryBM25Layered(db: Db, queryText: string, topK: number): QueryResult[] {\n const ftsQ = sanitizeFtsQuery(queryText);\n if (!ftsQ) return [];\n\n let rows: {\n rank: number;\n content: string;\n section: string | null;\n path: string;\n }[] = [];\n try {\n rows = db\n .prepare(\n `SELECT fc.rank as rank, c.content as content, c.section as section, d.path as path\n FROM fts_chunks fc\n JOIN chunks c ON fc.rowid = c.id\n JOIN documents d ON c.doc_id = d.id\n WHERE fc.fts_chunks MATCH ?\n ORDER BY fc.rank\n LIMIT ?`,\n )\n .all(ftsQ, topK) as {\n rank: number;\n content: string;\n section: string | null;\n path: string;\n }[];\n } catch (e) {\n // FTS5 边界 token 失败 → BM25 整体降级为空,上层 hybrid 回退纯向量\n logger.warn(`queryBM25Layered fts5 match: ${(e as Error).message}`);\n return [];\n }\n\n return rows.map((r) => ({\n file: r.path,\n chunk: r.content,\n // FTS5 rank 是负数(越小越相关),取绝对值作为正向分数;归一化留给 RRF\n score: Math.round(-r.rank * 10000) / 10000,\n section: r.section ?? '',\n }));\n}\n","/**\n * vectordb/query-hybrid.ts — 向量 + BM25 双路 RRF 融合\n *\n * 批次 22d strangler fig 第四步:从 src/lib/vectordb.ts copy 出 rrfMerge + queryHybrid。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 这是 4 路 query(flat / layered / bm25 / hybrid)里的\"组合\"端,自身不写 db query:\n * - 调 22c 的 `queryLayered` 拿向量三层结果\n * - 调本批的 `queryBM25Layered` 拿 BM25 三层结果\n * - 用 `rrfMerge` 把两路按 Reciprocal Rank Fusion 合并\n *\n * commands/vector.ts 的 `vector query --hybrid` 走这里;不带 flag 默认也是 hybrid\n * (根据当前 cli.ts 设定)。\n */\n\nimport { createHash } from 'node:crypto';\n\nimport { queryBM25Layered } from './query-bm25.js';\nimport { queryLayered } from './query-layered.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// rrfMerge — Reciprocal Rank Fusion\n// ---------------------------------------------------------------------------\n\n/**\n * Reciprocal Rank Fusion — 多路召回结果的排名合并。\n * 公式:score(item) = Σ 1 / (k + rank_i) (rank 从 1 开始,k 默认 60)\n * 在两路都靠前的 item 最终 score 最高。\n *\n * **23b 修**:dedup key 从 `${file}::${chunk.slice(0, 80)}` 改为\n * `${file}::${sha256(chunk).slice(0, 16)}`。\n *\n * 原代码用前 80 字做 dedup,中文长文档 chunk 段首固定开场白时(如\n * \"[title] [type]\\n\\n# title\\n## section\\n...\")两个真实不同的 chunk 前 80 字\n * 可能完全相同 → 被错误合并为 1 条,丢正确召回(22d B6 mock case 复现)。\n *\n * 改用 sha256 前 16 hex char(64 bits)作为 chunk 内容指纹,避碰概率 ≈ 2^-32 全文档级\n * 仍极低,且对 chunk 全文敏感不再依赖前缀重复程度。规划方决策:64 bits 足够,\n * 不用全 64 hex 节省 key 空间。\n */\nexport function rrfMerge(lists: QueryResult[][], topK: number, k: number = 60): QueryResult[] {\n // key = file + chunk sha256 前 16 hex(64 bits 指纹),value = { item, rrf }\n const merged = new Map();\n for (const list of lists) {\n list.forEach((item, i) => {\n const fingerprint = createHash('sha256').update(item.chunk).digest('hex').slice(0, 16);\n const key = `${item.file}::${fingerprint}`;\n const rrf = 1 / (k + i + 1);\n const prev = merged.get(key);\n if (prev) {\n prev.rrf += rrf;\n } else {\n merged.set(key, { item, rrf });\n }\n });\n }\n return [...merged.values()]\n .sort((a, b) => b.rrf - a.rrf)\n .slice(0, topK)\n .map(({ item, rrf }) => ({\n ...item,\n score: Math.round(rrf * 10000) / 10000,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// queryHybrid — 4 路 dispatcher\n// ---------------------------------------------------------------------------\n\n/**\n * Hybrid 分层召回:向量三层 + BM25 三层 + RRF 融合。\n * 跟 queryLayered 同签名(多一个可选 `k` 参数),可在上层命令里用 `--hybrid` flag 切换。\n * 不做 LLM re-rank(先生决定本轮不上,留给未来)。\n *\n * **23c 改**:新增可选 `k` 参数透传到 rrfMerge 的 RRF 公式常数(默认 60)。\n * 暴露此参数是给实验调优用(小 k 让排名靠前 item 权重更突出,大 k 更平滑),\n * cli 暂不暴露 flag(保持 surface 简洁向后兼容)。\n */\nexport function queryHybrid(\n db: Db,\n embedding: Float32Array,\n queryText: string,\n topK: number,\n threshold: number,\n k?: number,\n): QueryResult[] {\n // 两路各召回 topK * 2 作为候选,给 RRF 足够的排名信息\n const candN = topK * 2;\n const vecResults = queryLayered(db, embedding, candN, threshold);\n const bm25Results = queryBM25Layered(db, queryText, candN);\n return rrfMerge([vecResults, bm25Results], topK, k);\n}\n","/**\n * vectordb/status.ts — 检索模式推荐 + 全库 status 元数据\n *\n * 批次 22e strangler fig 第五步:从 src/lib/vectordb.ts copy 出 computeMode + getStatus。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - `computeMode`:纯函数,按 indexed_files 对比 MODE_THRESHOLD_FILES 决定推荐 text / vector\n * - `getStatus`:读路径,打开 db → COUNT 各表 → 读 meta(last_sync/model/dim) → 调\n * collectFiles 拿 total_indexable_files → 调 computeMode 拼 StatusInfo 返回\n *\n * commands/vector.ts 的 `vector status` 子命令直接调 getStatus 拿 JSON 给 wiki-query\n * skill 读 mode 字段决定 text/vector 检索路径。\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport * as logger from '../../utils/logger.js';\nimport { collectFiles } from './files.js';\nimport { EMBEDDING_DIM, MODE_THRESHOLD_FILES, openDb } from './schema.js';\nimport type { StatusInfo } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// computeMode — 纯函数:indexed + indexedFiles → mode + reason\n// ---------------------------------------------------------------------------\n\n/**\n * 根据 indexed_files 决定检索模式推荐。\n * - 向量库未建 → text(没得选)\n * - indexed_files < 阈值 → text(Read 三层精度最高)\n * - indexed_files >= 阈值 → vector(扁平 Read 太慢)\n */\nfunction computeMode(\n indexed: boolean,\n indexedFiles: number,\n): { mode: 'text' | 'vector'; reason: string } {\n if (!indexed) {\n return {\n mode: 'text',\n reason: 'vector index not built; text Read is the only option',\n };\n }\n if (indexedFiles < MODE_THRESHOLD_FILES) {\n return {\n mode: 'text',\n reason: `indexed_files=${indexedFiles} < ${MODE_THRESHOLD_FILES}; Read three-tier is sharpest at small scale`,\n };\n }\n return {\n mode: 'vector',\n reason: `indexed_files=${indexedFiles} >= ${MODE_THRESHOLD_FILES}; flat Read too slow, switch to layered vector retrieval`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// getStatus — 读全库元数据,拼 StatusInfo\n// ---------------------------------------------------------------------------\n\n/**\n * 读 `/.wiki/vector.sqlite` 元数据:\n * - 不存在 → 返回 `{indexed: false, message, mode: 'text'}`(mode_threshold + mode_reason 仍填)\n * - 存在 → openDb → COUNT documents/chunks/dir_summaries/page_summaries\n * + 读 meta(last_sync/model/dim) + collectFiles 算 total_indexable_files\n * + computeMode 决定 mode / mode_reason\n *\n * **23a 改动**:原沉默 catch(老 db 缺 dir_summaries / page_summaries 表的兼容兜底)\n * 改为 `logger.warn(...)` + 明确注释。dirCount / pageCount 留 0 不阻塞 status 输出。\n */\nexport async function getStatus(corpus: string): Promise {\n const dbPath = join(corpus, '.wiki', 'vector.sqlite');\n if (!existsSync(dbPath)) {\n const rec = computeMode(false, 0);\n return {\n indexed: false,\n message: \"No vector index found. Run 'lorekit vector sync' first.\",\n mode: rec.mode,\n mode_threshold: MODE_THRESHOLD_FILES,\n mode_reason: rec.reason,\n };\n }\n\n const db = await openDb(corpus);\n\n const docCount = (db.prepare('SELECT COUNT(*) as n FROM documents').get() as { n: number }).n;\n const chunkCount = (db.prepare('SELECT COUNT(*) as n FROM chunks').get() as { n: number }).n;\n const lastSync = db.prepare(\"SELECT value FROM meta WHERE key = 'last_sync'\").get() as\n | { value: string }\n | undefined;\n const model = db.prepare(\"SELECT value FROM meta WHERE key = 'model'\").get() as\n | { value: string }\n | undefined;\n const dim = db.prepare(\"SELECT value FROM meta WHERE key = 'dim'\").get() as\n | { value: string }\n | undefined;\n\n const totalFiles = collectFiles(corpus).length;\n\n let dirCount = 0;\n let pageCount = 0;\n try {\n dirCount = (db.prepare('SELECT COUNT(*) as n FROM dir_summaries').get() as { n: number }).n;\n pageCount = (db.prepare('SELECT COUNT(*) as n FROM page_summaries').get() as { n: number }).n;\n } catch (e) {\n // 老 db(批次 22 之前的版本)可能缺 dir_summaries / page_summaries 表,\n // 留 dirCount/pageCount=0 不阻塞 status 输出;下次 lorekit sync 会通过 openDb\n // 的 DDL CREATE IF NOT EXISTS 自动建表。\n logger.warn(`getStatus: layered tables missing, treat as 0 (${(e as Error).message})`);\n }\n\n db.close();\n\n const rec = computeMode(true, docCount);\n\n return {\n indexed: true,\n total_indexable_files: totalFiles,\n indexed_files: docCount,\n chunks: chunkCount,\n layered: { dirs: dirCount, pages: pageCount },\n embedding_dim: dim ? parseInt(dim.value, 10) : EMBEDDING_DIM,\n last_sync: lastSync?.value ?? null,\n model: model?.value ?? null,\n backend: 'ollama',\n mode: rec.mode,\n mode_threshold: MODE_THRESHOLD_FILES,\n mode_reason: rec.reason,\n };\n}\n","/**\n * vectordb/index.ts — 子模块对外主入口(barrel re-export)\n *\n * 批次 22e strangler fig 第五步:建主入口模块。本文件**不含 runtime 代码**,\n * 仅 re-export 公开 API;实现散在 22a-22d + 22e 的 9 个子文件里。\n *\n * 22e 阶段:本文件目前未被任何调用方 import;commands/vector.ts 仍 dynamic\n * import 旧 src/lib/vectordb.ts。22f 才切换 + 删旧。\n *\n * **公开 surface**(commands/*.ts 真正用到的 API):\n * - 9 个 value:openDb / syncFile / buildLayeredIndex / collectFiles\n * + queryFlat / queryLayered / queryBM25Layered / queryHybrid + getStatus\n * - 1 个额外算法 export:rrfMerge(commands 暂未用,但作为 hybrid 配套算法暴露)\n * - 2 个常量:EMBEDDING_DIM / MODE_THRESHOLD_FILES\n * - 3 个 type:Db / StatusInfo / QueryResult\n *\n * **不 re-export 的内部 helper**(保持封装,commands 不应直接调用):\n * - sha256 / float32ToBuffer / distanceToScore / shouldIndex / extractPageSummary(files.ts)\n * - sanitizeFtsQuery(query-bm25.ts,私有)\n * - parseIndexSections / parseIndexEntries / findAllIndexFiles(build-layered-index.ts,私有)\n * - DDL / FTS_DDL / vecDdl / loadSqlite(schema.ts,仅 openDb 内部用)\n * - EmbedFn type(sync.ts + build-layered-index.ts 双 inline,commands 自定义实现喂入)\n *\n * **EmbedFn 决策**(22e 拍板):保持双 inline 不上提到 schema.ts。理由:\n * (a) commands 不用 EmbedFn type(commands 自定义实现喂入 syncFile / buildLayeredIndex)\n * (b) 上提引入 sync/build-layered → schema.ts 的额外耦合\n * (c) 仅 2 处 inline,type 定义 1 行 + 注释 2 行,复制成本极小\n */\n\n// ---------------------------------------------------------------------------\n// 公开 value(commands/*.ts 用)\n// ---------------------------------------------------------------------------\n\nexport { openDb } from './schema.js';\nexport { syncFile } from './sync.js';\nexport { buildLayeredIndex } from './build-layered-index.js';\nexport { queryFlat } from './query-flat.js';\nexport { queryLayered } from './query-layered.js';\nexport { queryBM25Layered } from './query-bm25.js';\nexport { queryHybrid, rrfMerge } from './query-hybrid.js';\nexport { getStatus } from './status.js';\nexport { collectFiles } from './files.js';\n\n// ---------------------------------------------------------------------------\n// 公开常量\n// ---------------------------------------------------------------------------\n\nexport { EMBEDDING_DIM, MODE_THRESHOLD_FILES } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// 公开 type\n// ---------------------------------------------------------------------------\n\nexport type { Db, StatusInfo, QueryResult } from './schema.js';\n","#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport Database from 'better-sqlite3';\nimport { findCorpus, collectMdFiles } from './lib/corpus.js';\nimport { debug, print } from './utils/logger.js';\nimport { readVersion } from './utils/fs.js';\n\n// commands\nimport { initCommand } from './commands/init.js';\nimport { doctorCommand } from './commands/doctor.js';\nimport { statsCommand } from './commands/stats.js';\nimport { lintCommand } from './commands/lint.js';\nimport { auditCommand } from './commands/audit.js';\nimport { indexCommand } from './commands/dir-index.js';\nimport { installSkillsCommand } from './commands/install-skills.js';\nimport { snapshotCommand } from './commands/snapshot.js';\nimport { restoreCommand } from './commands/restore.js';\nimport { searchCommand } from './commands/search.js';\nimport { vectorCommand } from './commands/vector.js';\nimport { fetchCommand } from './commands/fetch.js';\nimport { ingestCommand } from './commands/ingest.js';\nimport { syncCommand } from './commands/sync.js';\nimport { obsidianTuneCommand } from './commands/obsidian-tune.js';\nimport { removeCommand } from './commands/remove.js';\nimport { gbrainCommand } from './commands/gbrain.js';\n\nconst version = readVersion();\n\nfunction showBanner() {\n const corpus = findCorpus();\n let pages = '—';\n let indexed = '0';\n let model = '—';\n\n if (corpus) {\n try {\n pages = String(collectMdFiles(corpus).length);\n } catch (e) {\n // banner 是 best-effort 装饰,corpus 扫失败时不阻塞用户操作 — 仅 debug 留痕\n debug(`banner: collectMdFiles failed: ${(e as Error).message}`);\n }\n\n try {\n const dbPath = `${corpus}/.wiki/vector.sqlite`;\n if (existsSync(dbPath)) {\n const db = new Database(dbPath, { readonly: true });\n const cntRow = db.prepare('SELECT COUNT(*) as c FROM documents').get() as\n | { c: number }\n | undefined;\n indexed = String(cntRow?.c ?? 0);\n const row = db.prepare(\"SELECT value FROM meta WHERE key='model'\").get() as\n | { value: string }\n | undefined;\n model = row?.value ?? '—';\n db.close();\n }\n } catch (e) {\n // 向量库读失败(坏文件 / 锁 / native 加载错)不该阻断 banner 显示\n debug(`banner: vector.sqlite read failed: ${(e as Error).message}`);\n }\n }\n\n const short = corpus && corpus.length > 45 ? '...' + corpus.slice(-42) : (corpus ?? '—');\n const B = chalk.blue;\n const BB = chalk.blueBright.bold;\n const C = chalk.cyan;\n const D = chalk.dim;\n const W = chalk.white.bold;\n\n print();\n print(` ${BB('██╗ ██████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗')}`);\n print(` ${BB('██║ ██╔═══██╗██╔══██╗██╔════╝██║ ██╔╝██║╚══██╔══╝')}`);\n print(` ${BB('██║ ██║ ██║██████╔╝█████╗ █████╔╝ ██║ ██║ ')}`);\n print(` ${B('██║ ██║ ██║██╔══██╗██╔══╝ ██╔═██╗ ██║ ██║ ')}`);\n print(` ${B('███████╗╚██████╔╝██║ ██║███████╗██║ ██╗██║ ██║ ')}`);\n print(` ${D('╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ')}`);\n print(` ${D('Personal LLM Wiki Toolkit')} ${C(`v${version}`)}`);\n print();\n print(` ${C('corpus')} ${short}`);\n print(` ${C('pages')} ${pages.padEnd(10)} ${C('indexed')} ${indexed}`);\n if (model !== '—') print(` ${C('model')} ${model}`);\n print();\n print(` ${W('$ lorekit doctor')} 健康检查`);\n print(` ${W('$ lorekit fetch')} 抓取网页`);\n print(` ${W('$ lorekit search')} 搜索`);\n print(` ${W('$ lorekit --help')} 所有命令`);\n print();\n}\n\nconst program = new Command();\n\n// CONVENTIONS #4:commander 默认对 missing arg / unknown command 都退出 1,\n// 跟我们\"参数错→2\"的语义不匹配。改用 exitOverride 拦截后按错误码分类。\nconst ARG_ERROR_CODES = new Set([\n 'commander.missingArgument',\n 'commander.missingMandatoryOptionValue',\n 'commander.invalidArgument',\n 'commander.invalidOptionArgument',\n 'commander.unknownCommand',\n 'commander.unknownOption',\n 'commander.excessArguments',\n]);\nprogram.exitOverride((cmdErr) => {\n // help / version 是正常退出\n if (\n cmdErr.code === 'commander.help' ||\n cmdErr.code === 'commander.version' ||\n cmdErr.code === 'commander.helpDisplayed'\n ) {\n process.exit(0);\n }\n if (ARG_ERROR_CODES.has(cmdErr.code)) {\n process.exit(2);\n }\n process.exit(cmdErr.exitCode || 1);\n});\n\nprogram.name('lorekit').version(version).description('Personal LLM Wiki Toolkit');\n\n// register commands\ninitCommand(program);\ndoctorCommand(program);\nstatsCommand(program);\nlintCommand(program);\nauditCommand(program);\nindexCommand(program);\ninstallSkillsCommand(program);\nsnapshotCommand(program);\nrestoreCommand(program);\nsearchCommand(program);\nvectorCommand(program);\nfetchCommand(program);\ningestCommand(program);\nsyncCommand(program);\nobsidianTuneCommand(program);\nremoveCommand(program);\ngbrainCommand(program);\n\n// no subcommand → show banner\nif (process.argv.length <= 2) {\n showBanner();\n} else {\n program.parse();\n}\n","import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';\nimport { join, dirname, basename } from 'node:path';\nimport matter from 'gray-matter';\nimport { alwaysExcludeNames } from './paths.js';\nimport { debug } from '../utils/logger.js';\n\nexport function findCorpus(startDir?: string): string | null {\n let dir = startDir || process.cwd();\n while (dir !== '/' && dir) {\n if (existsSync(join(dir, '.wiki')) || existsSync(join(dir, 'CLAUDE.md'))) {\n return dir;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nexport function requireCorpus(startDir?: string): string {\n const corpus = findCorpus(startDir);\n if (!corpus) {\n throw new Error('not inside a corpus (no .wiki/ or CLAUDE.md found)');\n }\n return corpus;\n}\n\nexport interface Frontmatter {\n type?: string;\n title?: string;\n slug?: string;\n created?: string;\n updated?: string | Date;\n [key: string]: unknown;\n}\n\nexport function extractFrontmatter(filePath: string): Frontmatter {\n try {\n const content = readFileSync(filePath, 'utf-8');\n const { data } = matter(content);\n return data as Frontmatter;\n } catch (e) {\n // 文件读不到 / YAML 损坏时返回空对象。在 lint / index 等命令里被大量\n // 调用,warn 会刷屏,所以走 debug;真有异常先生开 LOREKIT_DEBUG=1 复现\n debug(`extractFrontmatter(${filePath}) failed: ${(e as Error).message}`);\n return {};\n }\n}\n\nexport function hasFrontmatter(filePath: string): boolean {\n try {\n const first = readFileSync(filePath, 'utf-8').slice(0, 4);\n return first === '---\\n' || first === '---\\r';\n } catch (e) {\n // 同 extractFrontmatter:批量调用,走 debug\n debug(`hasFrontmatter(${filePath}) failed: ${(e as Error).message}`);\n return false;\n }\n}\n\nexport function extractFrontmatterField(filePath: string, key: string): string | undefined {\n const fm = extractFrontmatter(filePath);\n const val = fm[key];\n return typeof val === 'string' ? val : undefined;\n}\n\n/**\n * Find an existing source page in 原料/ that has the given source_url.\n * Returns the absolute path or null.\n */\nexport function findSourceByUrl(corpus: string, url: string): string | null {\n const sourcesRoot = join(corpus, '原料');\n if (!existsSync(sourcesRoot)) return null;\n for (const mdPath of collectMdFiles(sourcesRoot)) {\n const fm = extractFrontmatter(mdPath);\n if (fm.source_url === url || fm.url === url) return mdPath;\n }\n return null;\n}\n\nexport function collectMdFiles(dir: string, opts?: { excludeIndex?: boolean }): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n function walk(d: string) {\n for (const entry of readdirSync(d, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.md') && !alwaysExcludeNames.has(entry.name)) {\n results.push(full);\n }\n }\n }\n\n walk(dir);\n return results.sort();\n}\n","import { createHash } from 'node:crypto';\nimport { readFileSync, statSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { warn } from './logger.js';\n\nexport function sha256(filePath: string): string {\n const content = readFileSync(filePath);\n return createHash('sha256').update(content).digest('hex');\n}\n\nexport function fileMtime(filePath: string): Date {\n return statSync(filePath).mtime;\n}\n\nexport function lorekitRoot(): string {\n const thisFile = fileURLToPath(import.meta.url);\n // tsup bundles everything to dist/cli.js — dirname(thisFile) === dist/,\n // so the package root is one level up.\n return join(dirname(thisFile), '..');\n}\n\nexport function readVersion(): string {\n try {\n return readFileSync(join(lorekitRoot(), 'VERSION'), 'utf-8').trim();\n } catch (e) {\n // VERSION 文件应当随 lorekit 包发布,缺了说明安装环境异常 — 用 warn 不静默\n warn(`VERSION file missing or unreadable: ${(e as Error).message}`);\n return 'unknown';\n }\n}\n","import type { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n cpSync,\n readFileSync,\n writeFileSync,\n statSync,\n} from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport chalk from 'chalk';\nimport { ok, bad, warn, print } from '../utils/logger.js';\nimport { readVersion, lorekitRoot } from '../utils/fs.js';\n\nconst MINIMAL_DIRS = ['原料', '知识库/概念', '知识库/实体', '知识库/摘要', '每日', '系统', '.wiki'];\n\nfunction ask(question: string): Promise {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nfunction isDirEmpty(dir: string): boolean {\n if (!existsSync(dir)) return true;\n const entries = readdirSync(dir).filter((n) => n !== '.DS_Store' && n !== '.git');\n return entries.length === 0;\n}\n\n/**\n * Recursively copy files from src to dest, skipping files that already exist.\n *\n * 顶层目录 `.obsidian/` 由 `deployObsidianGraphConfig` 与 `deployObsidianPlugin`\n * 单独处理(safe-write + 用户提示),这里跳过避免重复/越权覆盖。\n */\nfunction copyTemplateFiles(src: string, dest: string, isRoot = true) {\n if (!existsSync(dest)) mkdirSync(dest, { recursive: true });\n\n for (const entry of readdirSync(src, { withFileTypes: true })) {\n // 顶层 `.obsidian/` 单独由 deployObsidian* 处理\n if (isRoot && entry.isDirectory() && entry.name === '.obsidian') continue;\n\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyTemplateFiles(srcPath, destPath, false);\n } else {\n if (!existsSync(destPath)) {\n mkdirSync(join(destPath, '..'), { recursive: true });\n cpSync(srcPath, destPath);\n }\n }\n }\n}\n\nfunction deployObsidianPlugin(corpusPath: string) {\n const pluginSrc = join(lorekitRoot(), 'plugins', 'obsidian-audit');\n const pluginDest = join(corpusPath, '.obsidian', 'plugins', 'lorekit-audit');\n\n if (!existsSync(pluginSrc)) {\n warn('obsidian-audit plugin not found in lorekit install, skipping');\n return;\n }\n\n mkdirSync(pluginDest, { recursive: true });\n for (const file of readdirSync(pluginSrc)) {\n cpSync(join(pluginSrc, file), join(pluginDest, file));\n }\n ok('deployed obsidian-audit plugin → .obsidian/plugins/lorekit-audit/');\n}\n\n/**\n * safe-write `.obsidian/graph.json`:\n * - 已存在 → 跳过 + stderr 警告(保留用户自定义的 colorGroups / forceGravity 等)\n * - 目标已有 `.obsidian/` 但缺 graph.json → 只写 graph.json\n * - 目标完全没 `.obsidian/` → 建目录 + 写 graph.json\n *\n * 批次 25 引入:把\"lorekit 决定的结构(_工作台 / _INDEX / 系统 ...)\"\n * 对应的 Obsidian graph filter 作为预设内置,用户无需自己发明一遍。\n */\nfunction deployObsidianGraphConfig(corpusPath: string) {\n const src = join(lorekitRoot(), 'templates', 'default-corpus', '.obsidian', 'graph.json');\n if (!existsSync(src)) {\n // 模板缺失不阻塞 init;warn 即可\n warn('templates/default-corpus/.obsidian/graph.json not found, skipping graph config');\n return;\n }\n\n const destDir = join(corpusPath, '.obsidian');\n const dest = join(destDir, 'graph.json');\n\n if (existsSync(dest)) {\n // 绝对不许覆盖用户已有的 graph.json(可能含 colorGroups / forceGravity 等个性化字段)\n warn('.obsidian/graph.json 已存在,跳过写入。推荐 filter 见 docs/QUICKSTART.md');\n return;\n }\n\n if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });\n cpSync(src, dest);\n ok('deployed Obsidian graph filter → .obsidian/graph.json');\n}\n\nfunction createWikiMeta(corpusPath: string) {\n const wikiDir = join(corpusPath, '.wiki');\n mkdirSync(wikiDir, { recursive: true });\n\n const version = readVersion();\n writeFileSync(join(wikiDir, 'version'), version + '\\n');\n\n const configPath = join(wikiDir, 'config.yaml');\n if (!existsSync(configPath)) {\n writeFileSync(\n configPath,\n [\n '# lorekit corpus config',\n `version: \"${version}\"`,\n 'lang: zh-CN',\n 'frontmatter_required: true',\n '',\n ].join('\\n'),\n );\n }\n ok(`created .wiki/version (${version}) + config.yaml`);\n}\n\nexport function initCommand(program: Command) {\n program\n .command('init')\n .argument('[path]', 'target directory', '.')\n .option('--in-place', 'initialize in-place even if directory is non-empty')\n .option('--minimal', 'only create core directories (no template files)')\n .description('initialize a new lorekit corpus')\n .action(async (targetPath: string, opts: { inPlace?: boolean; minimal?: boolean }) => {\n const resolved = resolve(targetPath);\n const templateDir = join(lorekitRoot(), 'templates', 'default-corpus');\n\n if (opts.minimal) {\n // Minimal mode: just create core directories\n for (const dir of MINIMAL_DIRS) {\n mkdirSync(join(resolved, dir), { recursive: true });\n }\n createWikiMeta(resolved);\n ok(`minimal corpus initialized at ${resolved}`);\n return;\n }\n\n if (!isDirEmpty(resolved) && !opts.inPlace) {\n print(chalk.yellow(`\\n target directory is not empty: ${resolved}\\n`));\n const answer = await ask(\n ' [b] backup & init [i] in-place (skip existing) [c] cancel\\n > ',\n );\n\n if (answer === 'c' || answer === 'C' || answer === '') {\n bad('cancelled');\n return;\n }\n if (answer === 'b' || answer === 'B') {\n const backupDir = resolved + '.bak.' + Date.now();\n cpSync(resolved, backupDir, { recursive: true });\n ok(`backed up to ${backupDir}`);\n }\n // answer 'i'/'b' both fall through to in-place copy\n }\n\n // Copy template files (skip existing)\n if (existsSync(templateDir)) {\n copyTemplateFiles(templateDir, resolved);\n ok('template files copied (skipped existing)');\n } else {\n warn('template directory not found, creating minimal structure');\n for (const dir of MINIMAL_DIRS) {\n mkdirSync(join(resolved, dir), { recursive: true });\n }\n }\n\n createWikiMeta(resolved);\n deployObsidianGraphConfig(resolved);\n deployObsidianPlugin(resolved);\n\n print();\n ok(chalk.bold(`corpus initialized at ${resolved}`));\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport chalk from 'chalk';\nimport { ok, bad, warn, print, out } from '../utils/logger.js';\nimport { requireCorpus, collectMdFiles, hasFrontmatter } from '../lib/corpus.js';\nimport { isIndexExcluded, isFolderPackage } from '../lib/paths.js';\nimport {\n getRecommendedFilter,\n readCorpusFilter,\n isFilterComplete,\n} from '../lib/obsidian.js';\nimport {\n doctorGbrain,\n type GbrainDoctorIssue,\n type GbrainDoctorResult,\n} from '../lib/integrations/gbrain.js';\n\nconst EXPECTED_DIRS = [\n '每日',\n '知识库/实体',\n '知识库/概念',\n '知识库/专题',\n '原料',\n '原料/录音',\n '写作',\n '系统',\n '_工作台',\n];\n\ntype DoctorStatus = 'ok' | 'warn' | 'error';\ntype DoctorSectionName =\n | 'directories'\n | 'wikiMetadata'\n | 'frontmatter'\n | 'indexFiles'\n | 'archive'\n | 'obsidian'\n | 'integrations';\n\ninterface DoctorIssue {\n section: DoctorSectionName | 'gbrain';\n severity: 'warn' | 'error';\n message: string;\n recommendation?: string;\n}\n\ninterface DoctorSectionReport {\n status: DoctorStatus;\n [key: string]: unknown;\n}\n\nexport interface DoctorRunReport {\n status: DoctorStatus;\n generatedAt: string;\n corpus: string;\n sections: Partial>;\n issues: DoctorIssue[];\n hardIssues: number;\n}\n\nexport interface DoctorOptions {\n section?: 'all' | 'integrations';\n}\n\nfunction inspectDirs(corpus: string): { missing: string[] } {\n const missing: string[] = [];\n for (const dir of EXPECTED_DIRS) {\n const full = join(corpus, dir);\n if (!existsSync(full)) missing.push(dir);\n }\n return { missing };\n}\n\nfunction checkDirs(corpus: string): number {\n const { missing } = inspectDirs(corpus);\n for (const dir of EXPECTED_DIRS) {\n if (missing.includes(dir)) bad(`${dir}/ ${chalk.dim('missing')}`);\n else ok(`${dir}/`);\n }\n return missing.length;\n}\n\nfunction inspectWikiVersion(corpus: string): { exists: boolean; version: string | null } {\n const versionFile = join(corpus, '.wiki', 'version');\n if (existsSync(versionFile)) {\n const ver = readFileSync(versionFile, 'utf-8').trim();\n return { exists: true, version: ver };\n }\n return { exists: false, version: null };\n}\n\nfunction checkWikiVersion(corpus: string): number {\n const result = inspectWikiVersion(corpus);\n if (result.exists) {\n ok(`.wiki/version → ${result.version}`);\n return 0;\n }\n bad('.wiki/version missing');\n return 1;\n}\n\nfunction inspectFrontmatterCoverage(corpus: string) {\n const files = collectMdFiles(corpus);\n const withFm = files.filter((f) => hasFrontmatter(f)).length;\n const total = files.length;\n const pct = total === 0 ? 100 : Math.round((withFm / total) * 100);\n return { withFrontmatter: withFm, total, pct };\n}\n\nfunction checkFrontmatterCoverage(corpus: string) {\n const { withFrontmatter, total, pct } = inspectFrontmatterCoverage(corpus);\n const color = pct >= 90 ? chalk.green : pct >= 60 ? chalk.yellow : chalk.red;\n const icon = pct >= 90 ? '✓' : pct >= 60 ? '⚠' : '✗';\n print(`${color(icon)} frontmatter coverage: ${withFrontmatter}/${total} (${pct}%)`);\n}\n\nfunction findMissingIndexDirs(corpus: string): string[] {\n const missing: string[] = [];\n\n function walk(dir: string) {\n if (!existsSync(dir)) return;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n if (!entry.isDirectory()) continue;\n\n const full = join(dir, entry.name);\n const rel = relative(corpus, full);\n\n // 复用 index 命令的排除规则:不对这些目录要求 _INDEX.md\n if (isIndexExcluded(rel)) continue;\n // 目录包装式原料(xxx/article.md)是一个 entry,不是容器——不需要 _INDEX.md\n if (isFolderPackage(full)) continue;\n\n // 本目录是否应该有 _INDEX.md:\n // 有直接 .md 文件 或 有目录包装式原料子目录\n let shouldHaveIndex = false;\n for (const name of readdirSync(full)) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n const childPath = join(full, name);\n let stat;\n try {\n stat = lstatSync(childPath);\n } catch {\n continue;\n }\n if (stat.isFile() && name.endsWith('.md')) {\n shouldHaveIndex = true;\n break;\n }\n if (stat.isDirectory() && isFolderPackage(childPath)) {\n shouldHaveIndex = true;\n break;\n }\n }\n\n if (shouldHaveIndex && !existsSync(join(full, '_INDEX.md'))) {\n missing.push(rel);\n }\n walk(full);\n }\n }\n\n walk(corpus);\n return missing;\n}\n\nfunction checkIndexFiles(corpus: string): number {\n const missing = findMissingIndexDirs(corpus);\n for (const rel of missing) warn(`_INDEX.md missing in ${rel}/`);\n if (missing.length === 0) {\n ok('all directories with .md files have _INDEX.md');\n }\n return missing.length;\n}\n\n/**\n * 检查 .obsidian/graph.json filter 是否含推荐项(批次 26 触达老用户)。\n * obsidian 是可选用途,不阻塞 doctor 整体绿 —— 故意不计入 issues 总数。\n */\nfunction inspectObsidianGraph(corpus: string): DoctorSectionReport {\n try {\n const recommended = getRecommendedFilter();\n const cur = readCorpusFilter(corpus);\n if (!cur.exists) {\n return {\n status: 'warn',\n message: 'graph filter 不完整,运行 lorekit obsidian-tune 查看详情',\n };\n }\n if (isFilterComplete(cur.search, recommended)) {\n return { status: 'ok', message: 'graph filter 完整' };\n }\n return {\n status: 'warn',\n message: 'graph filter 不完整,运行 lorekit obsidian-tune 查看详情',\n };\n } catch (e) {\n return { status: 'warn', message: `检查 graph filter 失败: ${(e as Error).message}` };\n }\n}\n\nfunction checkObsidianGraph(corpus: string): void {\n const result = inspectObsidianGraph(corpus);\n if (result.status === 'ok') ok(`obsidian: ${result.message}`);\n else warn(`obsidian: ${result.message}`);\n}\n\nfunction inspectArchive(corpus: string): DoctorSectionReport {\n const archiveDir = join(corpus, '_归档');\n if (existsSync(archiveDir)) {\n return { status: 'ok', exists: true };\n }\n return { status: 'warn', exists: false, message: '_归档/ not found (optional)' };\n}\n\nfunction checkArchive(corpus: string): number {\n const result = inspectArchive(corpus);\n if (result.status === 'ok') ok('_归档/ exists');\n else warn(String(result.message));\n return 0; // not a hard failure\n}\n\nfunction statusFromIssues(issues: DoctorIssue[]): DoctorStatus {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.length > 0) return 'warn';\n return 'ok';\n}\n\nfunction convertGbrainIssue(issue: GbrainDoctorIssue): DoctorIssue {\n return {\n section: 'gbrain',\n severity: issue.severity,\n message: issue.message,\n recommendation: issue.recommendation,\n };\n}\n\nfunction gbrainSection(gbrain: GbrainDoctorResult): DoctorSectionReport {\n return {\n status: gbrain.status,\n gbrain: {\n status: gbrain.status,\n installed: gbrain.gbrain.installed,\n binary: gbrain.gbrain.binary,\n version: gbrain.gbrain.version,\n brainInitialized: gbrain.gbrain.brainInitialized,\n manifestPath: gbrain.manifestPath,\n syncReportPath: gbrain.syncReportPath,\n issues: gbrain.issues,\n },\n };\n}\n\nexport async function runDoctorReport(\n corpus: string,\n opts: DoctorOptions = {},\n): Promise {\n const section = opts.section ?? 'all';\n if (section !== 'all' && section !== 'integrations') {\n throw new Error(`unsupported doctor section: ${section}`);\n }\n\n const report: DoctorRunReport = {\n status: 'ok',\n generatedAt: new Date().toISOString(),\n corpus,\n sections: {},\n issues: [],\n hardIssues: 0,\n };\n\n if (section === 'all') {\n const dirs = inspectDirs(corpus);\n report.sections.directories = {\n status: dirs.missing.length > 0 ? 'error' : 'ok',\n expected: EXPECTED_DIRS,\n missing: dirs.missing,\n };\n for (const dir of dirs.missing) {\n report.issues.push({\n section: 'directories',\n severity: 'error',\n message: `${dir}/ missing`,\n });\n }\n\n const wiki = inspectWikiVersion(corpus);\n report.sections.wikiMetadata = {\n status: wiki.exists ? 'ok' : 'error',\n version: wiki.version,\n versionFileExists: wiki.exists,\n };\n if (!wiki.exists) {\n report.issues.push({\n section: 'wikiMetadata',\n severity: 'error',\n message: '.wiki/version missing',\n });\n }\n\n const fm = inspectFrontmatterCoverage(corpus);\n report.sections.frontmatter = {\n status: fm.pct >= 90 ? 'ok' : fm.pct >= 60 ? 'warn' : 'error',\n ...fm,\n };\n\n const missingIndexes = findMissingIndexDirs(corpus);\n report.sections.indexFiles = {\n status: missingIndexes.length > 0 ? 'warn' : 'ok',\n missing: missingIndexes,\n };\n for (const rel of missingIndexes) {\n report.issues.push({\n section: 'indexFiles',\n severity: 'warn',\n message: `_INDEX.md missing in ${rel}/`,\n });\n }\n\n report.sections.archive = inspectArchive(corpus);\n report.sections.obsidian = inspectObsidianGraph(corpus);\n }\n\n const gbrain = await doctorGbrain(corpus);\n report.sections.integrations = gbrainSection(gbrain);\n report.issues.push(...gbrain.issues.map(convertGbrainIssue));\n\n report.hardIssues = report.issues.filter((issue) => issue.severity === 'error').length;\n report.status = statusFromIssues(report.issues);\n return report;\n}\n\n/**\n * 程序内复用入口:跑健康体检。\n * 返回 issue 总数。调用方自行决定要不要把退出码设成非零。\n */\nexport async function runDoctor(corpus: string): Promise {\n print(chalk.bold(`\\nlorekit doctor — ${corpus}\\n`));\n\n let issues = 0;\n\n print(chalk.cyan('── directories ──'));\n issues += checkDirs(corpus);\n print();\n\n print(chalk.cyan('── wiki metadata ──'));\n issues += checkWikiVersion(corpus);\n print();\n\n print(chalk.cyan('── frontmatter ──'));\n checkFrontmatterCoverage(corpus);\n print();\n\n print(chalk.cyan('── index files ──'));\n issues += checkIndexFiles(corpus);\n print();\n\n print(chalk.cyan('── archive ──'));\n checkArchive(corpus);\n print();\n\n print(chalk.cyan('── obsidian ──'));\n checkObsidianGraph(corpus);\n print();\n\n print(chalk.cyan('── integrations ──'));\n const gbrain = await doctorGbrain(corpus);\n if (gbrain.status === 'ok') {\n ok('gbrain: integration healthy');\n } else {\n for (const issue of gbrain.issues) {\n const line = `gbrain: ${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n }\n const integrationErrors = gbrain.issues.filter((issue) => issue.severity === 'error').length;\n issues += integrationErrors;\n print();\n\n if (issues === 0) {\n print(chalk.green.bold('all checks passed ✓'));\n } else {\n print(chalk.yellow(`${issues} issue(s) found`));\n }\n print();\n\n return issues;\n}\n\nexport function doctorCommand(program: Command) {\n program\n .command('doctor')\n .description('run health checks on the corpus')\n .option('--json', 'output machine-readable doctor report', false)\n .option('--section ', 'only run a doctor section (currently: integrations)', 'all')\n .action(async (opts: { json?: boolean; section?: 'all' | 'integrations' | string }) => {\n const corpus = requireCorpus();\n if (opts.json) {\n const report = await runDoctorReport(corpus, {\n section: opts.section === 'integrations' ? 'integrations' : 'all',\n });\n out(JSON.stringify(report, null, 2));\n process.exitCode = report.hardIssues > 0 ? 1 : 0;\n return;\n }\n if (opts.section === 'integrations') {\n const report = await runDoctorReport(corpus, { section: 'integrations' });\n print(chalk.bold(`\\nlorekit doctor — ${corpus}\\n`));\n print(chalk.cyan('── integrations ──'));\n const integration = report.sections.integrations?.gbrain as\n | { issues?: GbrainDoctorIssue[] }\n | undefined;\n const issues = integration?.issues ?? [];\n if (issues.length === 0) ok('gbrain: integration healthy');\n for (const issue of issues) {\n const line = `gbrain: ${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n process.exitCode = report.hardIssues > 0 ? 1 : 0;\n return;\n }\n const issues = await runDoctor(corpus);\n process.exitCode = issues > 0 ? 1 : 0;\n });\n}\n","/**\n * obsidian.ts — Obsidian graph filter SSOT helper(批次 26)\n *\n * 推荐 filter 的单一事实源是 `templates/default-corpus/.obsidian/graph.json`。\n * obsidian-tune 命令与 doctor 集成都从这里读,避免和模板漂移。\n *\n * filter 完整性判断采用\"包含所有推荐 token\"——用户可能加了自己的\n * colorGroups / 额外 filter,只要把推荐项都覆盖到就算 OK,不要求完全相等。\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { lorekitRoot } from '../utils/fs.js';\n\nexport interface GraphConfig {\n search?: string;\n showTags?: boolean;\n showAttachments?: boolean;\n hideUnresolved?: boolean;\n showOrphans?: boolean;\n [key: string]: unknown;\n}\n\nexport interface CorpusFilterReadResult {\n exists: boolean;\n search?: string;\n raw?: GraphConfig;\n}\n\n/** 模板里的推荐 graph.json(完整对象)。读不到会 throw —— 模板属于包发布产物 */\nexport function getRecommendedGraphConfig(): GraphConfig {\n const tpl = join(lorekitRoot(), 'templates', 'default-corpus', '.obsidian', 'graph.json');\n const raw = readFileSync(tpl, 'utf-8');\n return JSON.parse(raw) as GraphConfig;\n}\n\n/** 推荐 filter 的 search 字符串(SSOT) */\nexport function getRecommendedFilter(): string {\n const cfg = getRecommendedGraphConfig();\n return cfg.search ?? '';\n}\n\n/** 读 corpus 内的 .obsidian/graph.json。不存在不抛错,返回 exists=false */\nexport function readCorpusFilter(corpus: string): CorpusFilterReadResult {\n const dest = join(corpus, '.obsidian', 'graph.json');\n if (!existsSync(dest)) return { exists: false };\n try {\n const raw = readFileSync(dest, 'utf-8');\n const parsed = JSON.parse(raw) as GraphConfig;\n return { exists: true, search: parsed.search, raw: parsed };\n } catch {\n // JSON 损坏视为存在但 filter 不可读 —— 当作 search 缺失处理\n return { exists: true, search: undefined };\n }\n}\n\n/**\n * 把 search 字符串拆 token(按空白)。\n * Obsidian search 语法 token 形如 `-path:\"_工作台\"` / `-file:\"_INDEX\"`,\n * 引号内含中文也安全:split 走简单空白,引号内不会含空白。\n */\nfunction tokenize(search: string): string[] {\n return search\n .split(/\\s+/)\n .map((t) => t.trim())\n .filter(Boolean);\n}\n\n/**\n * 完整性判断:actual 必须包含 recommended 拆出的全部 token(顺序无关、可有额外项)。\n * actual 缺失 / 空字符串都视为不完整。\n */\nexport function isFilterComplete(actual: string | undefined, recommended: string): boolean {\n if (!actual) return false;\n const want = new Set(tokenize(recommended));\n const have = new Set(tokenize(actual));\n for (const t of want) {\n if (!have.has(t)) return false;\n }\n return true;\n}\n\n/** 列出 actual 缺少哪些推荐 token(用于 diff 输出) */\nexport function missingTokens(actual: string | undefined, recommended: string): string[] {\n const have = new Set(actual ? tokenize(actual) : []);\n return tokenize(recommended).filter((t) => !have.has(t));\n}\n","import { existsSync, mkdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { sha256 } from '../../utils/fs.js';\nimport { getGbrainStatus, type GbrainStatusResult } from './gbrain-status.js';\nimport { exportForGbrain, type GbrainExportResult } from './gbrain-export.js';\nimport { readJsonFile, writeJsonFile, type GbrainExportManifest } from './manifest.js';\nimport { runExternalCommand, type ExternalCommandResult } from './process.js';\n\nexport { getGbrainStatus, exportForGbrain };\n\nexport interface GbrainSyncOptions {\n dryRun?: boolean;\n json?: boolean;\n exportEvenIfMissing?: boolean;\n}\n\nexport interface GbrainSyncResult {\n status: 'ok' | 'error';\n dryRun: boolean;\n startedAt: string;\n finishedAt: string;\n corpus: string;\n export: GbrainExportResult | null;\n gbrain: {\n binary: string;\n version: string | null;\n command: string[];\n exitCode: number | null;\n stdout: string;\n stderr: string;\n durationMs: number;\n } | null;\n gbrainImport?: { skipped: true; reason: string };\n warnings: string[];\n errors: string[];\n}\n\nexport interface GbrainDoctorIssue {\n section: 'gbrain';\n severity: 'warn' | 'error';\n message: string;\n recommendation: string;\n}\n\nexport interface GbrainDoctorResult {\n status: 'ok' | 'warn' | 'error';\n corpus: string;\n gbrain: GbrainStatusResult;\n manifestPath: string;\n syncReportPath: string;\n issues: GbrainDoctorIssue[];\n}\n\nexport interface GbrainQueryResult {\n status: 'ok' | 'error';\n source: 'gbrain';\n message: string;\n staleCheck: {\n skipped: boolean;\n status: GbrainDoctorResult['status'] | null;\n issues: GbrainDoctorIssue[];\n };\n gbrain: ExternalCommandResult | null;\n warnings: string[];\n errors: string[];\n}\n\nfunction syncReportPath(corpus: string): string {\n return join(corpus, '.wiki', 'integrations', 'gbrain', 'sync-report.json');\n}\n\nfunction writeSyncReport(corpus: string, result: GbrainSyncResult): void {\n const path = syncReportPath(corpus);\n mkdirSync(join(corpus, '.wiki', 'integrations', 'gbrain'), { recursive: true });\n writeJsonFile(path, result);\n}\n\nfunction commandSummary(binary: string, pagesDir: string): string[] {\n return [binary, 'import', pagesDir];\n}\n\nexport async function syncGbrain(\n corpus: string,\n opts: GbrainSyncOptions = {},\n): Promise {\n const dryRun = opts.dryRun ?? false;\n const startedAt = new Date().toISOString();\n\n if (dryRun) {\n const exportResult = exportForGbrain(corpus, { dryRun: true });\n return {\n status: 'ok',\n dryRun: true,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: null,\n gbrainImport: { skipped: true, reason: 'dry-run' },\n warnings: exportResult.warnings,\n errors: [],\n };\n }\n\n const gbrainStatus = await getGbrainStatus();\n if (!gbrainStatus.installed) {\n const exportResult = opts.exportEvenIfMissing\n ? exportForGbrain(corpus, { dryRun: false })\n : null;\n const result: GbrainSyncResult = {\n status: 'error',\n dryRun: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: null,\n gbrainImport: { skipped: true, reason: 'gbrain-missing' },\n warnings: exportResult?.warnings ?? [],\n errors: ['gbrain is not installed', ...gbrainStatus.errors],\n };\n writeSyncReport(corpus, result);\n return result;\n }\n\n const exportResult = exportForGbrain(corpus, { dryRun: false });\n const importCommand = commandSummary(gbrainStatus.binary, exportResult.pagesDir);\n const external = await runExternalCommand({\n command: gbrainStatus.binary,\n args: ['import', exportResult.pagesDir],\n cwd: corpus,\n timeoutMs: 120_000,\n });\n\n const result: GbrainSyncResult = {\n status: external.exitCode === 0 ? 'ok' : 'error',\n dryRun: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: {\n binary: gbrainStatus.binary,\n version: gbrainStatus.version,\n command: importCommand,\n exitCode: external.exitCode,\n stdout: external.stdout,\n stderr: external.stderr,\n durationMs: external.durationMs,\n },\n warnings: exportResult.warnings,\n errors: external.exitCode === 0 ? [] : [external.error || external.stderr || 'gbrain import failed'],\n };\n writeSyncReport(corpus, result);\n return result;\n}\n\nexport async function doctorGbrain(corpus: string): Promise {\n const issues: GbrainDoctorIssue[] = [];\n const gbrain = await getGbrainStatus();\n if (!gbrain.installed) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain binary is not installed',\n recommendation: 'Install GBrain only if you want graph retrieval: git clone + bun install + bun link',\n });\n }\n\n const manifestPath = join(corpus, '.wiki', 'integrations', 'gbrain-export', 'manifest.json');\n const syncPath = syncReportPath(corpus);\n const manifest = readJsonFile(manifestPath);\n if (!manifest) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain export manifest is missing',\n recommendation: 'Run lorekit gbrain export',\n });\n } else {\n for (const page of manifest.pages) {\n const sourcePath = join(corpus, page.sourcePath);\n if (!existsSync(sourcePath)) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: `Exported page source is missing: ${page.sourcePath}`,\n recommendation: 'Run lorekit gbrain export to refresh the staging directory',\n });\n continue;\n }\n const currentHash = 'sha256:' + sha256(sourcePath);\n if (currentHash !== page.hash) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: `GBrain export is stale: ${page.sourcePath}`,\n recommendation: 'Run lorekit gbrain export or lorekit gbrain sync',\n });\n }\n }\n }\n\n if (!existsSync(syncPath)) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain sync report is missing',\n recommendation: 'Run lorekit gbrain sync after export when GBrain is installed',\n });\n } else {\n try {\n const report = JSON.parse(readFileSync(syncPath, 'utf-8')) as { status?: string };\n if (report.status !== 'ok') {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'Last GBrain sync did not finish successfully',\n recommendation: 'Inspect .wiki/integrations/gbrain/sync-report.json and rerun sync',\n });\n }\n } catch (e) {\n issues.push({\n section: 'gbrain',\n severity: 'error',\n message: `GBrain sync report is unreadable: ${(e as Error).message}`,\n recommendation: 'Regenerate it with lorekit gbrain sync',\n });\n }\n }\n\n const hasError = issues.some((i) => i.severity === 'error');\n return {\n status: hasError ? 'error' : issues.length > 0 ? 'warn' : 'ok',\n corpus,\n gbrain,\n manifestPath,\n syncReportPath: syncPath,\n issues,\n };\n}\n\nexport interface GbrainQueryOptions {\n staleCheck?: boolean;\n}\n\nexport async function queryGbrain(\n corpus: string,\n text: string,\n opts: GbrainQueryOptions = {},\n): Promise {\n const message =\n 'This answer comes from GBrain index generated from lorekit export. To persist new knowledge, use wiki-fileback / lorekit audit.';\n const shouldCheck = opts.staleCheck !== false;\n if (shouldCheck) {\n const check = await doctorGbrain(corpus);\n if (!check.gbrain.installed) {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: false, status: check.status, issues: check.issues },\n gbrain: null,\n warnings: check.issues.map((i) => i.message),\n errors: ['gbrain is not installed', ...check.gbrain.errors],\n };\n }\n if (check.status !== 'ok') {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: false, status: check.status, issues: check.issues },\n gbrain: null,\n warnings: check.issues.map((i) => i.message),\n errors: [\n 'GBrain export is not ready. Run lorekit gbrain sync first, or pass --no-stale-check to query anyway.',\n ...check.issues.map((i) => i.message),\n ],\n };\n }\n }\n\n const status = await getGbrainStatus();\n if (!status.installed) {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: !shouldCheck, status: null, issues: [] },\n gbrain: null,\n warnings: [],\n errors: ['gbrain is not installed', ...status.errors],\n };\n }\n\n const r = await runExternalCommand({\n command: status.binary,\n args: ['query', text],\n timeoutMs: 120_000,\n });\n return {\n status: r.exitCode === 0 ? 'ok' : 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: !shouldCheck, status: shouldCheck ? 'ok' : null, issues: [] },\n gbrain: r,\n warnings: [],\n errors: r.exitCode === 0 ? [] : [r.error || r.stderr || 'gbrain query failed'],\n };\n}\n","import { existsSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { runExternalCommand } from './process.js';\n\nexport const GBRAIN_INSTALL_HINT = [\n 'git clone https://github.com/garrytan/gbrain.git ~/code/gbrain',\n 'cd ~/code/gbrain',\n 'bun install',\n 'bun link',\n 'gbrain init',\n].join('\\n');\n\nexport interface GbrainStatusResult {\n installed: boolean;\n binary: string;\n version: string | null;\n brainInitialized: boolean;\n installHint: string;\n errors: string[];\n}\n\nexport async function getGbrainStatus(): Promise {\n const binary = process.env.LOREKIT_GBRAIN_BIN || 'gbrain';\n const errors: string[] = [];\n const versionProbe = await runExternalCommand({\n command: binary,\n args: ['--version'],\n timeoutMs: 10_000,\n });\n\n if (versionProbe.exitCode !== 0) {\n errors.push(versionProbe.error || versionProbe.stderr.trim() || 'gbrain binary not installed');\n return {\n installed: false,\n binary,\n version: null,\n brainInitialized: existsSync(join(homedir(), '.gbrain')),\n installHint: GBRAIN_INSTALL_HINT,\n errors,\n };\n }\n\n const version = (versionProbe.stdout || versionProbe.stderr).trim() || null;\n return {\n installed: true,\n binary,\n version,\n brainInitialized: existsSync(join(homedir(), '.gbrain')),\n installHint: GBRAIN_INSTALL_HINT,\n errors,\n };\n}\n","import { spawn } from 'node:child_process';\n\nexport interface ExternalCommandArgs {\n command: string;\n args: string[];\n cwd?: string;\n timeoutMs?: number;\n}\n\nexport interface ExternalCommandResult {\n command: string;\n args: string[];\n exitCode: number;\n stdout: string;\n stderr: string;\n durationMs: number;\n timedOut: boolean;\n error?: string;\n}\n\nexport function runExternalCommand(opts: ExternalCommandArgs): Promise {\n const startedAt = Date.now();\n const timeoutMs = opts.timeoutMs ?? 120_000;\n\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let settled = false;\n let timedOut = false;\n\n const child = spawn(opts.command, opts.args, {\n cwd: opts.cwd,\n shell: false,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill('SIGTERM');\n }, timeoutMs);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf-8');\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n\n child.on('error', (e: NodeJS.ErrnoException) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n command: opts.command,\n args: opts.args,\n exitCode: -1,\n stdout,\n stderr,\n durationMs: Date.now() - startedAt,\n timedOut,\n error: e.message,\n });\n });\n\n child.on('close', (code) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n command: opts.command,\n args: opts.args,\n exitCode: code ?? (timedOut ? 124 : 1),\n stdout,\n stderr,\n durationMs: Date.now() - startedAt,\n timedOut,\n });\n });\n });\n}\n","import { createHash } from 'node:crypto';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport matter from 'gray-matter';\nimport {\n type GbrainExportManifest,\n type GbrainExportManifestPage,\n type GbrainExportManifestSkipped,\n writeJsonFile,\n} from './manifest.js';\n\nexport interface GbrainExportOptions {\n out?: string;\n dryRun?: boolean;\n}\n\nexport interface GbrainExportResult {\n status: 'ok' | 'warn';\n dryRun: boolean;\n corpus: string;\n exportDir: string;\n pagesDir: string;\n manifestPath: string;\n exportedAt: string;\n pagesExported: number;\n pagesSkipped: number;\n pages: GbrainExportManifestPage[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n}\n\ninterface Candidate {\n absPath: string;\n sourcePath: string;\n}\n\nfunction toPosixPath(path: string): string {\n return path.split('\\\\').join('/');\n}\n\nfunction sha256Content(content: Buffer | string): string {\n return 'sha256:' + createHash('sha256').update(content).digest('hex');\n}\n\nfunction exportRoot(corpus: string, out?: string): string {\n if (!out) return join(corpus, '.wiki', 'integrations', 'gbrain-export');\n return resolve(corpus, out);\n}\n\nfunction collectKnowledgeMarkdown(corpus: string): {\n candidates: Candidate[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n} {\n const root = join(corpus, '知识库');\n const candidates: Candidate[] = [];\n const skipped: GbrainExportManifestSkipped[] = [];\n const warnings: string[] = [];\n\n if (!existsSync(root)) {\n warnings.push('知识库/ not found; no pages exported');\n return { candidates, skipped, warnings };\n }\n\n function walk(dir: string): void {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n const absPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(absPath);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.md')) continue;\n\n const sourcePath = toPosixPath(relative(corpus, absPath));\n if (sourcePath === '知识库/模板' || sourcePath.startsWith('知识库/模板/')) {\n skipped.push({ sourcePath, reason: 'template file skipped by default' });\n continue;\n }\n if (entry.name === '_INDEX.md') {\n skipped.push({ sourcePath, reason: 'index file skipped by default' });\n continue;\n }\n if (entry.name === 'index.md') {\n skipped.push({ sourcePath, reason: 'local index file skipped by default' });\n continue;\n }\n candidates.push({ absPath, sourcePath });\n }\n }\n\n walk(root);\n candidates.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));\n skipped.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));\n return { candidates, skipped, warnings };\n}\n\nfunction ensureFreshExportDir(root: string, exportedAt: string): void {\n mkdirSync(root, { recursive: true });\n const backupRoot = join(root, 'backups', exportedAt.replace(/[:.]/g, '-'));\n let moved = false;\n\n for (const name of ['pages', 'manifest.json', 'README.md']) {\n const current = join(root, name);\n if (!existsSync(current)) continue;\n if (!moved) {\n mkdirSync(backupRoot, { recursive: true });\n moved = true;\n }\n renameSync(current, join(backupRoot, name));\n }\n}\n\nfunction normalizeForGbrain(raw: string, sourcePath: string, exportedAt: string): string {\n const parsed = matter(raw);\n const data: Record = { ...parsed.data };\n delete data.slug;\n data.lorekit_source_path = sourcePath;\n data.lorekit_layer = 'artifact';\n data.lorekit_hash = sha256Content(raw);\n data.lorekit_exported_at = exportedAt;\n return matter.stringify(parsed.content, data);\n}\n\nfunction pageMeta(raw: string): { title: string | null; type: string | null } {\n const parsed = matter(raw);\n return {\n title: typeof parsed.data.title === 'string' ? parsed.data.title : null,\n type: typeof parsed.data.type === 'string' ? parsed.data.type : null,\n };\n}\n\nexport function exportForGbrain(corpus: string, opts: GbrainExportOptions = {}): GbrainExportResult {\n const dryRun = opts.dryRun ?? false;\n const exportedAt = new Date().toISOString();\n const root = exportRoot(corpus, opts.out);\n const pagesDir = join(root, 'pages');\n const manifestPath = join(root, 'manifest.json');\n const { candidates, skipped, warnings } = collectKnowledgeMarkdown(corpus);\n\n const pages: GbrainExportManifestPage[] = [];\n for (const candidate of candidates) {\n const rawBuffer = readFileSync(candidate.absPath);\n const raw = rawBuffer.toString('utf-8');\n const relUnderKnowledge = toPosixPath(relative(join(corpus, '知识库'), candidate.absPath));\n const exportPath = toPosixPath(join('pages', relUnderKnowledge));\n const meta = pageMeta(raw);\n pages.push({\n sourcePath: candidate.sourcePath,\n exportPath,\n title: meta.title,\n type: meta.type,\n hash: sha256Content(rawBuffer),\n bytes: statSync(candidate.absPath).size,\n status: 'exported',\n });\n }\n\n if (!dryRun) {\n ensureFreshExportDir(root, exportedAt);\n for (const candidate of candidates) {\n const raw = readFileSync(candidate.absPath, 'utf-8');\n const relUnderKnowledge = relative(join(corpus, '知识库'), candidate.absPath);\n const target = join(pagesDir, relUnderKnowledge);\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, normalizeForGbrain(raw, candidate.sourcePath, exportedAt), 'utf-8');\n }\n const manifest: GbrainExportManifest = {\n version: 1,\n integration: 'gbrain',\n source: 'lorekit',\n corpus,\n exportedAt,\n pages,\n skipped,\n warnings,\n };\n writeJsonFile(manifestPath, manifest);\n writeFileSync(\n join(root, 'README.md'),\n [\n '# GBrain export',\n '',\n 'Generated by `lorekit gbrain export`.',\n 'This directory is a staging copy for GBrain import; lorekit 知识库/ remains the source of truth.',\n '',\n ].join('\\n'),\n 'utf-8',\n );\n }\n\n return {\n status: warnings.length > 0 ? 'warn' : 'ok',\n dryRun,\n corpus,\n exportDir: root,\n pagesDir,\n manifestPath,\n exportedAt,\n pagesExported: pages.length,\n pagesSkipped: skipped.length,\n pages,\n skipped,\n warnings,\n };\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs';\n\nexport interface GbrainExportManifestPage {\n sourcePath: string;\n exportPath: string;\n title: string | null;\n type: string | null;\n hash: string;\n bytes: number;\n status: 'exported';\n}\n\nexport interface GbrainExportManifestSkipped {\n sourcePath: string;\n reason: string;\n}\n\nexport interface GbrainExportManifest {\n version: 1;\n integration: 'gbrain';\n source: 'lorekit';\n corpus: string;\n exportedAt: string;\n pages: GbrainExportManifestPage[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n}\n\nexport function writeJsonFile(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + '\\n', 'utf-8');\n}\n\nexport function readJsonFile(path: string): T | null {\n if (!existsSync(path)) return null;\n return JSON.parse(readFileSync(path, 'utf-8')) as T;\n}\n","import type { Command } from 'commander';\nimport { readFileSync, statSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport { debug, out } from '../utils/logger.js';\n\nexport function statsCommand(program: Command) {\n program\n .command('stats')\n .description('output corpus statistics as JSON')\n .action(() => {\n const corpus = requireCorpus();\n const files = collectMdFiles(corpus);\n const now = Date.now();\n const sevenDays = 7 * 24 * 60 * 60 * 1000;\n\n const byType: Record = {};\n const byDir: Record = {};\n const inboundLinks = new Set();\n let recentActive7d = 0;\n let lastUpdated = '';\n\n for (const file of files) {\n // by_type\n const fm = extractFrontmatter(file);\n const type = fm.type || 'unknown';\n byType[type] = (byType[type] || 0) + 1;\n\n // by_dir (top-level directory relative to corpus)\n const rel = relative(corpus, file);\n const topDir = rel.split('/')[0] || '.';\n byDir[topDir] = (byDir[topDir] || 0) + 1;\n\n // recent_active_7d\n try {\n const mtime = statSync(file).mtime;\n if (now - mtime.getTime() < sevenDays) {\n recentActive7d++;\n }\n const iso = mtime.toISOString();\n if (iso > lastUpdated) lastUpdated = iso;\n } catch (e) {\n // 单文件 stat 失败不应中断整体统计;走 debug\n debug(`stats: stat(${file}) failed: ${(e as Error).message}`);\n }\n\n // Collect wikilink targets to identify orphans later\n try {\n const content = readFileSync(file, 'utf-8');\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) {\n inboundLinks.add(m[1].trim());\n }\n } catch (e) {\n // 单文件读失败时该文件的 wikilinks 漏掉,但不影响全局统计;走 debug\n debug(`stats: readFileSync(${file}) failed: ${(e as Error).message}`);\n }\n }\n\n // Compute orphans: pages that receive zero inbound links\n const orphans: string[] = [];\n for (const file of files) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n const baseName = stem.split('/').pop()!;\n // A page is an orphan if neither its full relative stem nor its base name\n // appears as a wikilink target\n if (!inboundLinks.has(stem) && !inboundLinks.has(baseName)) {\n orphans.push(rel);\n }\n }\n\n const result = {\n total_pages: files.length,\n by_type: byType,\n by_dir: byDir,\n recent_active_7d: recentActive7d,\n orphans: orphans.length,\n last_updated: lastUpdated || null,\n };\n\n out(JSON.stringify(result, null, 2));\n });\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { relative, basename } from 'node:path';\nimport chalk from 'chalk';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport {\n lintSkipFrontmatterBasenames,\n lintRootOnlySkipBasenames,\n lintSkipOrphanPrefixes,\n lintSkipFrontmatterPrefixes,\n lintSkipBrokenLinkPrefixes,\n} from '../lib/paths.js';\nimport { bad, ok, print } from '../utils/logger.js';\n\nconst REQUIRED_FIELDS = ['type', 'title', 'slug', 'created', 'updated'] as const;\n\nfunction isRootLevel(rel: string): boolean {\n return !rel.includes('/');\n}\n\nfunction shouldSkipFrontmatter(rel: string): boolean {\n const base = basename(rel);\n if (lintSkipFrontmatterBasenames.has(base)) return true;\n if (isRootLevel(rel) && lintRootOnlySkipBasenames.has(base)) return true;\n for (const prefix of lintSkipFrontmatterPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\nfunction shouldSkipOrphan(rel: string): boolean {\n const base = basename(rel);\n if (lintSkipFrontmatterBasenames.has(base)) return true;\n if (isRootLevel(rel) && lintRootOnlySkipBasenames.has(base)) return true;\n for (const prefix of lintSkipOrphanPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\nfunction shouldSkipBrokenLink(rel: string): boolean {\n for (const prefix of lintSkipBrokenLinkPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\n// 系统隔离:frontmatter `graph-excluded: true` 的页面不入 Obsidian 图谱,\n// 所以也不应被 orphan 检查报\"无入链\"。典型:QUESTIONS.md / overview.md / 输出/*\nfunction isGraphExcluded(fm: Record): boolean {\n return fm['graph-excluded'] === true || fm['graph_excluded'] === true;\n}\n\n// 去掉围栏代码块和行内代码,避免文档里 `[[Page]]` 这类占位符被当作真 wikilink\nfunction stripCodeBlocks(content: string): string {\n content = content.replace(/```[\\s\\S]*?```/g, '');\n content = content.replace(/`[^`\\n]+`/g, '');\n return content;\n}\n\ninterface LintIssue {\n file: string;\n kind: 'missing-field' | 'broken-link' | 'orphan';\n detail: string;\n}\n\nexport function runLint(corpus: string): LintIssue[] {\n const files = collectMdFiles(corpus);\n const issues: LintIssue[] = [];\n\n // Build lookup sets for wikilink resolution\n // Map: base name (no ext) → relative path, and full relative stem → relative path\n const stemSet = new Set();\n const baseNameSet = new Set();\n // Track inbound links per base name / stem for orphan detection\n const inboundLinks = new Set();\n\n for (const file of files) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n stemSet.add(stem);\n baseNameSet.add(stem.split('/').pop()!);\n\n // 文件夹包装式原料:`原料/文章/xxx/article.md` 的规范引用是 `[[原料/文章/xxx]]`\n // 把父目录路径也登记为有效链接目标\n if (stem.endsWith('/article')) {\n const folderStem = stem.replace(/\\/article$/, '');\n stemSet.add(folderStem);\n baseNameSet.add(folderStem.split('/').pop()!);\n }\n }\n\n // Pass 1: frontmatter + collect wikilinks\n const fileLinks = new Map();\n const fileFrontmatter = new Map>();\n\n for (const file of files) {\n const rel = relative(corpus, file);\n\n // 总是提取 fm 存起来(Pass 3 orphan 检查用 graph-excluded 判断)\n let fm: Record = {};\n try {\n fm = extractFrontmatter(file);\n } catch {\n /* 无 frontmatter / 读不到都按空对象处理 */\n }\n fileFrontmatter.set(rel, fm);\n\n // Check required frontmatter fields (skip top-level config/index files)\n if (!shouldSkipFrontmatter(rel)) {\n for (const field of REQUIRED_FIELDS) {\n if (!fm[field]) {\n issues.push({\n file: rel,\n kind: 'missing-field',\n detail: `missing frontmatter field: ${field}`,\n });\n }\n }\n }\n\n // Extract wikilinks (ignore matches inside code blocks)\n try {\n const content = stripCodeBlocks(readFileSync(file, 'utf-8'));\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n const targets: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) {\n const target = m[1].trim();\n targets.push(target);\n inboundLinks.add(target);\n }\n fileLinks.set(rel, targets);\n } catch {\n /* skip unreadable files */\n }\n }\n\n // Pass 2: broken links\n for (const [rel, targets] of fileLinks) {\n if (shouldSkipBrokenLink(rel)) continue; // 模板占位符不算死链\n for (const target of targets) {\n if (!stemSet.has(target) && !baseNameSet.has(target)) {\n issues.push({\n file: rel,\n kind: 'broken-link',\n detail: `broken link: [[${target}]]`,\n });\n }\n }\n }\n\n // Pass 3: orphan pages (no inbound links)\n for (const file of files) {\n const rel = relative(corpus, file);\n if (shouldSkipOrphan(rel)) continue;\n\n // graph-excluded 系统文件(QUESTIONS.md / overview.md / 输出/* 等)不入 Obsidian 图谱,\n // 天然\"无入链\"合理,不应报 orphan\n const fm = fileFrontmatter.get(rel) ?? {};\n if (isGraphExcluded(fm)) continue;\n\n const stem = rel.replace(/\\.md$/, '');\n const baseName = stem.split('/').pop()!;\n\n let hasInbound = inboundLinks.has(stem) || inboundLinks.has(baseName);\n\n // 文件夹包装式原料:父目录形式的引用也算入链\n if (!hasInbound && stem.endsWith('/article')) {\n const folderStem = stem.replace(/\\/article$/, '');\n const folderName = folderStem.split('/').pop()!;\n hasInbound = inboundLinks.has(folderStem) || inboundLinks.has(folderName);\n }\n\n if (!hasInbound) {\n issues.push({\n file: rel,\n kind: 'orphan',\n detail: 'orphan page (no inbound links)',\n });\n }\n }\n\n return issues;\n}\n\nexport function printLintReport(corpus: string, issues: LintIssue[]): void {\n print(chalk.bold(`\\nlorekit lint — ${corpus}\\n`));\n\n if (issues.length === 0) {\n ok('no issues found');\n print();\n return;\n }\n\n // Group by kind\n const grouped: Record = {};\n for (const issue of issues) {\n (grouped[issue.kind] ??= []).push(issue);\n }\n\n const kindLabels: Record = {\n 'missing-field': 'frontmatter',\n 'broken-link': 'broken links',\n orphan: 'orphan pages',\n };\n\n for (const [kind, items] of Object.entries(grouped)) {\n print(chalk.cyan(`── ${kindLabels[kind] ?? kind} (${items.length}) ──`));\n for (const item of items) {\n bad(`${item.file}: ${item.detail}`);\n }\n print();\n }\n\n print(chalk.yellow(`${issues.length} issue(s) total\\n`));\n}\n\nexport function lintCommand(program: Command) {\n program\n .command('lint')\n .description('check frontmatter, broken wikilinks, and orphan pages')\n .action(() => {\n const corpus = requireCorpus();\n const issues = runLint(corpus);\n printLintReport(corpus, issues);\n if (issues.length > 0) process.exitCode = 1;\n });\n}\n","import { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, basename } from 'node:path';\nimport {\n requireCorpus,\n collectMdFiles,\n hasFrontmatter,\n extractFrontmatter,\n} from '../lib/corpus.js';\nimport { tsCompact, tsMinute } from '../lib/date.js';\nimport { ok, err, print } from '../utils/logger.js';\n\nconst SEVERITY_ORDER: Record = { high: 3, medium: 2, low: 1 };\n\ninterface AuditEntry {\n severity: string;\n sevOrder: number;\n target: string;\n status: string;\n created: string;\n preview: string;\n}\n\nfunction extractPreview(filePath: string): string {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n let inFm = false;\n for (const line of lines) {\n if (line.trimEnd() === '---') {\n if (!inFm) {\n inFm = true;\n continue;\n } else {\n inFm = false;\n continue;\n }\n }\n if (inFm) continue;\n if (line.trim() === '') continue;\n return line.trim();\n }\n return '';\n}\n\nfunction listAudit(root: string, filter: 'all' | 'open' | 'resolved'): void {\n const dirs: string[] = [];\n if (filter === 'open' || filter === 'all') dirs.push(join(root, '反馈', '待处理'));\n if (filter === 'resolved' || filter === 'all') dirs.push(join(root, '反馈', '已处理'));\n\n const entries: AuditEntry[] = [];\n\n for (const dir of dirs) {\n if (!existsSync(dir)) continue;\n const files = collectMdFiles(dir);\n for (const f of files) {\n if (basename(f) === '.gitkeep') continue;\n if (!hasFrontmatter(f)) continue;\n\n const fm = extractFrontmatter(f);\n const severity = (fm.severity as string) ?? '';\n const target = (fm.target as string) ?? '';\n const created = (fm.created as string) ?? '';\n const status = (fm.status as string) ?? '';\n const preview = extractPreview(f);\n\n entries.push({\n severity,\n sevOrder: SEVERITY_ORDER[severity] ?? 0,\n target,\n status,\n created,\n preview,\n });\n }\n }\n\n if (entries.length === 0) {\n print('No audit entries found.');\n return;\n }\n\n // Sort by severity descending\n entries.sort((a, b) => b.sevOrder - a.sevOrder);\n\n for (const e of entries) {\n print(`[${e.severity}] ${e.target} — ${e.preview} (${e.created}) [${e.status}]`);\n }\n print();\n print(`Total: ${entries.length} entries`);\n}\n\nfunction createAudit(root: string, target: string, severity: string, text: string): void {\n if (!target) {\n err('audit --create requires --target');\n process.exit(2);\n }\n if (!severity) {\n err('audit --create requires --severity');\n process.exit(2);\n }\n if (!text) {\n err('audit --create requires --text');\n process.exit(2);\n }\n\n if (!['low', 'medium', 'high'].includes(severity)) {\n err(`severity must be low|medium|high, got: ${severity}`);\n process.exit(2);\n }\n\n const slug = basename(target, '.md').replace(/[\\s/]/g, '-').toLowerCase();\n\n const now = new Date();\n const filename = `${tsCompact(now)}-${slug}.md`;\n const tsFm = tsMinute(now);\n\n const destDir = join(root, '反馈', '待处理');\n mkdirSync(destDir, { recursive: true });\n\n const dest = join(destDir, filename);\n const content = `---\ntype: audit\ntarget: ${target}\nseverity: ${severity}\nstatus: open\ncreated: ${tsFm}\n---\n\n${text}\n`;\n\n writeFileSync(dest, content, 'utf-8');\n ok(`created: 反馈/待处理/${filename}`);\n print(` target: ${target}`);\n print(` severity: ${severity}`);\n}\n\nexport function auditCommand(program: Command): void {\n const cmd = program\n .command('audit')\n .description('Human feedback loop for corpus content')\n .option('--list', 'List entries (default)')\n .option('--open', 'Only show open (待处理) entries')\n .option('--resolved', 'Only show resolved (已处理) entries')\n .option('--create', 'Create a new audit entry')\n .option('--target ', 'Target file path (relative to corpus root)')\n .option('--severity ', 'Severity: low | medium | high')\n .option('--text ', 'Feedback text');\n\n cmd.action((opts) => {\n const root = requireCorpus();\n\n if (opts.create) {\n createAudit(root, opts.target ?? '', opts.severity ?? '', opts.text ?? '');\n } else {\n let filter: 'all' | 'open' | 'resolved' = 'all';\n if (opts.open) filter = 'open';\n else if (opts.resolved) filter = 'resolved';\n listAudit(root, filter);\n }\n });\n}\n","/**\n * date.ts — 日期 / 时间戳 helper 集中处。\n *\n * 历史背景(LEGACY P1-2):`pad(n)`、`today()`、`todayYMD()` 等小工具散落在\n * init / index / audit / snapshot / ingest / fetcher 各文件内重复实现。本文件\n * 把它们集中起来,下游 import 即可。\n *\n * 时区策略:除 `*Shanghai*` 后缀的函数显式按 Asia/Shanghai 偏移外,其他函数\n * 默认走 JS 系统时区(在先生这台机器上 = Asia/Shanghai)。\n *\n * 后续批次(9 / 21)继续往本文件追加;先生不要在其他文件里\"另起炉灶\"。\n */\n\nconst SHANGHAI_TZ_OFFSET_MS = 8 * 60 * 60 * 1000;\n\n/** 把数字补足到 2 位(如月 / 日 / 时 / 分 / 秒) */\nexport function pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/** 今天的 YYYY-MM-DD(按 Asia/Shanghai 偏移)—— 适合记录 source_date / created / updated */\nexport function todayYMDShanghai(): string {\n const d = new Date(Date.now() + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/** 把任意 Date 格式化成 YYYY-MM-DD(**UTC**,常用于 frontmatter 解析回写) */\nexport function dateToYMDUtc(d: Date): string {\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\n/** 把任意 Date 格式化成 YYYY-MM-DD(**本地**时区,常用于 mtime 显示) */\nexport function dateToYMDLocal(d: Date): string {\n return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;\n}\n\n/**\n * 紧凑文件名时间戳:YYYYMMDD-HHMMSS(本地时区)。\n * 用在 snapshot 文件名、audit 反馈条目文件名等不希望出现冒号 / 空格的位置。\n */\nexport function tsCompact(d: Date = new Date()): string {\n return [\n d.getFullYear(),\n pad2(d.getMonth() + 1),\n pad2(d.getDate()),\n '-',\n pad2(d.getHours()),\n pad2(d.getMinutes()),\n pad2(d.getSeconds()),\n ].join('');\n}\n\n/** YYYY-MM-DD HH:MM(本地时区,audit frontmatter 用) */\nexport function tsMinute(d: Date = new Date()): string {\n return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;\n}\n","import { Command } from 'commander';\nimport { existsSync, readdirSync, readFileSync, statSync, writeFileSync, lstatSync } from 'node:fs';\nimport { join, basename, relative, resolve } from 'node:path';\nimport { requireCorpus, hasFrontmatter, extractFrontmatter } from '../lib/corpus.js';\nimport {\n indexExcludeDirPrefixes,\n isIndexExcluded,\n isFolderPackage,\n} from '../lib/paths.js';\nimport { dateToYMDUtc, dateToYMDLocal } from '../lib/date.js';\nimport { ok, warn, err } from '../utils/logger.js';\n\nfunction extractSummary(filePath: string): string {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n let found = false;\n for (const line of lines) {\n if (/^## Compiled Truth/.test(line)) {\n found = true;\n continue;\n }\n if (!found) continue;\n if (/^---\\s*$/.test(line)) break;\n if (/^## /.test(line)) break;\n if (line.trim() === '') continue;\n\n let text = line.trim().replace(/^\\*\\*[^*]*\\*\\*\\s*/, '');\n const periodMatch = text.match(/^([^。.]*[。.])/);\n if (periodMatch && periodMatch[1].length <= 50) return periodMatch[1];\n return text.slice(0, 50);\n }\n return '';\n}\n\ninterface IndexEntry {\n slug: string; // corpus 根相对路径,不含 .md;对目录包装式原料用父目录路径\n title: string; // frontmatter.title 或 basename\n summary: string; // Compiled Truth 首句或 \"—\"\n updated: string; // YYYY-MM-DD\n}\n\nfunction readEntryFromFile(filePath: string, slug: string): IndexEntry {\n let title = '';\n let updated = '';\n let summary = '';\n\n if (hasFrontmatter(filePath)) {\n const fm = extractFrontmatter(filePath);\n title = typeof fm.title === 'string' ? fm.title : fm.title != null ? String(fm.title) : '';\n\n if (fm.updated instanceof Date) {\n updated = dateToYMDUtc(fm.updated);\n } else {\n updated = fm.updated != null ? String(fm.updated) : '';\n }\n\n summary = extractSummary(filePath);\n if (!summary) summary = '—';\n } else {\n summary = '(缺少 frontmatter)';\n }\n\n if (!title) title = basename(filePath, '.md');\n\n if (!updated) {\n try {\n updated = dateToYMDLocal(statSync(filePath).mtime);\n } catch {\n updated = 'unknown';\n }\n }\n\n return { slug, title, summary, updated };\n}\n\n// 转义表格单元格里的 | 字符(防止撑散 markdown 表格)\nfunction escapeCell(s: string): string {\n return s.replace(/\\|/g, '\\\\|');\n}\n\nfunction buildIndex(dir: string, root: string): boolean {\n const reldir = dir === root ? '' : relative(root, dir);\n const dirName = reldir === '' ? basename(root) : basename(dir);\n const indexFile = join(dir, '_INDEX.md');\n\n let names: string[];\n try {\n names = readdirSync(dir, { encoding: 'utf-8' });\n } catch {\n return false;\n }\n\n const entries: IndexEntry[] = [];\n\n for (const name of names) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n\n if (stat.isFile() && name.endsWith('.md')) {\n // 普通 .md 文件:slug = 完整相对路径去 .md\n const slug = relative(root, full).replace(/\\.md$/, '');\n entries.push(readEntryFromFile(full, slug));\n } else if (stat.isDirectory() && isFolderPackage(full)) {\n // 目录包装式原料:xxx/article.md → slug = xxx 父目录路径\n const articlePath = join(full, 'article.md');\n const slug = relative(root, full);\n entries.push(readEntryFromFile(articlePath, slug));\n }\n }\n\n if (entries.length === 0) return false;\n\n entries.sort((a, b) => b.updated.localeCompare(a.updated));\n\n const lines: string[] = [];\n lines.push(`# ${dirName}`);\n lines.push('');\n lines.push(`> 本目录共 ${entries.length} 个条目。由 \\`lorekit index\\` 自动生成。`);\n lines.push('');\n lines.push('| 条目 | 摘要 | 更新 |');\n lines.push('|---|---|---|');\n for (const e of entries) {\n lines.push(`| [[${e.slug}]] | ${escapeCell(e.summary)} | ${e.updated} |`);\n }\n lines.push('');\n\n writeFileSync(indexFile, lines.join('\\n'), 'utf-8');\n const display = reldir === '' ? '_INDEX.md' : `${reldir}/_INDEX.md`;\n ok(`${display} (${entries.length} entries)`);\n return true;\n}\n\n/**\n * 递归发现\"可索引目录\":\n * - 目录下有直接 .md 文件(非 _INDEX.md / 隐藏)\n * - 或目录下有\"目录包装式原料\"子目录(xxx/article.md 形式)\n *\n * 排除规则:\n * - indexExcludeDirPrefixes 开头的目录整枝跳过\n * - corpus 根本身不索引(L0 = index.md 已承担其职能)\n * - 目录包装式原料的内部目录不递归(它们是条目,不是容器)\n */\nfunction findIndexableDirs(root: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string, isRoot: boolean) {\n const rel = dir === root ? '' : relative(root, dir);\n if (rel && isIndexExcluded(rel)) return;\n\n let names: string[];\n try {\n names = readdirSync(dir, { encoding: 'utf-8' });\n } catch {\n return;\n }\n\n if (!isRoot) {\n let hasIndexable = false;\n for (const name of names) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n\n if (stat.isFile() && name.endsWith('.md')) {\n hasIndexable = true;\n break;\n }\n if (stat.isDirectory() && isFolderPackage(full)) {\n hasIndexable = true;\n break;\n }\n }\n if (hasIndexable) results.push(dir);\n }\n\n // 递归子目录(跳过目录包装式原料的内部)\n for (const name of names) {\n if (name.startsWith('.')) continue;\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n if (!stat.isDirectory()) continue;\n if (isFolderPackage(full)) continue;\n walk(full, false);\n }\n }\n\n walk(root, true);\n return results.sort();\n}\n\n/**\n * 程序内复用入口:扫 corpus 生成所有 _INDEX.md。\n * 返回生成的文件数。specificDir 限定在单个子目录(相对 root 的路径)。\n *\n * 铁律:corpus 根不建 _INDEX.md —— L0 `corpus/index.md` 已经承担根级索引职能。\n * `--dir .` / `--dir \"\"` / `--dir ./` 这类 bypass 会被拒绝。\n */\nexport function runIndex(root: string, specificDir?: string): number {\n if (specificDir) {\n const full = join(root, specificDir);\n if (!existsSync(full)) {\n throw new Error(`directory not found: ${specificDir}`);\n }\n // 防止 --dir . / --dir \"\" / --dir ./ 等写法绕过根排除,在 corpus 根生成 _INDEX.md\n if (resolve(full) === resolve(root)) {\n throw new Error(\n `cannot index the corpus root itself — L0 corpus/index.md already serves this role`,\n );\n }\n // 子目录也要守住排除规则(避免 --dir _工作台 / --dir 系统 等强行生成)\n const rel = relative(root, full);\n if (isIndexExcluded(rel)) {\n throw new Error(\n `directory \"${rel}\" is in the exclude list (${indexExcludeDirPrefixes.join(' / ')})`,\n );\n }\n return buildIndex(full, root) ? 1 : 0;\n }\n const dirs = findIndexableDirs(root);\n if (dirs.length === 0) return 0;\n let generated = 0;\n for (const d of dirs) {\n if (buildIndex(d, root)) generated++;\n }\n return generated;\n}\n\nexport function indexCommand(program: Command): void {\n const cmd = program\n .command('index')\n .description('Generate _INDEX.md recursively for corpus directories')\n .option('--dir ', 'Only update a specific subdirectory');\n\n cmd.action((opts) => {\n const root = requireCorpus();\n\n try {\n if (opts.dir) {\n runIndex(root, opts.dir);\n } else {\n const generated = runIndex(root);\n if (generated === 0) {\n warn('no indexable directories found');\n } else {\n ok(`generated ${generated} _INDEX.md file(s)`);\n }\n }\n } catch (e) {\n err((e as Error).message);\n process.exit(1);\n }\n });\n}\n","import { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n symlinkSync,\n unlinkSync,\n readlinkSync,\n lstatSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport { lorekitRoot } from '../utils/fs.js';\nimport { ok, err, out, print } from '../utils/logger.js';\n\nfunction isSymlink(path: string): boolean {\n try {\n return lstatSync(path).isSymbolicLink();\n } catch {\n return false;\n }\n}\n\nexport function installSkillsCommand(program: Command): void {\n const cmd = program\n .command('install-skills')\n .description('Install lorekit skills into a harness (e.g. Claude Code)')\n .option('--target ', 'Target harness (currently only \"claude-code\")')\n .option('--list', 'List currently installed wiki-* skill symlinks')\n .option('--uninstall', 'Remove installed skill symlinks');\n\n cmd.action((opts) => {\n const skillsDest = join(process.env.HOME ?? '', '.claude', 'skills');\n\n // --list mode\n if (opts.list) {\n if (!existsSync(skillsDest)) return;\n const names = readdirSync(skillsDest, { encoding: 'utf-8' });\n for (const name of names) {\n if (!name.startsWith('wiki-')) continue;\n const full = join(skillsDest, name);\n if (!isSymlink(full)) continue;\n const target = readlinkSync(full);\n out(`${name} -> ${target}`);\n }\n return;\n }\n\n // Require --target\n if (!opts.target) {\n err('install-skills: --target required');\n process.exit(2);\n }\n if (opts.target !== 'claude-code') {\n err(`target '${opts.target}' not supported; only 'claude-code' is available`);\n process.exit(2);\n }\n\n mkdirSync(skillsDest, { recursive: true });\n\n const skillsSrc = join(lorekitRoot(), 'skills');\n if (!existsSync(skillsSrc)) {\n err(`skills directory not found: ${skillsSrc}`);\n process.exit(1);\n }\n\n // Find wiki-* skill directories\n const allNames = readdirSync(skillsSrc, { encoding: 'utf-8' });\n const skillNames = allNames.filter((name) => {\n if (!name.startsWith('wiki-')) return false;\n try {\n return lstatSync(join(skillsSrc, name)).isDirectory();\n } catch {\n return false;\n }\n });\n\n let count = 0;\n for (const name of skillNames) {\n const srcDir = join(skillsSrc, name);\n const skillFile = join(srcDir, 'SKILL.md');\n if (!existsSync(skillFile)) continue;\n\n const dest = join(skillsDest, name);\n\n if (opts.uninstall) {\n if (isSymlink(dest)) {\n unlinkSync(dest);\n ok(`removed ${name}`);\n count++;\n }\n } else {\n // Remove existing symlink if present\n if (isSymlink(dest)) unlinkSync(dest);\n\n symlinkSync(srcDir, dest);\n ok(`linked ${name}`);\n count++;\n }\n }\n\n if (count === 0) {\n print('No skills found to install.');\n } else if (!opts.uninstall) {\n print(`\\nInstalled ${count} skill(s). Restart Claude Code to load them.`);\n }\n });\n}\n","import type { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n writeFileSync,\n unlinkSync,\n readdirSync,\n statSync,\n} from 'node:fs';\nimport { join, relative } from 'node:path';\nimport * as tar from 'tar';\nimport { ok, bad, err } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { snapshotExcludeNames } from '../lib/paths.js';\nimport { tsCompact } from '../lib/date.js';\nimport { sha256 } from '../utils/fs.js';\n\ninterface ManifestEntry {\n path: string;\n sha256: string;\n bytes: number;\n mtime: string;\n}\n\nfunction collectAllFiles(dir: string, base: string): string[] {\n const results: string[] = [];\n\n function walk(d: string) {\n for (const entry of readdirSync(d, { withFileTypes: true })) {\n if (snapshotExcludeNames.has(entry.name)) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else {\n results.push(relative(base, full));\n }\n }\n }\n\n walk(dir);\n return results.sort();\n}\n\nexport async function createSnapshot(corpus: string, opts: { tag?: string } = {}): Promise {\n const snapshotsDir = join(corpus, '.wiki', 'snapshots');\n mkdirSync(snapshotsDir, { recursive: true });\n\n // Collect files\n const files = collectAllFiles(corpus, corpus);\n if (files.length === 0) {\n throw new Error('no files found in corpus');\n }\n\n // Build manifest\n const manifest: ManifestEntry[] = files.map((relPath) => {\n const full = join(corpus, relPath);\n const st = statSync(full);\n return {\n path: relPath,\n sha256: sha256(full),\n bytes: st.size,\n mtime: st.mtime.toISOString(),\n };\n });\n\n // Write temporary manifest into snapshots dir\n const manifestPath = join(snapshotsDir, 'manifest.json');\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n');\n\n try {\n // Build filename: YYYYMMDD-HHMMSS[-tag].tar.gz\n const tag = opts.tag ? `-${opts.tag}` : '';\n const tarName = `${tsCompact()}${tag}.tar.gz`;\n const tarPath = join(snapshotsDir, tarName);\n\n // Create tarball\n // Include all corpus files + the manifest\n const allEntries = [...files, relative(corpus, manifestPath)];\n\n await tar.create(\n {\n gzip: true,\n file: tarPath,\n cwd: corpus,\n prefix: '',\n },\n allEntries,\n );\n\n return tarPath;\n } finally {\n // LEGACY P4-2:tar.create 若抛错,manifest 原先会残留在 .wiki/snapshots/;\n // 放 finally 保证无论成功 / 失败都清掉。\n if (existsSync(manifestPath)) unlinkSync(manifestPath);\n }\n}\n\nexport function snapshotCommand(program: Command) {\n program\n .command('snapshot')\n .option('--tag ', 'optional tag appended to filename')\n .description('create a tarball snapshot of the corpus')\n .action(async (opts: { tag?: string }) => {\n const corpus = requireCorpus();\n try {\n const tarPath = await createSnapshot(corpus, opts);\n const tarStat = statSync(tarPath);\n const sizeMB = (tarStat.size / 1024 / 1024).toFixed(1);\n const count = collectAllFiles(corpus, corpus).length;\n ok(`snapshot saved: ${tarPath} (${count} files, ${sizeMB} MB)`);\n } catch (e) {\n const message = (e as Error).message;\n if (message === 'no files found in corpus') {\n bad(message);\n } else {\n err(message);\n process.exitCode = 1;\n }\n return;\n }\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, rmSync } from 'node:fs';\nimport { join, dirname, relative } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport { tmpdir } from 'node:os';\nimport * as tar from 'tar';\nimport chalk from 'chalk';\nimport { ok, bad, err, warn, print } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { sha256 } from '../utils/fs.js';\n\ninterface ManifestEntry {\n path: string;\n sha256: string;\n bytes: number;\n mtime: string;\n}\n\ntype DiffKind = 'MISSING' | 'CHANGED';\n\ninterface DiffEntry {\n kind: DiffKind;\n path: string;\n snapshotSha: string;\n currentSha: string | null;\n}\n\nfunction ask(question: string): Promise {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\n/**\n * 仅限 os.tmpdir() 子目录,不许扩展到任何用户数据路径。\n *\n * LEGACY P4-3 / 先生全局 CLAUDE.md 数据安全红线:lorekit 源码里的任何\n * `rm -rf` 等价操作都必须锁定在 tmpdir 下。本函数的唯一调用方 restore\n * action 中,`tmpDir` 由 `join(tmpdir(), 'lorekit-restore-' + Date.now())`\n * 构造——禁止把 user corpus 路径传进来。\n */\nfunction rmDirRecursive(dir: string) {\n rmSync(dir, { recursive: true, force: true });\n}\n\nexport function restoreCommand(program: Command) {\n program\n .command('restore')\n .requiredOption('--from ', 'path to snapshot .tar.gz')\n .option('--dry-run', 'only list differences, do not restore')\n .option('--file ', 'restore only this specific file')\n .description('restore files from a snapshot')\n .action(async (opts: { from: string; dryRun?: boolean; file?: string }) => {\n const corpus = requireCorpus();\n\n if (!existsSync(opts.from)) {\n bad(`snapshot not found: ${opts.from}`);\n // CONVENTIONS #4:用户提供的 --from 路径不存在 → 参数错 → exit 2\n process.exitCode = 2;\n return;\n }\n\n // Extract to temp dir\n const tmpDir = join(tmpdir(), `lorekit-restore-${Date.now()}`);\n mkdirSync(tmpDir, { recursive: true });\n\n try {\n await tar.extract({\n file: opts.from,\n cwd: tmpDir,\n });\n\n // Read manifest\n const manifestPath = join(tmpDir, '.wiki', 'snapshots', 'manifest.json');\n if (!existsSync(manifestPath)) {\n bad('manifest.json not found in snapshot');\n process.exitCode = 1;\n return;\n }\n\n const manifest: ManifestEntry[] = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n\n // Compute diffs\n const diffs: DiffEntry[] = [];\n\n for (const entry of manifest) {\n // Skip if --file is specified and doesn't match\n if (opts.file && entry.path !== opts.file) continue;\n\n const corpusPath = join(corpus, entry.path);\n if (!existsSync(corpusPath)) {\n diffs.push({\n kind: 'MISSING',\n path: entry.path,\n snapshotSha: entry.sha256,\n currentSha: null,\n });\n } else {\n const currentSha = sha256(corpusPath);\n if (currentSha !== entry.sha256) {\n diffs.push({\n kind: 'CHANGED',\n path: entry.path,\n snapshotSha: entry.sha256,\n currentSha,\n });\n }\n }\n }\n\n if (diffs.length === 0) {\n ok('corpus matches snapshot — nothing to restore');\n return;\n }\n\n // Display diffs\n const missing = diffs.filter((d) => d.kind === 'MISSING');\n const changed = diffs.filter((d) => d.kind === 'CHANGED');\n\n if (missing.length > 0) {\n print(chalk.yellow(`\\n MISSING (${missing.length}):`));\n for (const d of missing) {\n print(` + ${d.path}`);\n }\n }\n if (changed.length > 0) {\n print(chalk.cyan(`\\n CHANGED (${changed.length}):`));\n for (const d of changed) {\n print(` ~ ${d.path}`);\n }\n }\n print();\n\n if (opts.dryRun) {\n warn(`dry-run: ${diffs.length} file(s) would be restored`);\n return;\n }\n\n // Confirm\n const answer = await ask(` restore ${diffs.length} file(s)? [y/N] `);\n if (answer.toLowerCase() !== 'y') {\n bad('cancelled');\n return;\n }\n\n // Copy files from tmpDir to corpus\n let restored = 0;\n for (const d of diffs) {\n const src = join(tmpDir, d.path);\n const dest = join(corpus, d.path);\n if (!existsSync(src)) {\n warn(`file not in snapshot archive: ${d.path}`);\n continue;\n }\n mkdirSync(dirname(dest), { recursive: true });\n copyFileSync(src, dest);\n restored++;\n }\n\n ok(`restored ${restored} file(s) from snapshot`);\n } finally {\n // Clean up temp dir\n rmDirRecursive(tmpDir);\n }\n });\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { ok, bad, warn, out } from '../utils/logger.js';\nimport { requireCorpus, collectMdFiles } from '../lib/corpus.js';\n\ninterface SearchResult {\n file: string;\n line: number;\n text: string;\n}\n\nfunction searchWithRipgrep(\n query: string,\n corpus: string,\n opts: { type?: string; dir?: string },\n): SearchResult[] {\n const searchDir = opts.dir ? join(corpus, opts.dir) : corpus;\n const args: string[] = ['--json', '--no-heading', '-i'];\n\n if (opts.type) {\n args.push('--type', opts.type);\n }\n\n // Exclude internal dirs\n args.push('--glob', '!.wiki/**', '--glob', '!.git/**');\n args.push(query, searchDir);\n\n const result = spawnSync('rg', args, {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024,\n });\n\n if (result.error) {\n // rg not found\n return [];\n }\n\n const results: SearchResult[] = [];\n for (const line of (result.stdout || '').split('\\n')) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line);\n if (obj.type === 'match') {\n results.push({\n file: relative(corpus, obj.data.path.text),\n line: obj.data.line_number,\n text: obj.data.lines.text.trimEnd(),\n });\n }\n } catch {\n // skip malformed lines\n }\n }\n return results;\n}\n\nfunction searchFallback(query: string, corpus: string, opts: { dir?: string }): SearchResult[] {\n const searchDir = opts.dir ? join(corpus, opts.dir) : corpus;\n const files = collectMdFiles(searchDir);\n const pattern = new RegExp(query, 'i');\n const results: SearchResult[] = [];\n\n for (const filePath of files) {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n if (pattern.test(lines[i])) {\n results.push({\n file: relative(corpus, filePath),\n line: i + 1,\n text: lines[i].trimEnd(),\n });\n }\n }\n }\n return results;\n}\n\nfunction hasRipgrep(): boolean {\n const result = spawnSync('rg', ['--version'], { encoding: 'utf-8' });\n return !result.error && result.status === 0;\n}\n\nexport function searchCommand(program: Command) {\n program\n .command('search')\n .argument('', 'search query (regex supported)')\n .option('--type ', 'file type filter (passed to rg --type)')\n .option('--dir ', 'subdirectory within corpus to search')\n .description('search the corpus with ripgrep (fallback: built-in)')\n .action((query: string, opts: { type?: string; dir?: string }) => {\n const corpus = requireCorpus();\n\n let results: SearchResult[];\n\n if (hasRipgrep()) {\n results = searchWithRipgrep(query, corpus, opts);\n } else {\n warn('rg (ripgrep) not found, using built-in fallback');\n results = searchFallback(query, corpus, { dir: opts.dir });\n }\n\n // TODO Phase 3: if .wiki/vector.sqlite exists, also run vector similarity\n // search and merge results with text search hits.\n\n // Output JSON lines\n for (const r of results) {\n out(JSON.stringify(r));\n }\n\n if (results.length === 0) {\n warn('no results');\n }\n });\n}\n","import type { Command } from 'commander';\nimport { createHash } from 'node:crypto';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { ok, warn, err, out, print } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { pruneMissingDocuments } from '../lib/vectordb/prune.js';\n\nexport interface VectorSyncOptions {\n force?: boolean;\n layered?: boolean;\n model?: string;\n}\n\nexport interface VectorSyncResult {\n synced: number;\n skipped: number;\n totalChunks: number;\n layered: boolean;\n pruned: number;\n}\n\n/**\n * 程序内复用入口:增量同步向量库。\n * - 每个 .md 文件用 sha256 对比跳过未变更\n * - --force 全量重嵌入\n * - --layered 额外刷 L0/L1(默认 true——lorekit sync 需要)\n */\nexport async function runVectorSync(\n corpus: string,\n opts: VectorSyncOptions = {},\n): Promise {\n const force = opts.force ?? false;\n const layered = opts.layered ?? true;\n const model = opts.model ?? 'bge-m3';\n\n const { embed, embedSingle } = await import('../lib/ollama.js');\n const { openDb, syncFile, buildLayeredIndex, collectFiles } = await import('../lib/vectordb/index.js');\n\n const testEmb = await embedSingle('test', model);\n const dim = testEmb.length;\n\n const db = await openDb(corpus, dim);\n const files = collectFiles(corpus);\n const existingRelPaths = new Set(files.map((filePath) => relative(corpus, filePath)));\n const pruned = pruneMissingDocuments(db, existingRelPaths);\n if (pruned > 0) warn(`vector sync pruned ${pruned} missing file(s)`);\n\n let synced = 0;\n let skipped = 0;\n let totalChunks = 0;\n\n for (const filePath of files) {\n // LEGACY P4-5:原先 `filePath.replace(corpus + '/', '')` 字符串替换,\n // 若 corpus 路径在 filePath 内出现多次(罕见)会替换错位;用 path.relative 更稳。\n const rel = relative(corpus, filePath);\n\n if (!force) {\n const row = db.prepare('SELECT sha256 FROM documents WHERE path = ?').get(rel) as\n | { sha256: string }\n | undefined;\n if (row) {\n const sha = createHash('sha256').update(readFileSync(filePath)).digest('hex');\n if (row.sha256 === sha) {\n skipped++;\n continue;\n }\n }\n }\n\n const embedFn = (texts: string[]) => embed(texts, model);\n const result = await syncFile(db, filePath, corpus, embedFn);\n totalChunks += result.chunks;\n synced++;\n }\n\n const now = new Date().toISOString();\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('last_sync', ?)\").run(now);\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('model', ?)\").run(model);\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('dim', ?)\").run(String(dim));\n\n if (layered || force) {\n print('Building layered index (L0/L1)...');\n const embedBatch = (texts: string[]) => embed(texts, model);\n await buildLayeredIndex(db, corpus, embedBatch);\n }\n\n db.close();\n\n return { synced, skipped, totalChunks, layered: layered || force, pruned };\n}\n\nexport function vectorCommand(program: Command) {\n const vec = program\n .command('vector')\n .description('vector search engine — embed & search via ollama + sqlite-vec');\n\n // --- sync ---\n vec\n .command('sync')\n .option('--force', 'full rebuild (re-embed all files)', false)\n .option('--layered', 'build L0/L1 layered index', false)\n .option('--model ', 'ollama model name', 'bge-m3')\n .description('index corpus into vector DB')\n .action(async (opts: { force: boolean; layered: boolean; model: string }) => {\n const corpus = requireCorpus();\n const r = await runVectorSync(corpus, opts);\n ok(`synced ${r.synced} files (${r.totalChunks} chunks), skipped ${r.skipped} unchanged`);\n });\n\n // --- query ---\n vec\n .command('query')\n .requiredOption('--text ', 'search query text')\n .option('--top-k ', 'number of results', '5')\n .option('--threshold ', 'minimum similarity score', '0.5')\n .option('--layered', 'use L0→L1→L2 layered vector retrieval', false)\n .option('--hybrid', 'BM25 + vector layered + RRF fusion (阶段 2 推荐,无 re-rank)', false)\n .option('--bm25', 'BM25 layered only (FTS5, 用于 debug BM25 单路)', false)\n .option('--model ', 'ollama model name', 'bge-m3')\n .description('search the vector/FTS index')\n .action(\n async (opts: {\n text: string;\n topK: string;\n threshold: string;\n layered: boolean;\n hybrid: boolean;\n bm25: boolean;\n model: string;\n }) => {\n const corpus = requireCorpus();\n const topK = parseInt(opts.topK, 10);\n const threshold = parseFloat(opts.threshold);\n\n // CONVENTIONS #4:参数解析失败 → exit 2\n if (!Number.isFinite(topK) || topK <= 0) {\n err(`--top-k must be a positive integer, got: \"${opts.topK}\"`);\n process.exit(2);\n }\n if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {\n err(`--threshold must be a number in [0, 1], got: \"${opts.threshold}\"`);\n process.exit(2);\n }\n\n const { embedSingle } = await import('../lib/ollama.js');\n const { openDb, queryFlat, queryLayered, queryBM25Layered, queryHybrid } =\n await import('../lib/vectordb/index.js');\n\n // Probe dim from existing db or model\n let dim = 1024;\n const dbPath = join(corpus, '.wiki', 'vector.sqlite');\n if (existsSync(dbPath)) {\n const tmpDb = await openDb(corpus);\n const row = tmpDb.prepare(\"SELECT value FROM meta WHERE key = 'dim'\").get() as\n | { value: string }\n | undefined;\n if (row) dim = parseInt(row.value, 10);\n tmpDb.close();\n }\n\n const db = await openDb(corpus, dim);\n\n let results;\n if (opts.bm25) {\n // BM25 单路(不需要 embedding,快)\n results = queryBM25Layered(db, opts.text, topK);\n } else if (opts.hybrid) {\n const embedding = await embedSingle(opts.text, opts.model);\n results = queryHybrid(db, embedding, opts.text, topK, threshold);\n } else {\n const embedding = await embedSingle(opts.text, opts.model);\n results = opts.layered\n ? queryLayered(db, embedding, topK, threshold)\n : queryFlat(db, embedding, topK, threshold);\n }\n\n db.close();\n out(JSON.stringify(results, null, 2));\n },\n );\n\n // --- status ---\n vec\n .command('status')\n .description('show vector index status')\n .action(async () => {\n const corpus = requireCorpus();\n const { getStatus } = await import('../lib/vectordb/index.js');\n const info = await getStatus(corpus);\n out(JSON.stringify(info, null, 2));\n });\n}\n","import { relative } from 'node:path';\nimport { collectFiles } from './files.js';\nimport { openDb, type Db } from './schema.js';\n\nexport function pruneMissingDocuments(db: Db, existingRelPaths: Set): number {\n const rows = db.prepare('SELECT id, path FROM documents').all() as { id: number; path: string }[];\n const missing = rows.filter((row) => !existingRelPaths.has(row.path));\n if (missing.length === 0) return 0;\n\n const delVecChunk = db.prepare('DELETE FROM vec_chunks WHERE rowid = ?');\n const delFtsChunk = db.prepare('DELETE FROM fts_chunks WHERE rowid = ?');\n const delVecPage = db.prepare('DELETE FROM vec_pages WHERE rowid = ?');\n const delFtsPage = db.prepare('DELETE FROM fts_pages WHERE rowid = ?');\n const getChunkIds = db.prepare('SELECT id FROM chunks WHERE doc_id = ?');\n const getPageIds = db.prepare('SELECT id FROM page_summaries WHERE doc_id = ?');\n const deleteChunks = db.prepare('DELETE FROM chunks WHERE doc_id = ?');\n const deletePages = db.prepare('DELETE FROM page_summaries WHERE doc_id = ?');\n const deleteDoc = db.prepare('DELETE FROM documents WHERE id = ?');\n\n const tx = db.transaction((docs: typeof missing) => {\n for (const doc of docs) {\n const chunkIds = getChunkIds.all(doc.id) as { id: number }[];\n for (const { id } of chunkIds) {\n delVecChunk.run(id);\n delFtsChunk.run(id);\n }\n deleteChunks.run(doc.id);\n\n const pageIds = getPageIds.all(doc.id) as { id: number }[];\n for (const { id } of pageIds) {\n delVecPage.run(id);\n delFtsPage.run(id);\n }\n deletePages.run(doc.id);\n deleteDoc.run(doc.id);\n }\n });\n tx(missing);\n return missing.length;\n}\n\nexport async function pruneVectorDbMissingFiles(corpus: string): Promise {\n const db = await openDb(corpus);\n try {\n const files = collectFiles(corpus);\n const existingRelPaths = new Set(files.map((filePath) => relative(corpus, filePath)));\n return pruneMissingDocuments(db, existingRelPaths);\n } finally {\n db.close();\n }\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { findCorpus, findSourceByUrl, extractFrontmatter } from '../lib/corpus.js';\nimport { fetchUrl, fetchGist, fetchGithubDoc } from '../lib/fetcher/index.js';\nimport type { FetchResult } from '../lib/fetcher/index.js';\nimport { getIngestRecord, upsertIngestRecord, nextStepHint } from '../lib/ingest-state.js';\n\n// ---------------------------------------------------------------------------\n// URL routing helpers\n// ---------------------------------------------------------------------------\n\nfunction suggestResult(route: string, url: string, suggest: string): FetchResult {\n return { status: 'unsupported', route, url, suggest };\n}\n\nfunction getHost(url: string): string {\n try {\n return new URL(url).hostname.toLowerCase();\n } catch {\n return '';\n }\n}\n\nfunction isPdfUrl(url: string): boolean {\n try {\n const path = new URL(url).pathname.toLowerCase();\n return path.endsWith('.pdf');\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command\n// ---------------------------------------------------------------------------\n\nexport function fetchCommand(program: Command) {\n program\n .command('fetch')\n .argument('', 'URL to fetch')\n .option('--out ', 'output directory')\n .option('--force-rich', 'skip host routing, always use rich fetcher')\n .option('--no-images', 'skip image downloads')\n .option('--force', 'ignore duplicate-URL check and re-fetch anyway')\n .description('Fetch a URL into local markdown + images')\n .action(\n async (\n url: string,\n opts: { out?: string; forceRich?: boolean; images?: boolean; force?: boolean },\n ) => {\n // Resolve output root\n const corpus = findCorpus();\n let outRoot: string;\n if (opts.out) {\n outRoot = opts.out;\n } else {\n outRoot = corpus ? join(corpus, '_工作台', '收件', 'fetch') : '/tmp/lorekit-fetch';\n }\n if (!existsSync(outRoot)) {\n mkdirSync(outRoot, { recursive: true });\n }\n\n // Duplicate / resume detection: consult ingest-state.json first,\n // fall back to scanning 原料/*/*/article.md frontmatter for legacy ingests\n // without a state record.\n let duplicate: FetchResult['duplicate'] | undefined;\n if (corpus && !opts.force) {\n const state = getIngestRecord(corpus, url);\n\n if (state && state.status !== 'completed') {\n // Interrupted ingest — surface resume hint, do not re-fetch\n const hint = nextStepHint(state);\n console.error(\n `[lorekit fetch] in-progress ingest detected for ${url}\\n` +\n ` status: ${state.status} steps done: ${state.stepsDone.join(', ') || '(none)'}\\n` +\n ` started: ${state.startedAt}\\n` +\n ` next step → ${hint}\\n` +\n ` use --force to restart from scratch`,\n );\n console.log(\n JSON.stringify({\n status: 'in_progress',\n route: 'rich',\n url,\n ingestState: state,\n nextStep: hint,\n }),\n );\n return;\n }\n\n if (state && state.status === 'completed') {\n duplicate = {\n path: state.archivedTo ?? '(unknown)',\n sourceDate: state.sourceDate,\n title: state.title,\n };\n } else {\n // No state record — fall back to frontmatter scan\n const existing = findSourceByUrl(corpus, url);\n if (existing) {\n const fm = extractFrontmatter(existing);\n const sdRaw = fm.source_date;\n const sourceDate =\n typeof sdRaw === 'string'\n ? sdRaw\n : sdRaw instanceof Date\n ? sdRaw.toISOString().slice(0, 10)\n : undefined;\n duplicate = {\n path: relative(corpus, existing),\n sourceDate,\n title: typeof fm.title === 'string' ? fm.title : undefined,\n };\n }\n }\n\n if (duplicate) {\n console.error(\n `[lorekit fetch] duplicate url: ${url} already ingested at ${duplicate.path}` +\n (duplicate.sourceDate ? ` (source_date: ${duplicate.sourceDate})` : '') +\n `. Use --force to re-fetch anyway.`,\n );\n console.log(JSON.stringify({ status: 'duplicate', route: 'rich', url, duplicate }));\n return;\n }\n }\n\n // Route by host (unless --force-rich)\n const noImages = opts.images === false;\n let result: FetchResult;\n\n if (opts.forceRich) {\n result = await fetchUrl(url, { outRoot, noImages });\n } else {\n const host = getHost(url);\n\n if (host.includes('mp.weixin.qq.com')) {\n result = await fetchUrl(url, { outRoot, noImages });\n } else if (host.includes('feishu.cn') || host.includes('larkoffice.com')) {\n result = suggestResult('lark', url, 'lark-cli docs +read --as user --doc ');\n } else if (\n host === 'x.com' ||\n host === 'twitter.com' ||\n host.endsWith('.x.com') ||\n host.endsWith('.twitter.com')\n ) {\n result = suggestResult('x', url, 'paste screenshot or text (antibot too strong)');\n } else if (host === 'gist.github.com' || host === 'gist.githubusercontent.com') {\n result = await fetchGist(url, outRoot);\n } else if (host === 'github.com' || host === 'www.github.com') {\n result = await fetchGithubDoc(url, outRoot);\n } else if (isPdfUrl(url)) {\n result = suggestResult('pdf', url, 'pdf skill');\n } else {\n // Generic site\n result = await fetchUrl(url, { outRoot, noImages });\n }\n }\n\n // On successful fetch into a corpus, record state so subsequent runs\n // see this as an in-progress ingest until the agent marks it completed.\n if (corpus && result.status === 'ok' && result.markdown) {\n upsertIngestRecord(corpus, url, {\n title: result.title,\n sourceDate: result.publishDate,\n status: 'started',\n stepsDone: ['fetch'],\n workbenchMd: result.markdown,\n });\n }\n\n // Output single-line JSON\n console.log(JSON.stringify(result));\n\n // Exit with non-zero on error\n if (result.status === 'error') {\n process.exitCode = 1;\n }\n },\n );\n}\n","/**\n * fetcher/index.ts — fetcher 子模块对外主入口(barrel + fetchUrl 主流程)\n *\n * 批次 21g-pre strangler fig 第七步:建主入口模块。本文件含:\n * - `fetchUrl`:从 src/lib/fetcher.ts:493-606 copy,dispatcher 模式根据 site\n * 选 routes/web 或 routes/weixin 的 parser。frontmatter 拼装替换为 21b\n * buildFrontmatter(routeKind = 'article' | 'clipping')\n * - `fetchGist` / `fetchGithubDoc`:从 routes 直接 re-export\n * - `FetchResult` / `FetchOptions`:从 types.ts 直接 re-export(type-only)\n *\n * **不 re-export** parser / helpers / http / images / frontmatter —— 这些是\n * fetcher 子模块内部实现细节,对外 surface 只有 4 个公开 API。\n *\n * 21g-pre 阶段:本文件目前未被任何调用方 import;commands/fetch.ts 仍 import\n * 旧 src/lib/fetcher.ts。21g-final 才切换 + 删旧。\n *\n * **Strangler fig 双份代码状态**:\n * - 旧 src/lib/fetcher.ts 的 fetchUrl / fetchGist / fetchGithubDoc 完整保留\n * - 新 src/lib/fetcher/index.ts 的 fetchUrl 与旧版 byte 等价(21g-pre parity 验证)\n * - 21g-final commit 一旦完成切换 + 删旧,本文件就成为 SSOT\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { buildFrontmatter } from './frontmatter.js';\nimport { htmlToMarkdown, slugify, todayYMD } from './helpers.js';\nimport {\n buildHeaders,\n detectAntibot,\n detectSite,\n fetchHtmlL1,\n fetchHtmlL2,\n} from './http.js';\nimport { downloadImages, rewriteMarkdownImages } from './images.js';\nimport { parseGeneric } from './routes/web.js';\nimport { parseWeixin } from './routes/weixin.js';\nimport type { FetchOptions, FetchResult } from './types.js';\n\n// ---------------------------------------------------------------------------\n// fetchUrl 主入口(dispatcher)\n// ---------------------------------------------------------------------------\n\n/**\n * 主入口:URL → 本地 markdown + 图片。\n *\n * Pipeline:\n * 1. detectSite 选 weixin / generic 的 headers / parser\n * 2. L1 fetch;命中 antibot 关键字 → 清空 html\n * 3. L1 失败或被拦 → L2 playwright fallback;仍失败 → 报 ANTIBOT_BLOCKED\n * 4. site 选 parseWeixin / parseGeneric\n * 5. body 太短报 empty_body\n * 6. htmlToMarkdown → 写文件(含 21b buildFrontmatter)+ 下载图 + 改写图链\n *\n * 与旧 fetcher.ts:493-606 byte 等价(21g-pre parity 验证 generic + weixin 各一例)。\n */\nexport async function fetchUrl(url: string, opts: FetchOptions): Promise {\n const site = detectSite(url);\n const headers = buildHeaders(site);\n let sourceLayer = 'L1';\n let html = '';\n\n // --- L1 fetch ---\n try {\n html = await fetchHtmlL1(url, headers);\n if (detectAntibot(html, site)) {\n html = '';\n }\n } catch {\n // L1 失败(HTTP 非 2xx / abort / 网络错误)→ 退 L2 fallback\n html = '';\n }\n\n // --- L2 fallback ---\n if (!html) {\n sourceLayer = 'L2';\n const l2html = await fetchHtmlL2(url);\n if (!l2html) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'ANTIBOT_BLOCKED',\n suggest: 'Install playwright-core + chromium, or paste content manually',\n };\n }\n html = l2html;\n if (detectAntibot(html, site)) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'ANTIBOT_BLOCKED',\n suggest: 'Site requires login or manual intervention',\n };\n }\n }\n\n // --- Parse ---\n const doc = site === 'weixin' ? parseWeixin(html, url) : parseGeneric(html, url);\n\n if (!doc.bodyHtml || doc.bodyHtml.replace(/<[^>]*>/g, '').trim().length < 50) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'empty_body',\n };\n }\n\n // --- Convert to markdown ---\n let md = htmlToMarkdown(doc.bodyHtml);\n\n // --- Output paths (Obsidian-compatible flat layout) ---\n // /.md\n // /.assets/img_01.jpg\n const slug = slugify(doc.title || 'untitled');\n const assetsDir = join(opts.outRoot, `${slug}.assets`);\n await mkdir(opts.outRoot, { recursive: true });\n\n // --- Download images ---\n let imagesOk = 0;\n let imagesFailed = 0;\n if (!opts.noImages && doc.imgSrcs.length > 0) {\n const imgResults = await downloadImages(doc.imgSrcs, assetsDir, headers, `./${slug}.assets/`);\n md = rewriteMarkdownImages(md, imgResults);\n for (const r of imgResults) {\n if (r.status === 'ok') imagesOk++;\n else imagesFailed++;\n }\n }\n\n // --- Build frontmatter + write article.md ---\n // Follows templates/default-corpus/系统/frontmatter-spec.md\n // 21b buildFrontmatter(routeKind 二元)替换原 inline fmLines 拼装;title/\n // author 在 generic/weixin 路由都是条件输出(21b 已用 generic-full /\n // weixin-no-author-no-date / generic-no-title-no-author 三 case 验证 byte 等价)\n const sourceKind: 'article' | 'clipping' = site === 'weixin' ? 'clipping' : 'article';\n const today = todayYMD();\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: sourceKind,\n title: doc.title,\n today,\n url,\n author: doc.author,\n publishDate: doc.publishDate,\n }),\n );\n fmLines.push('');\n if (doc.title) fmLines.push(`# ${doc.title}`, '');\n fmLines.push(md, '');\n\n const articlePath = join(opts.outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'rich',\n url,\n title: doc.title || undefined,\n author: doc.author || undefined,\n publishDate: doc.publishDate,\n sourceKind,\n sourceLayer,\n slug,\n markdown: articlePath,\n assetsDir,\n imagesOk,\n imagesFailed,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API re-exports\n// ---------------------------------------------------------------------------\n\nexport { fetchGist } from './routes/gist.js';\nexport { fetchGithubDoc } from './routes/github.js';\nexport type { FetchOptions, FetchResult } from './types.js';\n","/**\n * fetcher/frontmatter.ts — 4 路由共用的 frontmatter 拼装\n *\n * 批次 21b strangler fig 第二步:从 src/lib/fetcher.ts 抽出 frontmatter 生成逻辑。\n * 原 fetcher.ts 仍保留 3 处内嵌拼装代码(generic/weixin 共一处 + gist + github),\n * commands/*.ts 暂未切换,本文件目前未被使用,21f 才切换并删旧文件。\n *\n * ## 4 路由字段差异矩阵\n *\n * | 字段 | generic/weixin | gist | github |\n * | ----------------------------- | ----------------- | ------------- | ----------- |\n * | `type: source` | 总有 | 总有 | 总有 |\n * | `title: \"...\"` | **条件** if title | 总有 | 总有 |\n * | `created: ` | 总有 | 总有 | 总有 |\n * | `updated: ` | 总有 | 总有 | 总有 |\n * | `source_url: ` | 总有 | 总有 | 总有 |\n * | `source_author: \"...\"` | **条件** if 有 | 总有 | 总有 |\n * | `source_date: ` | 条件 if 有 | 条件 if 有 | **从不输出**|\n * | `source_kind: ` | article/clipping | 固定 gist | 固定 github |\n *\n * 字段顺序、引号风格、缺字段不输出的语义都按上表,与原 4 路由内嵌实现 byte-level 等价。\n *\n * ## byte-level 一致性验证(手动)\n *\n * 用 4 mock 输入分别比对 buildFrontmatter() 和原 fetcher.ts 的 fmLines 数组,\n * `JSON.stringify(actual) === JSON.stringify(expected)` 全部 pass,\n * 详见 `tmp/frontmatter-parity-check.mjs`(一次性脚本,跑完 21b 即可丢弃)。\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * 4 路由的 sourceKind 取值。注意 weixin 写出来是 `clipping`(剪藏),\n * generic 是 `article`,gist / github 同名。\n */\nexport type RouteKind = 'article' | 'clipping' | 'gist' | 'github';\n\nexport interface BuildFrontmatterOpts {\n /** sourceKind,决定 `source_kind:` 字段值,也决定 title/author 必输出还是条件输出 */\n routeKind: RouteKind;\n /** 文章标题。generic/weixin 路由可空(不输出 title 行);gist/github 路由必填 */\n title?: string;\n /** 创建/更新日期 `YYYY-MM-DD`,调用方传入(一般是 todayYMD()) */\n today: string;\n /** 抓取来源 URL */\n url: string;\n /** 作者。generic/weixin 路由可空(不输出 source_author 行);gist/github 路由必填 */\n author?: string;\n /** 发布日期 `YYYY-MM-DD`。github 路由忽略此字段(永远不输出) */\n publishDate?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * YAML 双引号字符串内嵌双引号需 `\\\"` 转义。原 4 路由都用同一句 `replace(/\"/g, '\\\\\"')`。\n */\nfunction escapeDoubleQuote(s: string): string {\n return s.replace(/\"/g, '\\\\\"');\n}\n\n// ---------------------------------------------------------------------------\n// Public\n// ---------------------------------------------------------------------------\n\n/**\n * 生成 frontmatter YAML 块(含外层 `---` 起止符),返回行数组。\n *\n * 调用方拿到后用 `lines.push(...buildFrontmatter(opts))` 拼到文章 fmLines 里。\n * 拼接后的下一行通常是空行,再跟正文 `# title` 等。\n *\n * 不负责 slug / 写文件 / 正文 —— 仅 frontmatter 一段。\n *\n * 字段输出语义见文件头注释的差异矩阵。\n */\nexport function buildFrontmatter(opts: BuildFrontmatterOpts): string[] {\n const { routeKind, title, today, url, author, publishDate } = opts;\n const omitPublishDate = routeKind === 'github';\n\n const lines: string[] = ['---'];\n lines.push('type: source');\n\n // title / author 在 generic/weixin 是条件输出;在 gist/github 调用方\n // 必传非空字符串,分支同样命中。统一用 truthy 判定,与原 4 路由 `if (xxx)` 等价。\n if (title) {\n lines.push(`title: \"${escapeDoubleQuote(title)}\"`);\n }\n // slug 留空:fetcher 不知道最终归档位置(_工作台 vs 原料/剪藏 vs 原料/文章),\n // wiki-ingest 在 mv 时再补。语义同原代码注释。\n\n lines.push(`created: ${today}`);\n lines.push(`updated: ${today}`);\n lines.push(`source_url: ${url}`);\n\n if (author) {\n lines.push(`source_author: \"${escapeDoubleQuote(author)}\"`);\n }\n\n if (!omitPublishDate && publishDate) {\n lines.push(`source_date: ${publishDate}`);\n }\n\n lines.push(`source_kind: ${routeKind}`);\n lines.push('---');\n\n return lines;\n}\n","/**\n * fetcher/helpers.ts — 通用工具函数(slug / url / markdown / 日期)\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * self-contained:不 import fetcher 子目录下其他模块。\n */\nimport TurndownService from 'turndown';\n\n// ---------------------------------------------------------------------------\n// 字符串 / URL helper\n// ---------------------------------------------------------------------------\n\n/**\n * 把任意字符串裁成可作文件名的 slug:\n * 保留 word 字符(含中文),其他符号收敛成 `-`,限长 50;空串回退 `untitled`。\n */\nexport function slugify(s: string): string {\n let slug = s.replace(/[^\\w\\u4e00-\\u9fff-]+/g, '-').replace(/^-+|-+$/g, '');\n return slug.slice(0, 50) || 'untitled';\n}\n\n/**\n * 相对 URL 解析为绝对 URL;解析失败则原样返回,避免异常向上抛。\n */\nexport function resolveUrl(src: string, base: string): string {\n try {\n return new URL(src, base).href;\n } catch {\n // URL 构造失败说明输入已经是非法 URI(典型如 `data:` / 空字符串),\n // 维持原值让上游 caller 自己决定怎么处理。\n return src;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTML -> Markdown\n// ---------------------------------------------------------------------------\n\n/**\n * Turndown 包装:固定 ATX heading + fenced code 风格,trim 末尾空白。\n */\nexport function htmlToMarkdown(html: string): string {\n const td = new TurndownService({\n headingStyle: 'atx',\n codeBlockStyle: 'fenced',\n });\n return td.turndown(html).trim();\n}\n\n// ---------------------------------------------------------------------------\n// 日期 helper(Asia/Shanghai 视角)\n// ---------------------------------------------------------------------------\n\nconst SHANGHAI_TZ_OFFSET_MS = 8 * 60 * 60 * 1000;\n\n/**\n * 把 unix seconds 时间戳格式化为 `YYYY-MM-DD`(Asia/Shanghai)。\n */\nexport function tsToYMD(seconds: number): string {\n const d = new Date(seconds * 1000 + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * 当前日期 `YYYY-MM-DD`(Asia/Shanghai)。\n *\n * 注意:批次 9 已建立 `src/lib/date.ts`,迁移到统一 helper 的工作留给批次 21\n * 后续子批一起做(见 LEGACY 批次 9 备注)。本批次仅 copy 保持行为一致。\n */\nexport function todayYMD(): string {\n const d = new Date(Date.now() + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * 把人类常见格式的日期文本归一化为 `YYYY-MM-DD`。\n * 支持 ISO(`2026-04-15` / `2026/04/15` / 带 `T...`)和中文(`2026年4月15日`)。\n * 无法识别返回 undefined。\n */\nexport function normalizeDateText(raw: string): string | undefined {\n const s = raw.trim();\n if (!s) return undefined;\n // ISO-ish: 2026-04-15, 2026/04/15, 2026-04-15T10:00:00+08:00\n const iso = s.match(/(\\d{4})[-/.](\\d{1,2})[-/.](\\d{1,2})/);\n if (iso) {\n const [, y, m, d] = iso;\n return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;\n }\n // Chinese: 2026年4月15日\n const zh = s.match(/(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日/);\n if (zh) {\n const [, y, m, d] = zh;\n return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;\n }\n return undefined;\n}\n","/**\n * fetcher/http.ts — 站点检测、请求头、antibot 探测、L1/L2 抓页\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * 依赖关系:本文件不 import fetcher/helpers.ts 或 fetcher/images.ts,self-contained。\n * `HTTP_TIMEOUT_MS` 同时被 fetcher/images.ts 引用(images→http 单向依赖)。\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst UA_IPHONE =\n 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' +\n 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 ' +\n 'Mobile/15E148 Safari/604.1';\n\nconst UA_DESKTOP =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +\n 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';\n\n/**\n * L1 / L2 抓页超时(ms)。images.ts 下载图片时也复用此常量。\n */\nexport const HTTP_TIMEOUT_MS = 20_000;\n\nconst ANTIBOT_TRIGGERS = [\n '环境异常',\n '请在微信客户端打开',\n '完成验证后即可继续',\n 'Just a moment',\n 'cf-browser-verification',\n];\n\n// ---------------------------------------------------------------------------\n// Site detection / headers\n// ---------------------------------------------------------------------------\n\n/**\n * 简单 host 匹配:识别微信公众号文章,其他一律 'generic'。\n */\nexport function detectSite(url: string): 'weixin' | 'generic' {\n try {\n const host = new URL(url).hostname.toLowerCase();\n if (host.includes('mp.weixin.qq.com')) return 'weixin';\n } catch {\n /* ignore */\n }\n return 'generic';\n}\n\n/**\n * 按站点构造 fetch headers:微信公众号必须 iPhone UA + Referer 才返回正文。\n */\nexport function buildHeaders(site: string): Record {\n if (site === 'weixin') {\n return {\n 'User-Agent': UA_IPHONE,\n Referer: 'https://mp.weixin.qq.com/',\n 'Accept-Language': 'zh-CN,zh;q=0.9',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n };\n }\n return {\n 'User-Agent': UA_DESKTOP,\n 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n };\n}\n\n/**\n * 粗略判断返回 HTML 是否被反爬拦截。微信特别加了\"无 js_content 节点\"启发式。\n */\nexport function detectAntibot(html: string, site: string): boolean {\n if (ANTIBOT_TRIGGERS.some((t) => html.includes(t))) return true;\n if (site === 'weixin' && !html.includes('js_content')) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// L1 fetch — native Node fetch\n// ---------------------------------------------------------------------------\n\n/**\n * 用 Node 内置 fetch 拉 HTML,超时由 HTTP_TIMEOUT_MS 控制。\n * 失败抛 Error(HTTP 非 2xx 或 abort),由调用方决定是否走 L2 fallback。\n */\nexport async function fetchHtmlL1(url: string, headers: Record): Promise {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);\n try {\n const res = await fetch(url, {\n headers,\n redirect: 'follow',\n signal: controller.signal,\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return await res.text();\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// L2 fetch — optional playwright-core\n// ---------------------------------------------------------------------------\n\n/**\n * playwright-core fallback。可选依赖,缺了或 chromium 没装就返回 null,\n * 由调用方汇报 ANTIBOT_BLOCKED。\n */\nexport async function fetchHtmlL2(url: string): Promise {\n try {\n // Dynamic import — playwright-core is optional\n // @ts-ignore — playwright-core may not be installed\n const pw = await import('playwright-core');\n const browser = await pw.chromium.launch({ headless: true });\n try {\n const page = await browser.newPage();\n await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });\n return await page.content();\n } finally {\n await browser.close();\n }\n } catch {\n // playwright-core not installed or chromium not available\n return null;\n }\n}\n","/**\n * fetcher/images.ts — 图片下载、magic byte 嗅探、markdown 链接重写\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * 依赖关系:仅 import fetcher/http.ts 的 HTTP_TIMEOUT_MS,不依赖 helpers.ts。\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { HTTP_TIMEOUT_MS } from './http.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_IMG_BYTES = 5 * 1024 * 1024;\nconst IMG_CONCURRENCY = 5;\n\n// 常见图片格式的 magic bytes — 优先靠字节签名判断扩展名,\n// 其次才落回 Content-Type(远端 MIME 经常乱报)。\nconst MAGIC: Array<[Uint8Array | number[], string]> = [\n [[0xff, 0xd8, 0xff], '.jpg'],\n [[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], '.png'], // \\x89PNG\\r\\n\\x1a\\n\n [[0x47, 0x49, 0x46, 0x38, 0x37, 0x61], '.gif'], // GIF87a\n [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], '.gif'], // GIF89a\n];\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ImgDownloadResult {\n originalUrl: string;\n localRel: string | null; // relative path like ./images/img_01.jpg\n status: 'ok' | 'failed' | 'too_large';\n}\n\n// ---------------------------------------------------------------------------\n// Magic byte sniffing\n// ---------------------------------------------------------------------------\n\n/**\n * 优先按文件头的 magic bytes 判扩展,不命中再用 Content-Type 作 fallback。\n * 都不命中返回 null(调用方应当丢弃此图)。\n */\nexport function sniffExt(head: Uint8Array, contentType: string): string | null {\n for (const [sig, ext] of MAGIC) {\n if (sig.every((b, i) => head[i] === b)) return ext;\n }\n // RIFF....WEBP\n if (\n head[0] === 0x52 &&\n head[1] === 0x49 &&\n head[2] === 0x46 &&\n head[3] === 0x46 &&\n head[8] === 0x57 &&\n head[9] === 0x45 &&\n head[10] === 0x42 &&\n head[11] === 0x50\n ) {\n return '.webp';\n }\n const ct = contentType.toLowerCase();\n if (ct.includes('image/jpeg') || ct.includes('image/jpg')) return '.jpg';\n if (ct.includes('image/png')) return '.png';\n if (ct.includes('image/gif')) return '.gif';\n if (ct.includes('image/webp')) return '.webp';\n if (ct.includes('image/svg')) return '.svg';\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Image downloading\n// ---------------------------------------------------------------------------\n\n/**\n * 单张图下载:最多 2 次尝试;超过 MAX_IMG_BYTES 标 too_large;\n * 文件名格式 `img_{idx:02d}.{ext}`,相对链接形如 `./.assets/img_01.jpg`。\n */\nexport async function downloadOneImage(\n url: string,\n idx: number,\n imagesDir: string,\n headers: Record,\n assetsRelPath: string,\n): Promise {\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);\n const res = await fetch(url, {\n headers,\n redirect: 'follow',\n signal: controller.signal,\n });\n clearTimeout(timer);\n\n if (!res.ok) continue;\n\n const cl = Number(res.headers.get('content-length') || 0);\n if (cl && cl > MAX_IMG_BYTES) {\n return { originalUrl: url, localRel: null, status: 'too_large' };\n }\n\n const buf = await res.arrayBuffer();\n if (buf.byteLength > MAX_IMG_BYTES) {\n return { originalUrl: url, localRel: null, status: 'too_large' };\n }\n\n const data = new Uint8Array(buf);\n const ext = sniffExt(data.slice(0, 16), res.headers.get('content-type') || '');\n if (!ext) continue;\n\n const fname = `img_${String(idx).padStart(2, '0')}${ext}`;\n await writeFile(join(imagesDir, fname), data);\n // localRel 相对于 .md 文件位置;assetsRelPath 例如 \"./.assets/\"\n return { originalUrl: url, localRel: `${assetsRelPath}${fname}`, status: 'ok' };\n } catch {\n // retry\n }\n }\n return { originalUrl: url, localRel: null, status: 'failed' };\n}\n\n/**\n * 批量下载:按 IMG_CONCURRENCY 分批 Promise.all,避免一次打开太多 socket。\n * 返回结果数组顺序对应 imgSrcs。\n */\nexport async function downloadImages(\n imgSrcs: string[],\n imagesDir: string,\n headers: Record,\n assetsRelPath: string,\n): Promise {\n if (imgSrcs.length === 0) return [];\n await mkdir(imagesDir, { recursive: true });\n\n const results: ImgDownloadResult[] = [];\n // Process in batches of IMG_CONCURRENCY\n for (let i = 0; i < imgSrcs.length; i += IMG_CONCURRENCY) {\n const batch = imgSrcs.slice(i, i + IMG_CONCURRENCY);\n const batchResults = await Promise.all(\n batch.map((src, j) => downloadOneImage(src, i + j + 1, imagesDir, headers, assetsRelPath)),\n );\n results.push(...batchResults);\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Markdown link rewriting\n// ---------------------------------------------------------------------------\n\n/**\n * 用下载结果把 markdown 中的 `![alt](远端 URL)` 重写为 `![alt](本地相对路径)`。\n * 下载失败的图保留原 URL。\n */\nexport function rewriteMarkdownImages(md: string, imgResults: ImgDownloadResult[]): string {\n const urlToLocal = new Map();\n for (const r of imgResults) {\n if (r.status === 'ok' && r.localRel) {\n urlToLocal.set(r.originalUrl, r.localRel);\n }\n }\n // Replace ![alt](url) with ![alt](localRel)\n return md.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt, url) => {\n const local = urlToLocal.get(url);\n return local ? `![${alt}](${local})` : match;\n });\n}\n","/**\n * fetcher/routes/web.ts — generic 网页解析(OpenGraph + meta + article/main/body)\n *\n * 批次 21c strangler fig 第三步:从 src/lib/fetcher.ts copy 出 parseGeneric。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路 parser。21g 才创建 routes/index.ts dispatcher 取代\n * fetchUrl 内的 `site === 'weixin' ? parseWeixin : parseGeneric` 三元逻辑。\n *\n * 不含微信特定逻辑(见 21d 抽 routes/weixin.ts)。\n * 不含 frontmatter 拼装(见 21b 的 frontmatter.ts,本文件仅做 HTML → ParsedDoc)。\n */\nimport * as cheerio from 'cheerio';\n\nimport { normalizeDateText, resolveUrl } from '../helpers.js';\nimport type { ParsedDoc } from '../types.js';\n\n// ParsedDoc 21g-pre 上提到 fetcher/types.ts,本文件 21c 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// parseGeneric\n// ---------------------------------------------------------------------------\n\n/**\n * generic 网页 HTML → ParsedDoc。\n *\n * 策略:\n * - title: og:title > \n * - author: meta[name=author]\n * - publishDate: 一组常见 meta / `<time datetime>` / `<time>` 文本,按优先级第一个能解析的胜出\n * - body: <article> > <main> > <body>(找不到则返回空 bodyHtml)\n * - 图片:data-src > data-original > src,相对路径转绝对,丢弃 data:URL\n */\nexport function parseGeneric(html: string, baseUrl: string): ParsedDoc {\n const $ = cheerio.load(html);\n\n // Title\n const ogTitle = $('meta[property=\"og:title\"]').attr('content')?.trim();\n const titleTag = $('title').text().trim();\n const title = ogTitle || titleTag || '';\n\n // Author\n const author = $('meta[name=\"author\"]').attr('content')?.trim() || '';\n\n // Publish date — common places: OpenGraph article, meta, <time datetime>, JSON-LD\n let publishDate: string | undefined;\n const dateCandidates: Array<string | undefined> = [\n $('meta[property=\"article:published_time\"]').attr('content'),\n $('meta[property=\"og:article:published_time\"]').attr('content'),\n $('meta[name=\"article:published_time\"]').attr('content'),\n $('meta[itemprop=\"datePublished\"]').attr('content'),\n $('meta[name=\"date\"]').attr('content'),\n $('meta[name=\"pubdate\"]').attr('content'),\n $('meta[name=\"publishdate\"]').attr('content'),\n $('time[datetime]').first().attr('datetime'),\n $('time').first().text(),\n ];\n for (const cand of dateCandidates) {\n if (!cand) continue;\n const norm = normalizeDateText(cand);\n if (norm) {\n publishDate = norm;\n break;\n }\n }\n\n // Body: article > main > body\n let body = $('article');\n if (!body.length) body = $('main');\n if (!body.length) body = $('body');\n if (!body.length) {\n return { title, author, publishDate, bodyHtml: '', imgSrcs: [] };\n }\n\n // Clean junk\n body.find('script, style, nav, footer, header, aside').remove();\n\n // Normalize images\n const imgSrcs: string[] = [];\n body.find('img').each((_i, el) => {\n const $el = $(el);\n const real = (\n $el.attr('data-src') ||\n $el.attr('data-original') ||\n $el.attr('src') ||\n ''\n ).trim();\n if (!real || real.startsWith('data:')) {\n $el.remove();\n return;\n }\n const abs = resolveUrl(real, baseUrl);\n $el.attr('src', abs);\n imgSrcs.push(abs);\n });\n\n return { title, author, publishDate, bodyHtml: body.html() || '', imgSrcs };\n}\n","/**\n * fetcher/routes/weixin.ts — 微信公众号文章解析\n *\n * 批次 21d strangler fig 第四步:从 src/lib/fetcher.ts copy 出 parseWeixin。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路 parser。21g 才创建 routes/index.ts dispatcher 取代\n * fetchUrl 内的 `site === 'weixin' ? parseWeixin : parseGeneric` 三元逻辑。\n *\n * ## 与原 parseWeixin 的差异(LEGACY P4-4 顺手修,规划方批准)\n *\n * 旧版只识别 `<img>` 标签的 lazy attrs(data-src / data-original / data-url)。\n * 部分微信文章用 `<picture><source srcset=\"...\"><img ...></picture>` 或仅 `<picture>\n * <source srcset=\"...\"></picture>`(无 img)的写法,旧版会丢图。\n *\n * 新版在跑 img 流程前先扫一遍 `<picture>` 节点:\n * - 取内部第一个 `<source>` 的 `srcset`,parse 第一个 URL(srcset 语法:`url [w|x], url2 [w|x], ...`,用 `,` 分隔候选 + 空白分隔 url 与 descriptor)\n * - 若 picture 内有 `<img>` 且其 `src/data-src/data-original/data-url` 都为空,把 srcset url 写入 `data-src`(让后续 img 流程统一处理)\n * - 若 picture 内无 `<img>` 且 srcset 有效,append 一个 `<img data-src=\"...\">`\n * - 用 picture 的 first `<img>` 节点替换 picture 整体(unwrap),删掉 picture 与所有 `<source>` 子节点,避免 turndown 输出残留\n * - 兜底:扫整个 body 内残留的 `<source>` 节点(picture 之外野生的,极少见)一并 remove\n *\n * 这样后续 `body.find('img').each(...)` 流程不变,imgSrcs 自然累加。\n */\nimport * as cheerio from 'cheerio';\n\nimport { normalizeDateText, resolveUrl, tsToYMD } from '../helpers.js';\nimport type { ParsedDoc } from '../types.js';\n\n// ParsedDoc 21g-pre 上提到 fetcher/types.ts,本文件 21d 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// P4-4 helper: srcset → 第一个 URL\n// ---------------------------------------------------------------------------\n\n/**\n * srcset 形如:`a.jpg 320w, b.jpg 640w` 或 `a.jpg, b.jpg 2x`。\n * 取第一个候选(按 `,` 分),再取候选里第一个 whitespace token(去掉 `Nw` / `Nx` descriptor)。\n * 空 / 非法返回空串。\"最高质量 URL\"判断需要 parse w/x 比较,本批次只取第一个保持最简实现。\n */\nfunction firstSrcsetUrl(srcset: string): string {\n const s = srcset.trim();\n if (!s) return '';\n const firstCandidate = s.split(',')[0].trim();\n if (!firstCandidate) return '';\n const url = firstCandidate.split(/\\s+/)[0].trim();\n return url;\n}\n\n// ---------------------------------------------------------------------------\n// parseWeixin\n// ---------------------------------------------------------------------------\n\nexport function parseWeixin(html: string, baseUrl: string): ParsedDoc {\n const $ = cheerio.load(html);\n\n // Title\n let title =\n $('h1#activity-name').text().trim() ||\n $('h1.rich_media_title').text().trim() ||\n $('meta[property=\"og:title\"]').attr('content')?.trim() ||\n '';\n\n // Author\n const author = $('a#js_name').text().trim() || $('#js_author_name').text().trim() || '';\n\n // Publish date — prefer `var ct = \"<unix seconds>\"` (most reliable),\n // fallback to <em id=\"publish_time\"> text node.\n let publishDate: string | undefined;\n const ctMatch = html.match(/var\\s+ct\\s*=\\s*\"(\\d+)\"/);\n if (ctMatch) {\n const ts = Number(ctMatch[1]);\n if (Number.isFinite(ts) && ts > 0) publishDate = tsToYMD(ts);\n }\n if (!publishDate) {\n const ptText = $('em#publish_time').text().trim();\n if (ptText) publishDate = normalizeDateText(ptText);\n }\n\n // Body\n const body = $('#js_content');\n if (!body.length) {\n return { title, author, publishDate, bodyHtml: '', imgSrcs: [] };\n }\n\n // Clean\n body.find('script, style').remove();\n\n // ---------------------------------------------------------------------------\n // P4-4: 把 <picture><source srcset> 展开成 <img>,再走原有 img 流程\n // ---------------------------------------------------------------------------\n body.find('picture').each((_i, el) => {\n const $picture = $(el);\n // 取第一个 source 的 srcset\n const $firstSource = $picture.find('source[srcset]').first();\n const srcsetRaw = $firstSource.attr('srcset') || '';\n const pickedUrl = firstSrcsetUrl(srcsetRaw);\n\n let $img = $picture.find('img').first();\n\n if ($img.length) {\n // 有 img:若 src / data-* 都空才用 srcset 兜底,避免覆盖原有更明确的来源\n const existing = (\n $img.attr('data-src') ||\n $img.attr('data-original') ||\n $img.attr('data-url') ||\n $img.attr('src') ||\n ''\n ).trim();\n if (!existing && pickedUrl) {\n $img.attr('data-src', pickedUrl);\n }\n } else if (pickedUrl) {\n // 无 img:用 srcset url 新建一个,后续 img 流程统一处理\n $picture.append(`<img data-src=\"${pickedUrl}\">`);\n $img = $picture.find('img').first();\n }\n\n // unwrap:用 img 替换 picture 整体;若没拿到 img(srcset 也空),picture 整块移除\n if ($img.length) {\n $picture.replaceWith($img);\n } else {\n $picture.remove();\n }\n });\n\n // 兜底:清掉野生 <source> 节点(picture 之外极少见,但为 turndown 干净起见统一删)\n body.find('source').remove();\n\n // Normalize images: data-src / data-original → src\n const imgSrcs: string[] = [];\n body.find('img').each((_i, el) => {\n const $el = $(el);\n const real = (\n $el.attr('data-src') ||\n $el.attr('data-original') ||\n $el.attr('data-url') ||\n $el.attr('src') ||\n ''\n ).trim();\n if (!real || real.startsWith('data:')) {\n $el.remove();\n return;\n }\n const abs = resolveUrl(real, baseUrl);\n $el.attr('src', abs);\n // Remove noisy attrs\n for (const a of [\n 'data-src',\n 'data-original',\n 'data-url',\n 'data-w',\n 'data-ratio',\n 'data-type',\n 'data-s',\n 'srcset',\n ]) {\n $el.removeAttr(a);\n }\n imgSrcs.push(abs);\n });\n\n return { title, author, publishDate, bodyHtml: body.html() || '', imgSrcs };\n}\n","/**\n * fetcher/routes/gist.ts — GitHub Gist 抓取\n *\n * 批次 21e strangler fig 第五步:从 src/lib/fetcher.ts copy 出 parseGistUrl + fetchGist。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路。21g 才切换 dispatcher 并删旧。\n *\n * **本批首次集成 21b 的 buildFrontmatter()**:旧 fetchGist 内嵌的 fmLines 拼装段\n * 替换为 `lines.push(...buildFrontmatter({routeKind: 'gist', ...}))`,验证 21b 抽出的\n * helper 设计可用。21b 已用 6 mock case 证明 byte-level 等价;本文件因此 frontmatter\n * 段无需重复验证,整份文件 buffer 等价由 21e 自有 mock 兜底。\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport * as cheerio from 'cheerio';\n\nimport { buildFrontmatter } from '../frontmatter.js';\nimport { normalizeDateText, slugify, todayYMD } from '../helpers.js';\nimport { buildHeaders, fetchHtmlL1 } from '../http.js';\nimport type { FetchResult } from '../types.js';\n\n// FetchResult 21g-pre 上提到 fetcher/types.ts,本文件 21e 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// parseGistUrl\n// ---------------------------------------------------------------------------\n\n/**\n * 校验并解析 gist URL。仅接受 `gist.github.com` / `gist.githubusercontent.com`,\n * 路径需含 `/<user>/<id>` 至少两段。其余返回 null。\n */\nexport function parseGistUrl(url: string): { user: string; id: string } | null {\n try {\n const u = new URL(url);\n if (\n !u.hostname.endsWith('gist.github.com') &&\n !u.hostname.endsWith('gist.githubusercontent.com')\n ) {\n return null;\n }\n const parts = u.pathname.split('/').filter(Boolean);\n if (parts.length < 2) return null;\n return { user: parts[0], id: parts[1] };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// fetchGist 主流程\n// ---------------------------------------------------------------------------\n\n/**\n * gist 抓取流程:\n * 1. 校验 URL,解析 user / id\n * 2. fetch gist 主页 HTML(L1 only,gist 页面不需要 playwright)\n * 3. 用 cheerio 抽 title / author / publishDate\n * 4. 扫所有 `<a href>`,匹配 `/<user>/<id>/raw/<sha>/<filename>` 模式抽 raw 链接\n * 5. 优先 `.md` / `.markdown`,否则取第一个;二次 fetch 拿正文\n * 6. 拼 frontmatter(用 21b buildFrontmatter)+ 可选 H1 + 正文,写到 outRoot/<slug>.md\n *\n * 失败路径返回 `{status:'error', reason:...}`,由调用方决定是否兜底。\n */\nexport async function fetchGist(url: string, outRoot: string): Promise<FetchResult> {\n const parsed = parseGistUrl(url);\n if (!parsed) {\n return { status: 'error', route: 'gist', url, reason: 'invalid_gist_url' };\n }\n\n const headers = buildHeaders('generic');\n let html: string;\n try {\n html = await fetchHtmlL1(url, headers);\n } catch (e) {\n return {\n status: 'error',\n route: 'gist',\n url,\n reason: `fetch_failed: ${(e as Error).message}`,\n };\n }\n\n const $ = cheerio.load(html);\n\n // gist 页面把描述放在 .gist-header [itemprop=\"about\"],OpenGraph title 通常是第一个文件名\n const description = $('[itemprop=\"about\"]').first().text().trim();\n const ogTitle = $('meta[property=\"og:title\"]').attr('content')?.trim();\n const title = description || ogTitle || parsed.id;\n\n const author = parsed.user;\n\n // 日期:<relative-time datetime=\"ISO\"> 是 GitHub 标准元素\n let publishDate: string | undefined;\n const dateRaw =\n $('relative-time').first().attr('datetime') ||\n $('time-ago').first().attr('datetime') ||\n $('meta[property=\"article:published_time\"]').attr('content') ||\n '';\n if (dateRaw) publishDate = normalizeDateText(dateRaw);\n\n // 抽 raw 链接。gist 页面的 raw 链接形如:\n // /karpathy/442a6b.../raw/ac46de.../llm-wiki.md\n const rawRe = /^\\/([^/]+)\\/([a-f0-9]{20,})\\/raw\\/([a-f0-9]{20,})\\/(.+)$/i;\n const rawLinks: Array<{ name: string; rawUrl: string }> = [];\n $('a').each((_i, el) => {\n const href = $(el).attr('href') || '';\n const m = href.match(rawRe);\n if (m) {\n rawLinks.push({\n name: m[4],\n rawUrl: 'https://gist.githubusercontent.com' + href,\n });\n }\n });\n\n if (rawLinks.length === 0) {\n return { status: 'error', route: 'gist', url, reason: 'no_raw_files_found' };\n }\n\n // 优先 markdown,其次第一个\n const mdLink = rawLinks.find((l) => /\\.(md|markdown)$/i.test(l.name)) || rawLinks[0];\n\n let content: string;\n try {\n const res = await fetch(mdLink.rawUrl, { headers, redirect: 'follow' });\n if (!res.ok) throw new Error(`HTTP ${res.status} on ${mdLink.rawUrl}`);\n content = await res.text();\n } catch (e) {\n const err = e as Error & { cause?: Error };\n const cause = err.cause?.message ? ` (${err.cause.message})` : '';\n return {\n status: 'error',\n route: 'gist',\n url,\n reason: `raw_fetch_failed: ${err.message}${cause} [raw_url=${mdLink.rawUrl}]`,\n };\n }\n\n const slug = slugify(title);\n await mkdir(outRoot, { recursive: true });\n\n const today = todayYMD();\n const hasH1 = /^#\\s+/m.test(content);\n // 21b buildFrontmatter 返回的行数组与原 fetcher.ts:709-718 内嵌 fmLines 块\n // byte-level 等价(21b 已 6 mock case 证明),spread 进 fmLines 后续操作不变\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: 'gist',\n title,\n today,\n url,\n author,\n publishDate,\n }),\n );\n fmLines.push('');\n if (!hasH1) fmLines.push(`# ${title}`, '');\n fmLines.push(content.trim(), '');\n\n const articlePath = join(outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'gist',\n url,\n title,\n author,\n publishDate,\n sourceKind: 'gist',\n sourceLayer: 'L1',\n slug,\n markdown: articlePath,\n imagesOk: 0,\n imagesFailed: 0,\n };\n}\n","/**\n * fetcher/routes/github.ts — GitHub repo / blob 抓取(README.md 或具体文件)\n *\n * 批次 21f strangler fig 第六步:从 src/lib/fetcher.ts copy 出 parseGithubRepoUrl\n * + fetchGithubDoc。原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;\n * 本文件目前未被任何调用方 import,仅作旁路。21g 才切换 dispatcher 并删旧。\n *\n * 集成 21b 的 buildFrontmatter,routeKind='github':\n * - 21b 已验证 github routeKind 强制忽略 publishDate(即使传入也不输出)\n * - 原 fetchGithubDoc 本来就不抽 publishDate,调用时不传该字段,行为等价\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { buildFrontmatter } from '../frontmatter.js';\nimport { slugify, todayYMD } from '../helpers.js';\nimport { buildHeaders } from '../http.js';\nimport type { FetchResult } from '../types.js';\n\n// FetchResult 21g-pre 上提到 fetcher/types.ts,本文件 21f 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\ninterface GithubRepoRef {\n owner: string;\n repo: string;\n ref: string; // HEAD / branch / commit\n subpath?: string; // 具体文件路径,如 \"docs/foo.md\"\n}\n\n// ---------------------------------------------------------------------------\n// parseGithubRepoUrl\n// ---------------------------------------------------------------------------\n\n/**\n * 校验并解析 github URL。仅接受 `github.com` / `www.github.com`,\n * 路径需含 `/<owner>/<repo>` 至少两段。支持 `/blob/<ref>/<subpath>` 与 `/tree/<ref>`。\n * 其余返回 null。\n */\nexport function parseGithubRepoUrl(url: string): GithubRepoRef | null {\n try {\n const u = new URL(url);\n if (u.hostname !== 'github.com' && u.hostname !== 'www.github.com') return null;\n const parts = u.pathname.split('/').filter(Boolean);\n if (parts.length < 2) return null;\n const [owner, rawRepo, ...rest] = parts;\n const repo = rawRepo.replace(/\\.git$/, '');\n\n if (rest.length === 0) {\n return { owner, repo, ref: 'HEAD' };\n }\n if (rest[0] === 'blob' && rest.length >= 3) {\n return { owner, repo, ref: rest[1], subpath: rest.slice(2).join('/') };\n }\n if (rest[0] === 'tree' && rest.length >= 2) {\n return { owner, repo, ref: rest[1] };\n }\n return { owner, repo, ref: 'HEAD' };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// fetchGithubDoc 主流程\n// ---------------------------------------------------------------------------\n\n/**\n * github 抓取流程:\n * 1. 校验 URL\n * 2. 子路径模式:直接拼 raw URL;仓库根模式:循环试 5 个常见 README 文件名\n * 3. 第一个 HTTP 200 + 正文 > 20 字符 的胜出\n * 4. 拼 frontmatter(用 21b buildFrontmatter)+ 可选 H1 + `> Fetched from:` 注解 + 正文\n * 5. 写到 outRoot/<slug>.md\n *\n * 失败路径返回 `{status:'error', reason:...}`,由调用方决定是否兜底。\n */\nexport async function fetchGithubDoc(url: string, outRoot: string): Promise<FetchResult> {\n const parsed = parseGithubRepoUrl(url);\n if (!parsed) {\n return { status: 'error', route: 'github', url, reason: 'invalid_github_url' };\n }\n\n const { owner, repo, ref, subpath } = parsed;\n const headers = buildHeaders('generic');\n\n // 确定候选 raw URL 列表\n const candidates: string[] = [];\n if (subpath) {\n candidates.push(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${subpath}`);\n } else {\n // 仓库根:尝试常见 README 文件名\n for (const name of ['README.md', 'README.MD', 'Readme.md', 'readme.md', 'README']) {\n candidates.push(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${name}`);\n }\n }\n\n let content = '';\n let chosenUrl = '';\n for (const candUrl of candidates) {\n try {\n const res = await fetch(candUrl, { headers });\n if (!res.ok) continue;\n const text = await res.text();\n if (text && text.trim().length > 20) {\n content = text;\n chosenUrl = candUrl;\n break;\n }\n } catch {\n // try next\n }\n }\n\n if (!content) {\n return { status: 'error', route: 'github', url, reason: 'no_readable_content_found' };\n }\n\n const fileName = subpath ? subpath.split('/').pop()! : 'README.md';\n const title = subpath ? fileName.replace(/\\.(md|markdown)$/i, '') : `${owner}/${repo}`;\n\n const slug = slugify(subpath ? `${owner}-${repo}-${fileName}` : `${owner}-${repo}`);\n await mkdir(outRoot, { recursive: true });\n\n const today = todayYMD();\n const hasH1 = /^#\\s+/m.test(content);\n // 21b buildFrontmatter routeKind='github' 强制忽略 publishDate(21b parity case 6 已验证),\n // 不传该字段即可。返回行数组与原 fetcher.ts:826-834 内嵌 fmLines 块 byte-level 等价。\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: 'github',\n title,\n today,\n url,\n author: owner,\n }),\n );\n fmLines.push('');\n if (!hasH1) fmLines.push(`# ${title}`, '');\n fmLines.push(`> Fetched from: ${chosenUrl}`, '');\n fmLines.push(content.trim(), '');\n\n const articlePath = join(outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'github',\n url,\n title,\n author: owner,\n sourceKind: 'github',\n sourceLayer: 'L1',\n slug,\n markdown: articlePath,\n imagesOk: 0,\n imagesFailed: 0,\n };\n}\n","/**\n * ingest-state.ts — Persistent ingest pipeline state.\n *\n * Lives at `<corpus>/.wiki/ingest-state.json`. The authoritative record of\n * \"what URL was ingested, at what step, and did we finish it.\" Used by:\n * - `lorekit fetch` : dedupe + resume detection\n * - `lorekit ingest *` : step tracking from the skill/agent\n * - `lorekit ingest-check` : surface in-flight / failed ingests\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\n\n/**\n * Three coarse states the user actually cares about:\n * started — URL has entered the pipeline, not finished yet.\n * (which sub-step it's at is recorded in `stepsDone` below.)\n * completed — archived + wiki + lint all done.\n * failed — explicit abort with a reason.\n *\n * Finer sub-steps live in `stepsDone[]` so resuming an interrupted ingest\n * can skip already-done work, but the top-level symbol stays one of three.\n */\nexport type IngestStatus = 'started' | 'completed' | 'failed';\n\nexport type IngestStep = 'fetch' | 'archive' | 'wiki' | 'backlink' | 'lint';\n\nexport interface IngestRecord {\n url: string;\n title?: string;\n sourceDate?: string; // YYYY-MM-DD\n startedAt: string; // ISO timestamp\n updatedAt: string; // ISO timestamp\n status: IngestStatus;\n stepsDone: IngestStep[];\n workbenchMd?: string; // absolute path to <slug>.md while status=fetched\n // 老字段,兼容 0.3.x 之前的 state.json(产物是 <slug>/article.md 嵌套结构)\n workbenchDir?: string;\n archivedTo?: string; // relative-to-corpus path (e.g. 原料/剪藏/xxx)\n wikiPages?: string[]; // relative-to-corpus paths\n error?: string;\n}\n\nexport interface IngestStateFile {\n version: 1;\n ingests: Record<string, IngestRecord>;\n}\n\nfunction stateFilePath(corpus: string): string {\n return join(corpus, '.wiki', 'ingest-state.json');\n}\n\nexport function loadIngestState(corpus: string): IngestStateFile {\n const p = stateFilePath(corpus);\n if (!existsSync(p)) {\n return { version: 1, ingests: {} };\n }\n try {\n const raw = readFileSync(p, 'utf-8');\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') {\n return { version: 1, ingests: {} };\n }\n if (!parsed.ingests || typeof parsed.ingests !== 'object') {\n parsed.ingests = {};\n }\n parsed.version = 1;\n return parsed as IngestStateFile;\n } catch {\n return { version: 1, ingests: {} };\n }\n}\n\nexport function saveIngestState(corpus: string, state: IngestStateFile): void {\n const p = stateFilePath(corpus);\n mkdirSync(dirname(p), { recursive: true });\n const serialized = JSON.stringify(state, null, 2);\n writeFileSync(p, serialized + '\\n', 'utf-8');\n}\n\nexport function getIngestRecord(corpus: string, url: string): IngestRecord | undefined {\n return loadIngestState(corpus).ingests[url];\n}\n\nexport function upsertIngestRecord(\n corpus: string,\n url: string,\n patch: Partial<IngestRecord>,\n): IngestRecord {\n const state = loadIngestState(corpus);\n const now = new Date().toISOString();\n const existing = state.ingests[url];\n const merged: IngestRecord = existing\n ? { ...existing, ...patch, url, updatedAt: now }\n : {\n url,\n startedAt: now,\n updatedAt: now,\n status: (patch.status as IngestStatus) ?? 'started',\n stepsDone: patch.stepsDone ?? [],\n ...patch,\n };\n // Dedup stepsDone\n if (merged.stepsDone) {\n merged.stepsDone = Array.from(new Set(merged.stepsDone));\n }\n state.ingests[url] = merged;\n saveIngestState(corpus, state);\n return merged;\n}\n\nexport function deleteIngestRecord(corpus: string, url: string): boolean {\n const state = loadIngestState(corpus);\n if (!(url in state.ingests)) return false;\n delete state.ingests[url];\n saveIngestState(corpus, state);\n return true;\n}\n\nexport function listPendingIngests(corpus: string): IngestRecord[] {\n const state = loadIngestState(corpus);\n return Object.values(state.ingests).filter((r) => r.status !== 'completed');\n}\n\n/**\n * Suggest the next step for a resumed ingest.\n * Derived from stepsDone so the caller doesn't have to know the step order.\n */\nexport function nextStepHint(record: IngestRecord): string {\n if (record.status === 'completed') return 'nothing to do';\n if (record.status === 'failed') {\n return `failed: ${record.error ?? 'unknown error'} — inspect and re-run with --force if you want to retry`;\n }\n const done = new Set(record.stepsDone);\n if (!done.has('fetch')) {\n return 'fetch: nothing recorded yet — run `lorekit fetch <url>`';\n }\n if (!done.has('archive')) {\n return 'archive: mv the workbench dir into 原料/(剪藏|文章|书籍|...)';\n }\n if (!done.has('wiki')) {\n return 'wiki: compile wiki pages in 知识库/(概念|实体|摘要|专题)';\n }\n if (!done.has('lint')) {\n return 'lint: run `lorekit ingest-check`, fix any issues, then `lorekit ingest record <url> --complete`';\n }\n return 'all steps done but status not yet completed — run `lorekit ingest record <url> --complete`';\n}\n","/**\n * `lorekit ingest` — subcommands for marking ingest pipeline progress.\n *\n * The agent running wiki-ingest calls these as it advances through the\n * Decision tree; each call mutates .wiki/ingest-state.json. This makes\n * interrupted ingests resumable and provides the source of truth for\n * `lorekit ingest-check`.\n */\nimport type { Command } from 'commander';\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport {\n loadIngestState,\n saveIngestState,\n upsertIngestRecord,\n deleteIngestRecord,\n listPendingIngests,\n nextStepHint,\n type IngestRecord,\n type IngestStep,\n type IngestStatus,\n} from '../lib/ingest-state.js';\nimport { dateToYMDLocal } from '../lib/date.js';\nimport { out, print } from '../utils/logger.js';\n\nconst VALID_STEPS: IngestStep[] = ['fetch', 'archive', 'wiki', 'backlink', 'lint'];\n\n// Today as YYYY-MM-DD in local time (Asia/Shanghai assumed by user setup).\nfunction today(): string {\n return dateToYMDLocal(new Date());\n}\n\n/**\n * Append a structured ingest entry to corpus/log.md.\n *\n * The log.md format (see existing entries) uses `## [YYYY-MM-DD] ingest | 标题`\n * as a section header followed by structured bullet lines. We prepend the new\n * entry just below the file's intro block (after the first `>` blockquote line)\n * so the most recent ingest sits on top — matching the convention of existing\n * entries.\n *\n * `body` is the LLM-supplied one-paragraph summary describing what was done.\n * The CLI auto-fills url / archive / wiki pages from the state record so the\n * skill doesn't have to repeat them.\n */\nfunction appendLogEntry(corpus: string, record: IngestRecord, body: string): void {\n const logPath = join(corpus, 'log.md');\n const title = record.title ?? '(untitled)';\n const wikiList = (record.wikiPages ?? []).map((p) => ` - ${p}`).join('\\n');\n const archived = record.archivedTo ?? '(unrecorded)';\n\n const entry = [\n `## [${today()}] ingest | ${title}`,\n '',\n body.trim(),\n '',\n `- **URL**:${record.url}`,\n `- **归档**:${archived}`,\n record.wikiPages && record.wikiPages.length > 0\n ? `- **新建/更新页**:\\n${wikiList}`\n : '- **新建/更新页**:(无)',\n '',\n '',\n ].join('\\n');\n\n let existing = '';\n if (existsSync(logPath)) existing = readFileSync(logPath, 'utf-8');\n\n if (!existing) {\n // Bootstrap a minimal log.md with header + first entry.\n const header =\n '# Log\\n\\n> 操作时间线,append-only。每条格式:`## [YYYY-MM-DD] 操作类型 | 标题`\\n> 可用 `grep \"^## \\\\[\" log.md | tail -10` 快速查最近操作。\\n\\n';\n writeFileSync(logPath, header + entry, 'utf-8');\n return;\n }\n\n // Insert new entry between intro block and first existing `## [` section.\n const firstSection = existing.search(/^## \\[/m);\n if (firstSection === -1) {\n // No existing sections — append at end.\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n writeFileSync(logPath, existing + sep + entry, 'utf-8');\n } else {\n const before = existing.slice(0, firstSection);\n const after = existing.slice(firstSection);\n writeFileSync(logPath, before + entry + after, 'utf-8');\n }\n}\n\nexport function ingestCommand(program: Command): void {\n const group = program\n .command('ingest')\n .description('Track ingest pipeline state (record step progress, list pending, reconcile)');\n\n // ---------- list ----------\n group\n .command('list')\n .description('List every ingest record (completed + in-progress)')\n .action(() => {\n const corpus = requireCorpus();\n const state = loadIngestState(corpus);\n const rows = Object.values(state.ingests);\n if (rows.length === 0) {\n print('[lorekit ingest list] no records');\n out(JSON.stringify({ ingests: [] }));\n return;\n }\n const summary = rows.map((r) => {\n const done = r.stepsDone.join(',') || '(none)';\n const dest = r.archivedTo ?? r.workbenchMd ?? r.workbenchDir ?? '-';\n return ` [${r.status.padEnd(12)}] ${r.url}\\n steps: ${done} → ${dest}`;\n });\n print(`[lorekit ingest list] ${rows.length} record(s)\\n${summary.join('\\n')}`);\n out(JSON.stringify(state));\n });\n\n // ---------- pending ----------\n group\n .command('pending')\n .description('List only in-progress (non-completed) ingests — what you need to resume')\n .action(() => {\n const corpus = requireCorpus();\n const pending = listPendingIngests(corpus);\n if (pending.length === 0) {\n print('[lorekit ingest pending] all ingests are completed — nothing to resume');\n out(JSON.stringify({ pending: [] }));\n return;\n }\n const summary = pending.map((r) => {\n return ` [${r.status.padEnd(12)}] ${r.url}\\n next step → ${nextStepHint(r)}`;\n });\n print(\n `[lorekit ingest pending] ${pending.length} ingest(s) need attention\\n${summary.join('\\n')}`,\n );\n out(JSON.stringify({ pending }));\n process.exitCode = 1;\n });\n\n // ---------- record ----------\n group\n .command('record <url>')\n .description('Record step progress for an ingest (call from wiki-ingest skill)')\n .option(\n '--step <steps>',\n `mark step(s) as done. single: archive | multi: archive,wiki,backlink,lint. valid: ${VALID_STEPS.join(', ')}`,\n )\n .option('--archived-to <path>', 'relative path where the source was moved (e.g. 原料/剪藏/xxx)')\n .option('--wiki-page <path...>', 'relative path of a wiki page created (can be repeated)')\n .option(\n '--log <body>',\n 'append a one-paragraph summary to corpus/log.md (CLI auto-fills url/archive/pages)',\n )\n .option('--status <status>', 'explicit status (started|completed|failed)')\n .option('--complete', 'shortcut: mark status=completed')\n .option('--fail <reason>', 'shortcut: mark status=failed with reason')\n .action(\n (\n url: string,\n opts: {\n step?: string;\n archivedTo?: string;\n wikiPage?: string[];\n log?: string;\n status?: string;\n complete?: boolean;\n fail?: string;\n },\n ) => {\n const corpus = requireCorpus();\n const patch: Parameters<typeof upsertIngestRecord>[2] = {};\n\n // --step accepts either a single step (\"archive\") or a comma-separated\n // chain (\"archive,wiki,backlink,lint\") so the skill can close the books\n // in one call instead of four CLI invocations.\n let parsedSteps: IngestStep[] = [];\n if (opts.step) {\n parsedSteps = opts.step\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean) as IngestStep[];\n\n for (const s of parsedSteps) {\n if (!VALID_STEPS.includes(s)) {\n print(\n `[lorekit ingest record] invalid step: ${s}. valid: ${VALID_STEPS.join(', ')}`,\n );\n process.exitCode = 2;\n return;\n }\n }\n\n const existing = loadIngestState(corpus).ingests[url];\n const prev = existing?.stepsDone ?? [];\n // 去重:保留首次出现顺序。多次 record 链式调用追加同一 step 时不应重复,\n // 否则 ingest-state.json 的 stepsDone 会出现 `[archive, archive, wiki, wiki, backlink]`\n // 这种噪声,nextStepHint / pending 推断也会受影响。\n // 见批次 20b / P4-1 同模式(与 wikiPages 同步修)。\n patch.stepsDone = [...new Set([...prev, ...parsedSteps])];\n\n // status precedence: explicit flags win; otherwise, if the chain\n // includes 'lint' it implies completion.\n if (!opts.status && !opts.complete && !opts.fail) {\n if (parsedSteps.includes('lint')) patch.status = 'completed';\n else patch.status = 'started';\n }\n }\n if (opts.archivedTo) patch.archivedTo = opts.archivedTo;\n if (opts.wikiPage && opts.wikiPage.length > 0) {\n const existing = loadIngestState(corpus).ingests[url];\n const prev = existing?.wikiPages ?? [];\n // 去重:保留首次出现顺序。多次 record 调用追加同一 wiki 页面时不应重复,\n // 否则 log.md 的 `- **新建/更新页**` 块会出现重复条目。\n // 见 LEGACY P4-1 / 批次 20 的复现 smoke。\n patch.wikiPages = [...new Set([...prev, ...opts.wikiPage])];\n }\n if (opts.status) {\n const validStatuses: readonly IngestStatus[] = ['started', 'completed', 'failed'];\n if (!validStatuses.includes(opts.status as IngestStatus)) {\n print(\n `[lorekit ingest record] invalid --status: ${opts.status}. valid: ${validStatuses.join(', ')}`,\n );\n process.exitCode = 2;\n return;\n }\n patch.status = opts.status as IngestStatus;\n }\n if (opts.complete) patch.status = 'completed';\n if (opts.fail) {\n patch.status = 'failed';\n patch.error = opts.fail;\n }\n\n const updated = upsertIngestRecord(corpus, url, patch);\n\n // --log: append entry to corpus/log.md AFTER state is updated so the\n // entry can include the freshly-recorded archive path / wiki pages.\n let logAppended = false;\n if (opts.log) {\n try {\n appendLogEntry(corpus, updated, opts.log);\n logAppended = true;\n } catch (e) {\n print(`[lorekit ingest record] log append failed: ${(e as Error).message}`);\n }\n }\n\n print(\n `[lorekit ingest record] ${url}\\n` +\n ` status: ${updated.status} steps: ${updated.stepsDone.join(',') || '(none)'}` +\n (logAppended ? ' +log' : ''),\n );\n out(JSON.stringify({ ...updated, logAppended }));\n },\n );\n\n // ---------- check ----------\n // Pre-flight broken-link check for one or more wiki pages. Used by the\n // wiki-ingest skill RIGHT AFTER writing new pages, before recording the\n // backlink step. Catches `[[xxx]]` whose target page doesn't exist so the\n // skill can either create the target stub or downgrade `[[xxx]]` to plain\n // text — instead of leaving the corpus in a state lint will flag later.\n group\n .command('check <files...>')\n .description('Scan given wiki pages for broken [[wikilinks]] (pre-commit check)')\n .action((files: string[]) => {\n const corpus = requireCorpus();\n\n // Build the same lookup sets lint.ts uses (stems + bare names + folder\n // packages like 原料/剪藏/xxx/article.md → 原料/剪藏/xxx).\n const allMd = collectMdFiles(corpus);\n const stemSet = new Set<string>();\n const baseNameSet = new Set<string>();\n for (const file of allMd) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n stemSet.add(stem);\n baseNameSet.add(stem.split('/').pop()!);\n if (stem.endsWith('/article')) {\n const folder = stem.replace(/\\/article$/, '');\n stemSet.add(folder);\n baseNameSet.add(folder.split('/').pop()!);\n }\n }\n\n const stripCode = (s: string) => s.replace(/```[\\s\\S]*?```/g, '').replace(/`[^`\\n]+`/g, '');\n\n const broken: { file: string; link: string }[] = [];\n const okLinks: { file: string; link: string }[] = [];\n const checked: string[] = [];\n\n for (const f of files) {\n const abs = f.startsWith('/') ? f : join(process.cwd(), f);\n if (!existsSync(abs)) {\n print(`[lorekit ingest check] file not found: ${f}`);\n process.exitCode = 2;\n continue;\n }\n const rel = relative(corpus, abs);\n checked.push(rel);\n\n let content: string;\n try {\n content = stripCode(readFileSync(abs, 'utf-8'));\n } catch {\n continue;\n }\n\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n const seen = new Set<string>();\n while ((m = linkRe.exec(content)) !== null) {\n const target = m[1].trim();\n if (seen.has(target)) continue;\n seen.add(target);\n if (stemSet.has(target) || baseNameSet.has(target)) {\n okLinks.push({ file: rel, link: target });\n } else {\n broken.push({ file: rel, link: target });\n }\n }\n }\n\n const result = { checked, ok: okLinks, broken };\n\n if (broken.length === 0) {\n print(\n `[lorekit ingest check] ${checked.length} file(s), ${okLinks.length} link(s) ok, no broken links`,\n );\n } else {\n print(`[lorekit ingest check] ${broken.length} broken link(s) found:`);\n for (const b of broken) {\n print(` ✗ ${b.file}: [[${b.link}]]`);\n }\n process.exitCode = 1;\n }\n out(JSON.stringify(result));\n });\n\n // ---------- forget ----------\n group\n .command('forget <url>')\n .description('Remove a record from the state (e.g. after manual cleanup)')\n .action((url: string) => {\n const corpus = requireCorpus();\n const removed = deleteIngestRecord(corpus, url);\n print(\n removed\n ? `[lorekit ingest forget] removed ${url}`\n : `[lorekit ingest forget] no record for ${url}`,\n );\n out(JSON.stringify({ removed, url }));\n });\n\n // ---------- reconcile ----------\n group\n .command('reconcile')\n .description('Back-fill state for pre-existing 原料/ pages missing a state record')\n .option('--dry-run', 'list what would be added without writing')\n .action((opts: { dryRun?: boolean }) => {\n const corpus = requireCorpus();\n const sourcesRoot = join(corpus, '原料');\n if (!existsSync(sourcesRoot)) {\n print('[lorekit ingest reconcile] no 原料/ directory');\n return;\n }\n const state = loadIngestState(corpus);\n const added: string[] = [];\n for (const mdPath of collectMdFiles(sourcesRoot)) {\n const fm = extractFrontmatter(mdPath);\n const url =\n (typeof fm.source_url === 'string' && fm.source_url) ||\n (typeof fm.url === 'string' && fm.url) ||\n '';\n if (!url) continue;\n if (state.ingests[url]) continue;\n\n const rel = relative(corpus, mdPath);\n const archivedTo = rel.replace(/\\/article\\.md$/, '');\n const sdRaw = fm.source_date;\n const sourceDate =\n typeof sdRaw === 'string'\n ? sdRaw\n : sdRaw instanceof Date\n ? sdRaw.toISOString().slice(0, 10)\n : undefined;\n const now = new Date().toISOString();\n state.ingests[url] = {\n url,\n title: typeof fm.title === 'string' ? fm.title : undefined,\n sourceDate,\n startedAt: now,\n updatedAt: now,\n status: 'completed',\n stepsDone: ['fetch', 'archive', 'wiki', 'lint'],\n archivedTo,\n };\n added.push(url);\n }\n if (!opts.dryRun && added.length > 0) saveIngestState(corpus, state);\n print(\n `[lorekit ingest reconcile] ${opts.dryRun ? 'would add' : 'added'} ${added.length} record(s)`,\n );\n for (const u of added) print(` + ${u}`);\n out(JSON.stringify({ dryRun: !!opts.dryRun, added }));\n });\n}\n","import type { Command } from 'commander';\nimport chalk from 'chalk';\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { ok, warn, err, print, out } from '../utils/logger.js';\nimport { runIndex } from './dir-index.js';\nimport { runVectorSync } from './vector.js';\nimport { runDoctor } from './doctor.js';\nimport { refreshRootIndex } from '../lib/root-index.js';\n\nexport interface SyncOptions {\n force?: boolean;\n model?: string;\n skipDoctor?: boolean;\n skipVector?: boolean;\n skipRootIndex?: boolean;\n json?: boolean;\n report?: boolean;\n}\n\ntype SyncStepStatus = 'ok' | 'skipped' | 'error';\n\ninterface SyncStepReport {\n status: SyncStepStatus;\n detail?: string;\n [key: string]: unknown;\n}\n\nexport interface SyncRunReport {\n status: 'ok' | 'error';\n startedAt: string;\n finishedAt: string;\n corpus: string;\n steps: {\n index: SyncStepReport;\n rootIndex: SyncStepReport;\n vector: SyncStepReport;\n doctor: SyncStepReport;\n };\n reportPath: string | null;\n errors: string[];\n}\n\nfunction createReport(corpus: string): SyncRunReport {\n return {\n status: 'ok',\n startedAt: new Date().toISOString(),\n finishedAt: '',\n corpus,\n steps: {\n index: { status: 'skipped' },\n rootIndex: { status: 'skipped' },\n vector: { status: 'skipped' },\n doctor: { status: 'skipped' },\n },\n reportPath: null,\n errors: [],\n };\n}\n\nfunction writeSyncReport(corpus: string, report: SyncRunReport): string {\n const dir = join(corpus, '.wiki', 'reports', 'sync');\n mkdirSync(dir, { recursive: true });\n const stamp = report.startedAt.replace(/[:.]/g, '-');\n const path = join(dir, `${stamp}.json`);\n report.reportPath = path;\n writeFileSync(path, JSON.stringify(report, null, 2) + '\\n', 'utf-8');\n return path;\n}\n\n/**\n * lorekit sync — 一条命令把「文本档案 + 向量库」对齐。\n *\n * 执行顺序(必须是这个顺序):\n * 1a. runIndex:扫目录生成/刷新所有 _INDEX.md\n * → 向量 L1 的输入源必须先存在,才能被下一步读\n * 1b. refreshRootIndex:合并刷新 corpus/index.md 的四个受控区\n * → L0 向量的输入源;保留人类手写摘要,只追加新页 / 删失踪页\n * 2. runVectorSync(layered=true):增量嵌入 chunk + 刷 L0/L1 向量\n * → L0 读 corpus/index.md 的 ## 分区\n * → L1 读每个 {dir}/_INDEX.md 的条目行\n * 3. runDoctor:sanity check,只报告不阻塞\n */\nexport async function runSync(corpus: string, opts: SyncOptions = {}): Promise<SyncRunReport> {\n const force = opts.force ?? false;\n const model = opts.model ?? 'bge-m3';\n const report = createReport(corpus);\n\n // Step 1a: 各子目录的 _INDEX.md\n print(chalk.cyan('── [1/3] index: refresh _INDEX.md ──'));\n try {\n const generated = runIndex(corpus);\n report.steps.index = { status: 'ok', generated };\n if (generated === 0) {\n warn('no indexable directories found');\n } else {\n ok(`refreshed ${generated} _INDEX.md file(s)`);\n }\n } catch (e) {\n report.status = 'error';\n report.steps.index = { status: 'error', error: (e as Error).message };\n report.errors.push(`index failed: ${(e as Error).message}`);\n err(`index failed: ${(e as Error).message}`);\n throw e;\n }\n\n // Step 1b: corpus 根的 index.md(受控区合并刷新)\n if (!opts.skipRootIndex) {\n try {\n const r = refreshRootIndex(corpus);\n const totals = r.perSection.reduce(\n (acc, s) => ({\n added: acc.added + s.added.length,\n removed: acc.removed + s.removed.length,\n kept: acc.kept + s.kept,\n }),\n { added: 0, removed: 0, kept: 0 },\n );\n report.steps.rootIndex = {\n status: 'ok',\n changed: r.changed,\n added: totals.added,\n removed: totals.removed,\n kept: totals.kept,\n };\n if (!r.changed) {\n ok(`index.md unchanged (${totals.kept} entries across managed sections)`);\n } else {\n ok(\n `index.md merged: +${totals.added} added, -${totals.removed} removed, ${totals.kept} kept`,\n );\n for (const s of r.perSection) {\n if (s.added.length === 0 && s.removed.length === 0) continue;\n for (const slug of s.added) print(` + ${slug}`);\n for (const slug of s.removed) print(` - ${slug} (file gone)`);\n }\n }\n } catch (e) {\n report.status = 'error';\n report.steps.rootIndex = { status: 'error', error: (e as Error).message };\n report.errors.push(`root index sync failed: ${(e as Error).message}`);\n err(`root index sync failed: ${(e as Error).message}`);\n throw e;\n }\n } else {\n report.steps.rootIndex = { status: 'skipped', reason: 'skip-root-index' };\n }\n print();\n\n // Step 2: 向量库(除非显式 --skip-vector)\n if (!opts.skipVector) {\n print(chalk.cyan('── [2/3] vector: sync chunks + L0/L1 ──'));\n try {\n const r = await runVectorSync(corpus, { force, model, layered: true });\n report.steps.vector = { status: 'ok', ...r, model };\n ok(`synced ${r.synced} files (${r.totalChunks} chunks), skipped ${r.skipped} unchanged`);\n } catch (e) {\n report.status = 'error';\n report.steps.vector = { status: 'error', error: (e as Error).message, model };\n report.errors.push(`vector sync failed: ${(e as Error).message}`);\n err(`vector sync failed: ${(e as Error).message}`);\n throw e;\n }\n print();\n } else {\n report.steps.vector = { status: 'skipped', reason: 'skip-vector' };\n }\n\n // Step 3: 健康体检(只报告不阻塞)\n if (!opts.skipDoctor) {\n print(chalk.cyan('── [3/3] doctor: sanity check ──'));\n const issues = await runDoctor(corpus);\n report.steps.doctor = { status: 'ok', issues };\n } else {\n report.steps.doctor = { status: 'skipped', reason: 'skip-doctor' };\n }\n\n report.finishedAt = new Date().toISOString();\n return report;\n}\n\nexport function syncCommand(program: Command): void {\n program\n .command('sync')\n .description('one-shot: refresh _INDEX.md → vector sync (layered) → doctor')\n .option('--force', 'full rebuild of vector index', false)\n .option('--model <name>', 'ollama model name', 'bge-m3')\n .option('--skip-doctor', 'skip the final doctor sanity check', false)\n .option('--skip-vector', 'only refresh _INDEX.md, skip vector sync', false)\n .option('--skip-root-index', 'skip merging corpus/index.md against disk', false)\n .option('--json', 'output machine-readable sync report', false)\n .option('--report', 'write .wiki/reports/sync/<timestamp>.json', false)\n .action(async (opts: SyncOptions) => {\n const corpus = requireCorpus();\n try {\n const report = await runSync(corpus, opts);\n if (opts.report) writeSyncReport(corpus, report);\n if (opts.json) out(JSON.stringify(report, null, 2));\n } catch {\n process.exit(1);\n }\n });\n}\n","/**\n * root-index.ts — sync `corpus/index.md` against the actual filesystem.\n *\n * Unlike `_INDEX.md` (auto-overwritten by `lorekit index`), the root index.md\n * carries human-curated one-line summaries that we deliberately preserve.\n * The sync logic is therefore a *merge*, not an overwrite:\n *\n * - File on disk + already in index → keep the human-written line as-is\n * - File on disk + missing from index → append a new line, summary auto-\n * extracted from `## Compiled Truth` first sentence\n * - In index + file deleted on disk → drop the line\n *\n * Only the four wiki sections are managed: 概念 / 实体 / 摘要 / 专题.\n * Any other heading (e.g. \"写作\", \"待研究问题\", \"空缺\") stays untouched.\n */\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { debug } from '../utils/logger.js';\n\nconst MANAGED_SECTIONS: { heading: string; subdir: string }[] = [\n { heading: '## 概念', subdir: '知识库/概念' },\n { heading: '## 实体', subdir: '知识库/实体' },\n { heading: '## 摘要', subdir: '知识库/摘要' },\n { heading: '## 专题', subdir: '知识库/专题' },\n];\n\ninterface DiskEntry {\n slug: string;\n summary: string;\n}\n\nfunction listEntriesInDir(corpus: string, subdir: string): DiskEntry[] {\n const dirPath = join(corpus, subdir);\n if (!existsSync(dirPath)) return [];\n const out: DiskEntry[] = [];\n for (const name of readdirSync(dirPath)) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md') continue;\n if (!name.endsWith('.md')) continue;\n const file = join(dirPath, name);\n const slug = `${subdir}/${name.replace(/\\.md$/, '')}`;\n out.push({ slug, summary: extractCompiledTruthSnippet(file) });\n }\n return out.sort((a, b) => a.slug.localeCompare(b.slug));\n}\n\n/**\n * Extract a short summary from `## Compiled Truth` — first non-blank paragraph,\n * capped at the first sentence terminator or 80 chars. Falls back to \"—\".\n *\n * Strips a leading `**bold**` lead-in (common pattern: \"**EntityName** is …\")\n * so the summary reads like a definition rather than a label.\n */\nfunction extractCompiledTruthSnippet(filePath: string): string {\n let content: string;\n try {\n content = readFileSync(filePath, 'utf-8');\n } catch (e) {\n // refreshRootIndex 会扫整个 知识库 子目录,单个文件读失败不阻塞批量;走 debug\n debug(`extractCompiledTruthSnippet(${filePath}) failed: ${(e as Error).message}`);\n return '—';\n }\n\n // Skip frontmatter\n const body = content.replace(/^---\\n[\\s\\S]*?\\n---\\n/, '');\n\n // Find ## Compiled Truth section\n const sectionMatch = body.match(/##\\s*Compiled Truth\\s*\\n+([\\s\\S]*?)(?=\\n---|\\n##\\s|$)/);\n if (!sectionMatch) return '—';\n\n // First non-blank line/paragraph\n const para = sectionMatch[1]\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!para) return '—';\n\n // Strip leading **bold** label\n const cleaned = para.replace(/^\\*\\*([^*]+)\\*\\*\\s*/, '$1 ');\n\n // First sentence (Chinese 。 or English .) within 80 chars\n const sentenceMatch = cleaned.match(/^(.{1,80}?[。.!?!?])/);\n if (sentenceMatch) return sentenceMatch[1];\n\n return cleaned.slice(0, 80) + (cleaned.length > 80 ? '…' : '');\n}\n\ninterface MergeResult {\n added: string[];\n removed: string[];\n kept: number;\n}\n\n/**\n * Merge one section in-place. Returns the new content + bookkeeping.\n */\nfunction mergeSection(\n content: string,\n heading: string,\n onDisk: DiskEntry[],\n): { newContent: string; result: MergeResult } {\n const lines = content.split('\\n');\n const startIdx = lines.findIndex((l) => l.trim() === heading);\n if (startIdx === -1) {\n // Section header missing — leave content untouched\n return { newContent: content, result: { added: [], removed: [], kept: 0 } };\n }\n\n // Find end of section: next \"## \" heading or EOF\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n if (lines[i].startsWith('## ')) {\n endIdx = i;\n break;\n }\n }\n\n const sectionBody = lines.slice(startIdx + 1, endIdx);\n const linkRe = /^-\\s+\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/;\n\n const onDiskSlugs = new Set(onDisk.map((e) => e.slug));\n const seenInIndex = new Set<string>();\n const removed: string[] = [];\n const kept: string[] = [];\n\n for (const line of sectionBody) {\n const trimmed = line.trim();\n // Strip blank lines and the placeholder; we re-add canonical padding below.\n if (trimmed === '' || trimmed === '(暂无条目)') continue;\n\n const m = line.match(linkRe);\n if (m) {\n const slug = m[1].trim();\n if (onDiskSlugs.has(slug)) {\n seenInIndex.add(slug);\n kept.push(line);\n } else {\n removed.push(slug);\n }\n } else {\n // preserve any non-link manual annotation (e.g. \"> note about this section\")\n kept.push(line);\n }\n }\n\n // Append entries on disk that aren't in the index yet\n const added: string[] = [];\n for (const e of onDisk) {\n if (!seenInIndex.has(e.slug)) {\n kept.push(`- [[${e.slug}]] — ${e.summary}`);\n added.push(e.slug);\n }\n }\n\n const sectionContentLines = kept.length === 0 ? ['', '(暂无条目)', ''] : ['', ...kept, ''];\n\n const newLines = [\n ...lines.slice(0, startIdx + 1),\n ...sectionContentLines,\n ...lines.slice(endIdx),\n ];\n\n return {\n newContent: newLines.join('\\n'),\n result: { added, removed, kept: seenInIndex.size },\n };\n}\n\nexport interface RootIndexSyncResult {\n filePath: string;\n changed: boolean;\n perSection: { heading: string; added: string[]; removed: string[]; kept: number }[];\n}\n\nexport function refreshRootIndex(corpus: string): RootIndexSyncResult {\n const indexPath = join(corpus, 'index.md');\n if (!existsSync(indexPath)) {\n return { filePath: indexPath, changed: false, perSection: [] };\n }\n\n const before = readFileSync(indexPath, 'utf-8');\n let content = before;\n const perSection: RootIndexSyncResult['perSection'] = [];\n\n for (const sec of MANAGED_SECTIONS) {\n const onDisk = listEntriesInDir(corpus, sec.subdir);\n const { newContent, result } = mergeSection(content, sec.heading, onDisk);\n content = newContent;\n perSection.push({ heading: sec.heading, ...result });\n }\n\n const changed = content !== before;\n if (changed) writeFileSync(indexPath, content, 'utf-8');\n\n return { filePath: indexPath, changed, perSection };\n}\n","/**\n * obsidian-tune.ts — 老用户升级触达 + 维护 .obsidian/graph.json filter(批次 26)\n *\n * 三种模式:\n * - 默认:检查 filter 完整性,diff 输出到 stderr,exit 0/1 便于脚本判断\n * - --write:备份原文件后应用推荐 filter(先 cp .bak.<ts> 再覆盖)\n * - --print:把推荐 filter JSON 打到 stdout,便于 `lorekit obsidian-tune --print > .obsidian/graph.json`\n *\n * 推荐 filter SSOT 在 `templates/default-corpus/.obsidian/graph.json`,\n * 由 `lib/obsidian.ts` 统一读取(避免和模板漂移)。\n */\nimport type { Command } from 'commander';\nimport { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { ok, info, warn, err, print, out } from '../utils/logger.js';\nimport { findCorpus } from '../lib/corpus.js';\nimport {\n getRecommendedFilter,\n getRecommendedGraphConfig,\n readCorpusFilter,\n isFilterComplete,\n missingTokens,\n} from '../lib/obsidian.js';\nimport { tsCompact } from '../lib/date.js';\n\ninterface TuneOpts {\n write?: boolean;\n print?: boolean;\n}\n\nfunction runPrint(): void {\n // 推荐 graph.json 完整体走 stdout(管道友好)\n const cfg = getRecommendedGraphConfig();\n out(JSON.stringify(cfg, null, 2));\n}\n\nfunction runCheck(corpus: string): number {\n const recommended = getRecommendedFilter();\n const cur = readCorpusFilter(corpus);\n\n if (!cur.exists) {\n warn('.obsidian/graph.json 缺失');\n print('');\n print('推荐 filter(含 _归档 / 反馈 + 完整根元数据):');\n print(` ${recommended}`);\n print('');\n print('应用:lorekit obsidian-tune --write');\n return 1;\n }\n\n if (isFilterComplete(cur.search, recommended)) {\n ok('.obsidian/graph.json filter 完整');\n return 0;\n }\n\n warn('.obsidian/graph.json filter 不完整');\n print('');\n print('当前 filter(如有):');\n print(` ${cur.search ?? '(空)'}`);\n print('');\n print('推荐 filter(含 _归档 / 反馈 + 完整根元数据):');\n print(` ${recommended}`);\n print('');\n print('缺少的 token:');\n for (const t of missingTokens(cur.search, recommended)) {\n print(` - ${t}`);\n }\n print('');\n print('应用:lorekit obsidian-tune --write');\n return 1;\n}\n\nfunction runWrite(corpus: string): number {\n const dest = join(corpus, '.obsidian', 'graph.json');\n const destDir = join(corpus, '.obsidian');\n if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });\n\n if (existsSync(dest)) {\n // 先备份再写 —— 红线:绝不许覆盖用户文件无备份\n const backup = `${dest}.bak.${tsCompact()}`;\n cpSync(dest, backup);\n ok(`备份 .obsidian/graph.json → ${backup}`);\n }\n\n // 直接落盘推荐配置(完整对象,不只是 search 字段)\n const cfg = getRecommendedGraphConfig();\n writeFileSync(dest, JSON.stringify(cfg, null, 2) + '\\n', 'utf-8');\n ok('写入推荐 filter');\n info('请关掉 Obsidian「关系图谱」标签页再重开生效');\n return 0;\n}\n\nexport function obsidianTuneCommand(program: Command) {\n program\n .command('obsidian-tune')\n .description('check / apply recommended Obsidian graph filter for the corpus')\n .option('--write', 'apply recommended filter (backs up existing graph.json first)')\n .option('--print', 'print recommended graph.json to stdout (pipe-friendly)')\n .action((opts: TuneOpts) => {\n // --print 不依赖 corpus,纯打印模板\n if (opts.print) {\n runPrint();\n process.exitCode = 0;\n return;\n }\n\n const corpus = findCorpus();\n if (!corpus) {\n err('not inside a corpus (no .wiki/ or CLAUDE.md found)');\n process.exitCode = 2;\n return;\n }\n\n if (opts.write) {\n process.exitCode = runWrite(corpus);\n } else {\n process.exitCode = runCheck(corpus);\n }\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport matter from 'gray-matter';\nimport trash from 'trash';\nimport { collectMdFiles, extractFrontmatter, findSourceByUrl, requireCorpus } from '../lib/corpus.js';\nimport { loadIngestState, saveIngestState } from '../lib/ingest-state.js';\nimport { todayYMDShanghai, tsCompact } from '../lib/date.js';\nimport { createSnapshot } from './snapshot.js';\nimport { runSync } from './sync.js';\nimport { printLintReport, runLint } from './lint.js';\nimport { pruneVectorDbMissingFiles } from '../lib/vectordb/prune.js';\nimport { bad, err, ok, out, print } from '../utils/logger.js';\n\ninterface TrashTarget {\n rel: string;\n abs: string;\n reason: 'target' | 'source' | 'summary';\n}\n\ninterface PageChange {\n file: string;\n removedLines: string[];\n removedSources: string[];\n sourceCountBefore?: number;\n sourceCountAfter?: number;\n}\n\ninterface ReviewItem {\n file: string;\n section: 'Compiled Truth';\n text: string;\n}\n\ninterface RemovalPlan {\n input: string;\n apply: boolean;\n trashTargets: TrashTarget[];\n pageChanges: PageChange[];\n reviewItems: ReviewItem[];\n ingestRecords: string[];\n aliases: string[];\n snapshot?: string;\n syncSkippedVector?: boolean;\n vectorPruned?: number;\n lintIssues?: number;\n}\n\nfunction isUrl(input: string): boolean {\n return /^https?:\\/\\//i.test(input);\n}\n\nfunction toSlash(p: string): string {\n return p.split(sep).join('/');\n}\n\nfunction stripMd(rel: string): string {\n return rel.replace(/\\.md$/, '');\n}\n\nfunction normalizeRel(rel: string): string {\n return toSlash(rel).replace(/^\\.\\//, '').replace(/\\/+/g, '/');\n}\n\nfunction withinCorpus(corpus: string, abs: string): boolean {\n const rel = relative(corpus, abs);\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\n\nfunction resolveInputPath(corpus: string, input: string): string | null {\n const candidates = [];\n const rawAbs = isAbsolute(input) ? input : join(corpus, input);\n candidates.push(rawAbs);\n if (!input.endsWith('.md')) candidates.push(`${rawAbs}.md`);\n for (const candidate of candidates) {\n const abs = resolve(candidate);\n if (withinCorpus(corpus, abs) && existsSync(abs)) return abs;\n }\n return null;\n}\n\nfunction relFromAbs(corpus: string, abs: string): string {\n return normalizeRel(relative(corpus, abs));\n}\n\nfunction aliasesForRel(rel: string): string[] {\n const aliases = new Set<string>();\n const normalized = normalizeRel(rel);\n aliases.add(stripMd(normalized));\n if (normalized.endsWith('/article.md')) {\n aliases.add(stripMd(normalized).replace(/\\/article$/, ''));\n }\n return [...aliases];\n}\n\nfunction readText(abs: string): string {\n return readFileSync(abs, 'utf-8');\n}\n\nfunction extractWikilinks(content: string): string[] {\n const links: string[] = [];\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) links.push(m[1].trim());\n return links;\n}\n\nfunction addExistingTarget(\n corpus: string,\n targets: Map<string, TrashTarget>,\n relOrAbs: string,\n reason: TrashTarget['reason'],\n): void {\n const abs = isAbsolute(relOrAbs) ? relOrAbs : join(corpus, relOrAbs);\n if (!existsSync(abs)) return;\n const rel = relFromAbs(corpus, abs);\n targets.set(rel, { rel, abs, reason });\n}\n\nfunction addSourceTarget(\n corpus: string,\n targets: Map<string, TrashTarget>,\n relOrAbs: string,\n): void {\n const abs = isAbsolute(relOrAbs) ? relOrAbs : join(corpus, relOrAbs);\n if (!existsSync(abs)) return;\n\n const rel = relFromAbs(corpus, abs);\n if (rel.endsWith('/article.md')) {\n addExistingTarget(corpus, targets, dirname(abs), 'source');\n return;\n }\n\n addExistingTarget(corpus, targets, abs, 'source');\n\n if (rel.endsWith('.md')) {\n const assetsDir = abs.replace(/\\.md$/, '.assets');\n addExistingTarget(corpus, targets, assetsDir, 'source');\n }\n}\n\nfunction sourceCandidatesForSlug(corpus: string, slug: string): string[] {\n return [\n join(corpus, slug),\n join(corpus, `${slug}.md`),\n join(corpus, slug, 'article.md'),\n ];\n}\n\nfunction collectSourceUrls(corpus: string, targets: Map<string, TrashTarget>): string[] {\n const urls = new Set<string>();\n for (const target of targets.values()) {\n const files = existsSync(target.abs) && target.rel.endsWith('.md')\n ? [target.abs]\n : collectMdFiles(target.abs);\n for (const file of files) {\n const fm = extractFrontmatter(file);\n if (typeof fm.source_url === 'string') urls.add(fm.source_url);\n if (typeof fm.url === 'string') urls.add(fm.url);\n }\n }\n return [...urls];\n}\n\nfunction addSourcesFromSummary(\n corpus: string,\n targets: Map<string, TrashTarget>,\n summaryAbs: string,\n): void {\n const parsed = matter(readText(summaryAbs));\n const sources = Array.isArray(parsed.data.sources) ? parsed.data.sources : [];\n for (const source of sources) {\n if (typeof source !== 'string') continue;\n for (const candidate of sourceCandidatesForSlug(corpus, source)) {\n if (existsSync(candidate)) addSourceTarget(corpus, targets, candidate);\n }\n }\n\n for (const link of extractWikilinks(parsed.content)) {\n if (!link.startsWith('原料/')) continue;\n for (const candidate of sourceCandidatesForSlug(corpus, link)) {\n if (existsSync(candidate)) addSourceTarget(corpus, targets, candidate);\n }\n }\n}\n\nfunction addSummariesReferencingSources(\n corpus: string,\n targets: Map<string, TrashTarget>,\n aliases: Set<string>,\n): void {\n for (const file of collectMdFiles(join(corpus, '知识库', '摘要'))) {\n const rel = relFromAbs(corpus, file);\n if (targets.has(rel)) continue;\n const content = readText(file);\n if ([...aliases].some((alias) => content.includes(`[[${alias}`))) {\n addExistingTarget(corpus, targets, file, 'summary');\n addSourcesFromSummary(corpus, targets, file);\n }\n }\n}\n\nfunction compiledTruthSnippets(content: string, aliases: Set<string>, input: string): string[] {\n const body = content.replace(/^---\\n[\\s\\S]*?\\n---\\n/, '');\n const match = body.match(/##\\s*Compiled Truth\\s*\\n+([\\s\\S]*?)(?=\\n##\\s|$)/);\n if (!match) return [];\n return match[1]\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter((p) => {\n if (!p) return false;\n if (isUrl(input) && p.includes(input)) return true;\n return [...aliases].some((alias) => p.includes(`[[${alias}`));\n });\n}\n\nfunction rewritePageForRemoval(\n corpus: string,\n file: string,\n aliases: Set<string>,\n): { change: PageChange | null; nextContent: string } {\n const rel = relFromAbs(corpus, file);\n const parsed = matter(readText(file));\n const removedSources: string[] = [];\n let sourceCountBefore: number | undefined;\n let sourceCountAfter: number | undefined;\n\n if (Array.isArray(parsed.data.sources)) {\n const nextSources = parsed.data.sources.filter((source: unknown) => {\n if (typeof source !== 'string') return true;\n const remove = aliases.has(stripMd(normalizeRel(source)));\n if (remove) removedSources.push(source);\n return !remove;\n });\n if (removedSources.length > 0) {\n parsed.data.sources = nextSources;\n const rawCount = parsed.data.source_count;\n const numeric =\n typeof rawCount === 'number'\n ? rawCount\n : typeof rawCount === 'string'\n ? Number.parseInt(rawCount, 10)\n : Number.NaN;\n if (Number.isFinite(numeric)) {\n sourceCountBefore = numeric;\n sourceCountAfter = Math.max(0, numeric - new Set(removedSources).size);\n parsed.data.source_count = sourceCountAfter;\n }\n parsed.data.updated = todayYMDShanghai();\n }\n }\n\n const removedLines: string[] = [];\n const nextLines = parsed.content.split('\\n').filter((line) => {\n const trimmed = line.trim();\n const hasTargetLink = [...aliases].some((alias) => line.includes(`[[${alias}`));\n const removable = hasTargetLink && /^[-*]\\s+/.test(trimmed);\n if (removable) {\n removedLines.push(line);\n return false;\n }\n return true;\n });\n if (removedLines.length > 0) parsed.data.updated = todayYMDShanghai();\n\n const changed = removedLines.length > 0 || removedSources.length > 0;\n const nextContent = changed ? matter.stringify(nextLines.join('\\n'), parsed.data) : readText(file);\n\n return {\n nextContent,\n change: changed\n ? {\n file: rel,\n removedLines,\n removedSources,\n sourceCountBefore,\n sourceCountAfter,\n }\n : null,\n };\n}\n\nfunction buildRemovalPlan(corpus: string, input: string, apply: boolean): RemovalPlan {\n const targets = new Map<string, TrashTarget>();\n const ingestRecords = new Set<string>();\n\n if (isUrl(input)) {\n const state = loadIngestState(corpus);\n const record = state.ingests[input];\n ingestRecords.add(input);\n if (record?.archivedTo) addSourceTarget(corpus, targets, record.archivedTo);\n for (const page of record?.wikiPages ?? []) {\n if (normalizeRel(page).startsWith('知识库/摘要/')) {\n const pageAbs = join(corpus, page);\n addExistingTarget(corpus, targets, pageAbs, 'summary');\n if (existsSync(pageAbs)) addSourcesFromSummary(corpus, targets, pageAbs);\n }\n }\n const source = findSourceByUrl(corpus, input);\n if (source) addSourceTarget(corpus, targets, source);\n } else {\n const abs = resolveInputPath(corpus, input);\n if (!abs) throw new Error(`target not found inside corpus: ${input}`);\n const rel = relFromAbs(corpus, abs);\n if (rel.startsWith('原料/')) {\n addSourceTarget(corpus, targets, abs);\n } else if (rel.startsWith('知识库/摘要/')) {\n addExistingTarget(corpus, targets, abs, 'summary');\n addSourcesFromSummary(corpus, targets, abs);\n } else {\n addExistingTarget(corpus, targets, abs, 'target');\n }\n }\n\n let aliases = new Set([...targets.keys()].flatMap((rel) => aliasesForRel(rel)));\n addSummariesReferencingSources(corpus, targets, aliases);\n aliases = new Set([...targets.keys()].flatMap((rel) => aliasesForRel(rel)));\n\n for (const url of collectSourceUrls(corpus, targets)) ingestRecords.add(url);\n\n const trashedRels = new Set(targets.keys());\n const pageChanges: PageChange[] = [];\n const reviewItems: ReviewItem[] = [];\n for (const file of collectMdFiles(corpus)) {\n const rel = relFromAbs(corpus, file);\n if (trashedRels.has(rel)) continue;\n if ([...trashedRels].some((targetRel) => rel.startsWith(`${targetRel}/`))) continue;\n\n const { change } = rewritePageForRemoval(corpus, file, aliases);\n if (change) pageChanges.push(change);\n for (const text of compiledTruthSnippets(readText(file), aliases, input)) {\n reviewItems.push({ file: rel, section: 'Compiled Truth', text });\n }\n }\n\n return {\n input,\n apply,\n trashTargets: [...targets.values()].sort((a, b) => a.rel.localeCompare(b.rel)),\n pageChanges,\n reviewItems,\n ingestRecords: [...ingestRecords],\n aliases: [...aliases].sort(),\n };\n}\n\nasync function moveToTrash(paths: string[]): Promise<void> {\n const testTrashDir = process.env.LOREKIT_TEST_TRASH_DIR;\n if (testTrashDir) {\n mkdirSync(testTrashDir, { recursive: true });\n for (const p of paths) {\n if (!existsSync(p)) continue;\n const dest = join(testTrashDir, `${tsCompact()}-${basename(p)}`);\n renameSync(p, dest);\n }\n return;\n }\n await trash(paths, { glob: false });\n}\n\nfunction applyPageChanges(corpus: string, plan: RemovalPlan): void {\n const aliases = new Set(plan.aliases);\n for (const change of plan.pageChanges) {\n const file = join(corpus, change.file);\n const { nextContent } = rewritePageForRemoval(corpus, file, aliases);\n writeFileSync(file, nextContent, 'utf-8');\n }\n}\n\nfunction forgetIngestRecords(corpus: string, urls: string[]): void {\n if (urls.length === 0) return;\n const state = loadIngestState(corpus);\n let changed = false;\n for (const url of urls) {\n if (state.ingests[url]) {\n delete state.ingests[url];\n changed = true;\n }\n }\n if (changed) saveIngestState(corpus, state);\n}\n\nfunction printPlan(plan: RemovalPlan): void {\n print(`lorekit remove — ${plan.apply ? 'apply' : 'dry-run'}\\n`);\n\n print(`将移动到系统回收站 (${plan.trashTargets.length})`);\n for (const target of plan.trashTargets) {\n print(` - ${target.rel} (${target.reason})`);\n }\n if (plan.trashTargets.length === 0) print(' - (无)');\n print();\n\n print(`将修改页面 (${plan.pageChanges.length})`);\n for (const change of plan.pageChanges) {\n print(` - ${change.file}`);\n if (change.removedSources.length > 0) {\n print(` sources: -${change.removedSources.length}`);\n }\n if (change.sourceCountBefore !== undefined && change.sourceCountAfter !== undefined) {\n print(` source_count: ${change.sourceCountBefore} -> ${change.sourceCountAfter}`);\n }\n if (change.removedLines.length > 0) {\n print(` lines: -${change.removedLines.length}`);\n }\n }\n if (plan.pageChanges.length === 0) print(' - (无)');\n print();\n\n if (plan.reviewItems.length > 0) {\n print(`需人工复核 Compiled Truth (${plan.reviewItems.length})`);\n for (const item of plan.reviewItems) {\n print(` - ${item.file}: ${item.text.slice(0, 120)}`);\n }\n print();\n }\n\n if (!plan.apply) {\n print('dry-run only. Run again with --apply to move files to OS Trash.');\n }\n}\n\nexport function removeCommand(program: Command): void {\n program\n .command('remove')\n .argument('<target>', 'URL or corpus-relative path to remove')\n .option('--apply', 'execute the removal; default is dry-run', false)\n .option('--json', 'emit a machine-readable JSON report', false)\n .description('safely remove a source/wiki page and provenance-linked references')\n .action(async (target: string, opts: { apply?: boolean; json?: boolean }) => {\n const corpus = requireCorpus();\n let plan: RemovalPlan;\n try {\n plan = buildRemovalPlan(corpus, target, !!opts.apply);\n } catch (e) {\n err((e as Error).message);\n process.exitCode = 2;\n return;\n }\n\n if (!opts.json) printPlan(plan);\n if (opts.json && !opts.apply) out(JSON.stringify(plan));\n if (!opts.apply) return;\n\n if (plan.trashTargets.length === 0 && plan.pageChanges.length === 0) {\n bad('nothing to remove');\n process.exitCode = 1;\n if (opts.json) out(JSON.stringify(plan));\n return;\n }\n\n try {\n const snapshot = await createSnapshot(corpus, { tag: 'remove' });\n plan.snapshot = snapshot;\n ok(`snapshot saved: ${snapshot}`);\n\n applyPageChanges(corpus, plan);\n forgetIngestRecords(corpus, plan.ingestRecords);\n await moveToTrash(plan.trashTargets.map((t) => t.abs));\n ok(`moved ${plan.trashTargets.length} item(s) to OS Trash`);\n\n const hasVectorDb = existsSync(join(corpus, '.wiki', 'vector.sqlite'));\n if (hasVectorDb) {\n plan.vectorPruned = await pruneVectorDbMissingFiles(corpus);\n if (plan.vectorPruned > 0) ok(`vector pruned ${plan.vectorPruned} missing file(s)`);\n }\n\n const skipVector = !hasVectorDb || process.env.LOREKIT_TEST_SKIP_VECTOR_SYNC === '1';\n plan.syncSkippedVector = skipVector;\n await runSync(corpus, { skipVector });\n\n const issues = runLint(corpus);\n plan.lintIssues = issues.length;\n printLintReport(corpus, issues);\n } catch (e) {\n err((e as Error).message);\n process.exitCode = 1;\n }\n\n if (opts.json) out(JSON.stringify(plan));\n });\n}\n","import type { Command } from 'commander';\nimport { requireCorpus } from '../lib/corpus.js';\nimport {\n doctorGbrain,\n exportForGbrain,\n getGbrainStatus,\n queryGbrain,\n syncGbrain,\n} from '../lib/integrations/gbrain.js';\nimport { bad, info, ok, out, print, warn } from '../utils/logger.js';\n\nfunction printJson(result: unknown): void {\n out(JSON.stringify(result, null, 2));\n}\n\nexport function gbrainCommand(program: Command): void {\n const cmd = program\n .command('gbrain')\n .description('optional GBrain read-only integration');\n\n cmd\n .command('status')\n .description('check whether GBrain is installed')\n .option('--json', 'output json', false)\n .action(async (opts: { json?: boolean }) => {\n const result = await getGbrainStatus();\n if (opts.json) {\n printJson(result);\n return;\n }\n if (result.installed) {\n ok(`GBrain installed: ${result.version ?? result.binary}`);\n } else {\n warn('GBrain is not installed');\n print(result.installHint);\n }\n });\n\n cmd\n .command('export')\n .description('export lorekit 知识库/ pages into a GBrain-safe staging directory')\n .option('--out <dir>', 'export directory relative to corpus')\n .option('--dry-run', 'preview only; do not write files', false)\n .option('--json', 'output json', false)\n .action((opts: { out?: string; dryRun?: boolean; json?: boolean }) => {\n const corpus = requireCorpus();\n const result = exportForGbrain(corpus, { out: opts.out, dryRun: opts.dryRun });\n if (opts.json) {\n printJson(result);\n return;\n }\n if (result.dryRun) {\n info(`would export ${result.pagesExported} page(s) to ${result.exportDir}`);\n } else {\n ok(`exported ${result.pagesExported} page(s) to ${result.exportDir}`);\n }\n if (result.pagesSkipped > 0) warn(`skipped ${result.pagesSkipped} index file(s)`);\n for (const w of result.warnings) warn(w);\n });\n\n cmd\n .command('sync')\n .description('export lorekit pages and run gbrain import on the staging directory')\n .option('--dry-run', 'preview only; do not write export files or call gbrain import', false)\n .option('--json', 'output json', false)\n .option(\n '--export-even-if-missing',\n 'refresh staging export even when the gbrain binary is missing',\n false,\n )\n .action(async (opts: { dryRun?: boolean; json?: boolean; exportEvenIfMissing?: boolean }) => {\n const corpus = requireCorpus();\n const result = await syncGbrain(corpus, opts);\n if (opts.json) {\n printJson(result);\n } else if (result.status === 'ok') {\n if (result.dryRun) {\n info(`would export ${result.export?.pagesExported ?? 0} page(s); gbrain import skipped`);\n } else {\n ok(`gbrain sync complete: ${result.export?.pagesExported ?? 0} page(s) exported`);\n }\n } else {\n bad(`gbrain sync failed: ${result.errors.join('; ')}`);\n }\n process.exitCode = result.status === 'ok' ? 0 : 1;\n });\n\n cmd\n .command('doctor')\n .description('check GBrain integration health')\n .option('--json', 'output json', false)\n .action(async (opts: { json?: boolean }) => {\n const corpus = requireCorpus();\n const result = await doctorGbrain(corpus);\n if (opts.json) {\n printJson(result);\n } else {\n if (result.status === 'ok') ok('GBrain integration healthy');\n for (const issue of result.issues) {\n const line = `${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n }\n process.exitCode = result.status === 'error' ? 1 : 0;\n });\n\n cmd\n .command('query')\n .argument('<text>', 'query text')\n .description('run gbrain query without writing back to lorekit')\n .option('--json', 'output json', false)\n .option('--no-stale-check', 'skip corpus export/sync freshness guard')\n .action(async (text: string, opts: { json?: boolean; staleCheck?: boolean }) => {\n const corpus = requireCorpus();\n const result = await queryGbrain(corpus, text, { staleCheck: opts.staleCheck !== false });\n if (opts.json) {\n printJson(result);\n } else {\n info(result.message);\n for (const w of result.warnings) warn(w);\n if (result.gbrain?.stdout) print(result.gbrain.stdout.trim());\n if (result.gbrain?.stderr) warn(result.gbrain.stderr.trim());\n if (result.status === 'error') bad(result.errors.join('; '));\n }\n process.exitCode = result.status === 'ok' ? 0 : 1;\n });\n}\n"],"mappings":"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","names":["createHash","readFileSync","readdirSync","basename","join","relative","matter","sha256","existsSync","mkdirSync","join","Database","readFileSync","basename","matter","relative","chunkFile","sha256","existsSync","readFileSync","readdirSync","join","relative","matter","createHash","existsSync","join","rec","existsSync","chalk","readFileSync","statSync","join","dirname","join","dirname","readFileSync","existsSync","readdirSync","join","chalk","resolve","existsSync","readdirSync","join","version","program","chalk","existsSync","lstatSync","readFileSync","readdirSync","join","relative","chalk","existsSync","readFileSync","join","join","readFileSync","existsSync","existsSync","mkdirSync","readFileSync","join","existsSync","join","resolve","existsSync","join","version","createHash","existsSync","mkdirSync","readFileSync","readdirSync","statSync","writeFileSync","dirname","join","relative","resolve","matter","existsSync","readFileSync","writeFileSync","createHash","out","join","resolve","existsSync","readdirSync","relative","mkdirSync","matter","readFileSync","statSync","dirname","writeFileSync","join","mkdirSync","exportResult","result","existsSync","readFileSync","join","existsSync","chalk","readFileSync","readdirSync","relative","lstatSync","program","issues","readFileSync","statSync","relative","program","relative","statSync","readFileSync","readFileSync","relative","basename","chalk","basename","relative","readFileSync","chalk","program","existsSync","mkdirSync","readFileSync","writeFileSync","join","basename","readFileSync","join","existsSync","basename","mkdirSync","writeFileSync","program","existsSync","readdirSync","readFileSync","statSync","writeFileSync","lstatSync","join","basename","relative","resolve","readFileSync","basename","statSync","relative","join","readdirSync","lstatSync","writeFileSync","existsSync","resolve","program","existsSync","mkdirSync","readdirSync","lstatSync","join","lstatSync","program","join","existsSync","readdirSync","mkdirSync","existsSync","mkdirSync","writeFileSync","unlinkSync","readdirSync","statSync","join","relative","readdirSync","join","relative","mkdirSync","statSync","writeFileSync","existsSync","unlinkSync","program","existsSync","mkdirSync","readFileSync","join","dirname","createInterface","tar","chalk","ask","createInterface","resolve","program","existsSync","join","mkdirSync","readFileSync","chalk","dirname","readFileSync","join","relative","join","relative","readFileSync","program","createHash","existsSync","readFileSync","join","relative","relative","embed","embedSingle","openDb","syncFile","buildLayeredIndex","collectFiles","relative","createHash","readFileSync","program","queryFlat","queryLayered","queryBM25Layered","queryHybrid","join","existsSync","getStatus","info","existsSync","mkdirSync","join","relative","mkdir","writeFile","join","today","SHANGHAI_TZ_OFFSET_MS","join","join","cheerio","mkdir","writeFile","join","cheerio","err","mkdir","today","join","writeFile","mkdir","writeFile","join","mkdir","today","join","writeFile","join","mkdir","today","writeFile","existsSync","mkdirSync","readFileSync","writeFileSync","join","dirname","program","join","existsSync","mkdirSync","relative","existsSync","readFileSync","writeFileSync","join","relative","join","existsSync","readFileSync","writeFileSync","sep","program","relative","chalk","mkdirSync","writeFileSync","join","existsSync","readFileSync","readdirSync","writeFileSync","join","out","writeSyncReport","join","mkdirSync","writeFileSync","chalk","program","cpSync","existsSync","mkdirSync","writeFileSync","join","join","existsSync","mkdirSync","cpSync","writeFileSync","program","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","basename","dirname","join","relative","resolve","matter","relative","join","resolve","existsSync","readFileSync","dirname","matter","mkdirSync","basename","renameSync","writeFileSync","program","program","existsSync","chalk"]} \ No newline at end of file +{"version":3,"sources":["../src/lib/paths.ts","../src/utils/logger.ts","../src/lib/vectordb/files.ts","../src/lib/vectordb/schema.ts","../src/lib/ollama.ts","../src/lib/chunker.ts","../src/lib/vectordb/sync.ts","../src/lib/vectordb/build-layered-index.ts","../src/lib/vectordb/query-flat.ts","../src/lib/vectordb/query-layered.ts","../src/lib/vectordb/query-bm25.ts","../src/lib/vectordb/query-hybrid.ts","../src/lib/vectordb/status.ts","../src/lib/vectordb/index.ts","../src/cli.ts","../src/lib/corpus.ts","../src/utils/fs.ts","../src/commands/init.ts","../src/commands/doctor.ts","../src/lib/obsidian.ts","../src/lib/integrations/gbrain.ts","../src/lib/integrations/gbrain-status.ts","../src/lib/integrations/process.ts","../src/lib/integrations/gbrain-export.ts","../src/lib/integrations/manifest.ts","../src/commands/stats.ts","../src/commands/lint.ts","../src/commands/audit.ts","../src/lib/date.ts","../src/commands/dir-index.ts","../src/commands/install-skills.ts","../src/commands/snapshot.ts","../src/commands/restore.ts","../src/commands/search.ts","../src/commands/vector.ts","../src/lib/vectordb/prune.ts","../src/commands/fetch.ts","../src/lib/fetcher/index.ts","../src/lib/fetcher/frontmatter.ts","../src/lib/fetcher/helpers.ts","../src/lib/fetcher/http.ts","../src/lib/fetcher/images.ts","../src/lib/fetcher/routes/web.ts","../src/lib/fetcher/routes/weixin.ts","../src/lib/fetcher/routes/gist.ts","../src/lib/fetcher/routes/github.ts","../src/lib/ingest-state.ts","../src/commands/ingest.ts","../src/commands/sync.ts","../src/lib/root-index.ts","../src/commands/obsidian-tune.ts","../src/commands/remove.ts","../src/commands/gbrain.ts"],"sourcesContent":["/**\n * paths.ts — corpus 路径 / 排除规则的单一事实源。\n *\n * 历史背景(LEGACY P1-1):corpus.ts / vectordb.ts / commands/{index,lint,snapshot}\n * 各自维护独立的\"排除目录\"集合,加新顶层目录时容易漏改其中一处。本文件把所有\n * 集合集中起来,下游 import 即可。\n *\n * CONVENTIONS Do Not #11:建 paths.ts 后不许再硬编码新的\"排除目录\"常量。\n *\n * 命名约定:\n * - alwaysExclude* — 全局通用,所有 collect / scan 都该跳过\n * - vectorInclude* — 仅向量库索引时纳入\n * - vectorExclude* — 仅向量库索引时排除\n * - lintSkip* — 仅 lint 时跳过\n * - indexExclude* — 仅 dir-index (`_INDEX.md`) 生成时跳过\n * - snapshotExclude* — 仅 snapshot 时跳过\n *\n * 后续批次(6/7)会继续往本文件追加 set;先生不要在其他文件里\"另起炉灶\"。\n */\n\n/**\n * 全局排除:任何 markdown 收集都该跳过的文件名(不是目录)。\n * - .gitkeep / .DS_Store:环境噪声\n * - _INDEX.md:`lorekit index` 自动生成的目录索引文件,不是用户内容\n */\nexport const alwaysExcludeNames: ReadonlySet<string> = new Set([\n '.gitkeep',\n '.DS_Store',\n '_INDEX.md',\n]);\n\n// ---------------------------------------------------------------------------\n// 向量库索引(vectordb.ts)专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * 向量库纳入索引的目录前缀。任何文件 rel path 必须以下列之一开头才参与 chunk\n * embedding。注意:原料里只索引\"长文\"类(文章 / 书籍 / 会议);剪藏 / 录音\n * 走另外的路径(embed 摘要而非全文)。\n */\nexport const vectorIncludeDirs: readonly string[] = [\n '知识库',\n '每日',\n '写作',\n '原料/文章',\n '原料/书籍',\n '原料/会议',\n];\n\n/**\n * 向量库排除目录前缀。先 exclude 检查再 include 检查 — exclude 规则胜出。\n * - `_工作台` / `_archive` / `_归档`:过渡区 / 冷数据,不该污染向量空间\n * - `原料/录音` / `原料/剪藏`:走摘要 embedding,不索引全文 chunk\n * - `反馈` / `系统` / `.wiki`:流程 / 规范 / 元数据,跟知识检索无关\n * - `输出`:LLM 产物(问答 / 文章 / 幻灯片 / 图表 / 体检报告 / 空缺分析),\n * 是二次加工产物不该回流进向量空间(否则会和原始 wiki 页形成互相加强的回音)\n *\n * 注:`.assets/`(各 markdown 的图片子目录)**不需要在此单独列**——它一定是\n * 某个父目录的子目录,随父目录的 include / exclude 规则自动生效。\n */\nexport const vectorExcludePrefixes: readonly string[] = [\n '_工作台',\n '_archive',\n '_归档',\n '原料/录音',\n '原料/剪藏',\n '反馈',\n '系统',\n '输出',\n '.wiki',\n];\n\n/**\n * 向量库排除文件名(不含 `_INDEX.md` —— 注意跟 alwaysExcludeNames 不同)。\n * `_INDEX.md` 由 buildLayeredIndex 通过 L1 路径单独处理(embed 条目摘要),\n * 不应进 chunk 池;但当前 vectordb.shouldIndex 没有显式排除它,依赖 INCLUDE_DIRS\n * 圈定边界。本集合**严格保留迁移前行为**,不在本批改语义。\n */\nexport const vectorExcludeNames: ReadonlySet<string> = new Set(['.gitkeep', '.DS_Store']);\n\n// ---------------------------------------------------------------------------\n// `lorekit index` 专用规则(生成 _INDEX.md)\n// ---------------------------------------------------------------------------\n\n/**\n * `lorekit index` 不为下列前缀目录生成 `_INDEX.md`。\n * 系统 / 反馈 / 工作台 / 归档 / git / .wiki 都不是用户内容书架。\n */\nexport const indexExcludeDirPrefixes: readonly string[] = [\n '.wiki',\n '.git',\n '_归档',\n '_工作台',\n '系统',\n '反馈',\n];\n\n/**\n * 判断给定 corpus 内相对路径是否落在\"不索引\"前缀里。\n * 对外暴露的小工具(doctor.ts / commands/dir-index.ts 都用)。\n */\nexport function isIndexExcluded(rel: string): boolean {\n for (const prefix of indexExcludeDirPrefixes) {\n if (rel === prefix || rel.startsWith(prefix + '/')) return true;\n }\n return false;\n}\n\n/**\n * 判断目录是否\"目录包装式原料\"——即 `<dir>/article.md` 存在。\n * 此类目录在生成 `_INDEX.md` 时被当作\"一个条目\"而非容器,不递归进入。\n *\n * 注:lstatSync 直接 throw 时返回 false(catch 静默是有意——目录不存在 / 权限拒绝\n * 都按\"不是 folder package\" 处理;这是 paths.ts 的小预言式 helper,没有副作用)。\n */\nexport function isFolderPackage(dir: string): boolean {\n const articlePath = pathJoin(dir, 'article.md');\n try {\n return lstatSync(articlePath).isFile();\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// `lorekit lint` 专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * lint 时按 basename 跳过 frontmatter / orphan 检查的文件名(任何位置)。\n * 这些是 schema / 入口文档,不该被当作 wiki 页验证。\n */\nexport const lintSkipFrontmatterBasenames: ReadonlySet<string> = new Set([\n 'README.md',\n 'AGENTS.md',\n 'CLAUDE.md',\n 'MEMORY.md',\n]);\n\n/**\n * 仅在 corpus 根目录跳过的文件名。`index.md` / `log.md` 在子目录里如果有\n * 同名文件,仍按普通 wiki 页校验。\n */\nexport const lintRootOnlySkipBasenames: ReadonlySet<string> = new Set(['index.md', 'log.md']);\n\n/**\n * lint 不参与 orphan 检查的目录前缀(过渡区 / 冷数据 / 系统规范 / 模板区)。\n * `知识库/模板/` 下是页面模板,不是正式 wiki 页,天然无入链,不应报 orphan。\n */\nexport const lintSkipOrphanPrefixes: readonly string[] = [\n '_工作台/',\n '_归档/',\n '系统/',\n '知识库/模板/',\n];\n\n/**\n * lint 不参与 frontmatter 检查的目录前缀(过渡区 / 冷数据)。\n * 注:`系统/` 故意保留 frontmatter 检查(schema 文件应规范)。\n */\nexport const lintSkipFrontmatterPrefixes: readonly string[] = ['_工作台/', '_归档/'];\n\n/**\n * lint 不参与 broken-link 检查的目录前缀。\n * `知识库/模板/` 下模板正文含 `[[知识库/摘要/xxx]]` 这种占位符,让 LLM 建页时替换,\n * 不是真实 wikilink 目标,不该被报 broken。\n */\nexport const lintSkipBrokenLinkPrefixes: readonly string[] = ['知识库/模板/'];\n\n// ---------------------------------------------------------------------------\n// `lorekit snapshot` 专用规则\n// ---------------------------------------------------------------------------\n\n/**\n * snapshot 打包时跳过的目录 / 文件名。\n * - .wiki:corpus 元数据(含向量库),快照里要重建不要拷\n * - .git:corpus 自己的 git 仓库\n * - .DS_Store:环境噪声\n */\nexport const snapshotExcludeNames: ReadonlySet<string> = new Set(['.wiki', '.git', '.DS_Store']);\n\n// ---------------------------------------------------------------------------\n// 内部 helper(用静态 import;放文件末尾以保持顶部导出干净)\n// ---------------------------------------------------------------------------\n\nimport { lstatSync } from 'node:fs';\nimport { join as pathJoin } from 'node:path';\n","/**\n * logger.ts — 全仓库人类输出唯一通道(CONVENTIONS #2 强制)。\n *\n * 通道分流(CONVENTIONS #3):所有人类信息(ok / bad / warn / err / info / debug)\n * 一律 stderr,stdout 只留给机器可读输出(JSON / 数据)。这样\n * `lorekit xxx 2>/dev/null | jq .` 才能正确读到结果。\n *\n * debug:受 `LOREKIT_DEBUG=1` 环境变量控制;不开时静默。\n *\n * 历史:批次 10 之前 `ok` / `bad` 走 stdout,是 CONVENTIONS 已规定但 logger 自己\n * 没落地的 bug(LEGACY P1-3)。本批次修复。\n */\nimport chalk from 'chalk';\n\nconst DEBUG_ENABLED = process.env.LOREKIT_DEBUG === '1';\n\n/** 成功 / 完成提示,绿勾 */\nexport const ok = (msg: string) => console.error(`${chalk.green('✓')} ${msg}`);\n\n/** 失败 / 取消提示,红叉。批次 10 前错误地写 stdout,已修正 */\nexport const bad = (msg: string) => console.error(`${chalk.red('✗')} ${msg}`);\n\n/** 警告:可继续但需注意 */\nexport const warn = (msg: string) => console.error(`${chalk.yellow('lorekit:')} ${msg}`);\n\n/** 错误:致命或已退出条件 */\nexport const err = (msg: string) => console.error(`${chalk.red('lorekit:')} ${msg}`);\n\n/** 一般信息:进度 / 提示 */\nexport const info = (msg: string) => console.error(`${chalk.cyan('ℹ')} ${msg}`);\n\n/** 调试输出:仅当 LOREKIT_DEBUG=1 时打印;其他时候静默 */\nexport const debug = (msg: string) => {\n if (DEBUG_ENABLED) console.error(`${chalk.dim('debug:')} ${msg}`);\n};\n\n/**\n * 原样写一行到 stderr(无前缀 / 无装饰)。\n * 用于 banner / 分隔线 / 空行 / 自定义 chalk 着色的 header 等纯展示内容。\n * 调用方负责自己加 chalk —— 如 `print(chalk.cyan('── 区块 ──'))`。\n */\nexport const print = (msg = '') => console.error(msg);\n\n/**\n * 原样写一行到 **stdout**(机器可读通道)。\n * 用于 JSON / 数据结构 / 任何下游会用 `cmd | jq` 解析的输出。\n * 这是 stdout 的唯一合法 logger 入口(CONVENTIONS Do Not #2)。\n */\nexport const out = (msg: string) => console.log(msg);\n","/**\n * vectordb/files.ts — 文件发现 + 字节工具 + 页摘要抽取\n *\n * 批次 22a strangler fig 第一步:从 src/lib/vectordb.ts copy 出文件层小工具。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - sha256 / float32ToBuffer / distanceToScore:字节层小工具\n * - shouldIndex / collectFiles:基于 paths.ts 规则的 corpus 文件发现\n * - extractPageSummary:从 markdown 文件抽 \"title: intro\" 形式的页摘要\n *\n * 不依赖 schema.ts(无 db 状态),self-contained 除 paths.ts 与 gray-matter。\n */\n\nimport { createHash } from 'node:crypto';\nimport { readFileSync, readdirSync } from 'node:fs';\nimport { basename, join, relative } from 'node:path';\n\nimport matter from 'gray-matter';\n\nimport { vectorIncludeDirs, vectorExcludePrefixes, vectorExcludeNames } from '../paths.js';\n\n// ---------------------------------------------------------------------------\n// 字节层小工具\n// ---------------------------------------------------------------------------\n\n/**\n * 文件 SHA-256 hex digest。用于 sync 时判断 `<doc>.path → updated_at` 是否需要\n * 重新计算 embedding(`sha256` 没变就直接复用旧 chunks)。\n */\nexport function sha256(filePath: string): string {\n const data = readFileSync(filePath);\n return createHash('sha256').update(data).digest('hex');\n}\n\n/**\n * Float32Array → Buffer 零拷贝转换(共享底层 ArrayBuffer),用于把 embedding\n * 写进 sqlite BLOB 列。\n */\nexport function float32ToBuffer(arr: Float32Array): Buffer {\n return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);\n}\n\n/**\n * Convert sqlite-vec cosine distance to similarity score.\n * sqlite-vec returns sqrt(2*(1 - cos_sim)), so:\n * score = 1 - distance^2 / 2\n */\nexport function distanceToScore(distance: number): number {\n return 1.0 - (distance * distance) / 2.0;\n}\n\n// ---------------------------------------------------------------------------\n// 文件发现\n// ---------------------------------------------------------------------------\n\n/**\n * 判断给定 corpus 相对路径是否应进向量索引。\n *\n * 规则(来自 paths.ts,CONVENTIONS Do Not #11 集中维护):\n * 1. 文件名命中 `vectorExcludeNames`(如 `_INDEX.md` / `.gitkeep`)→ 跳\n * 2. 非 `.md` → 跳\n * 3. 路径前缀命中 `vectorExcludePrefixes`(如 `_工作台/` / `_归档/`)→ 跳\n * 4. 路径前缀命中 `vectorIncludeDirs`(如 `知识库/` / `原料/`)→ 收\n * 5. 都不命中 → 跳(保守策略:未明确包含的目录不索引)\n */\nexport function shouldIndex(rel: string): boolean {\n const parts = rel.split('/');\n if (vectorExcludeNames.has(parts[parts.length - 1])) return false;\n if (!rel.endsWith('.md')) return false;\n for (const prefix of vectorExcludePrefixes) {\n if (rel === prefix || rel.startsWith(prefix + '/')) return false;\n }\n for (const inc of vectorIncludeDirs) {\n if (rel === inc || rel.startsWith(inc + '/')) return true;\n }\n return false;\n}\n\n/**\n * 递归收集 corpus 下所有应进索引的 .md 文件,返回排序后的绝对路径列表。\n * 排序保证多次运行得到稳定的 doc_id 顺序,便于 diff debug。\n */\nexport function collectFiles(corpus: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string) {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n // 目录读不到(权限 / 临时被删)就跳,整体扫描继续。\n return;\n }\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.md')) {\n const rel = relative(corpus, full);\n if (shouldIndex(rel)) {\n results.push(full);\n }\n }\n }\n }\n\n walk(corpus);\n return results.sort();\n}\n\n// ---------------------------------------------------------------------------\n// 页摘要\n// ---------------------------------------------------------------------------\n\n/**\n * 给一个 markdown 文件抽 \"title: intro\" 形式的页摘要,用于 page_summaries 向量。\n *\n * title 优先级:frontmatter.title > 第一个 `# heading` > basename(file, '.md')\n *\n * intro 优先级:`## Compiled Truth` 段(lorekit 知识库页约定的核心摘要节)前 200 字\n * > body 起始前 200 字\n */\nexport function extractPageSummary(filePath: string): string {\n const raw = readFileSync(filePath, 'utf-8');\n const { data: fm, content: body } = matter(raw);\n\n let title = (fm.title as string) || '';\n if (!title) {\n const m = body.match(/^#\\s+(.+)/m);\n title = m ? m[1].trim() : basename(filePath, '.md');\n }\n\n // Try \"## Compiled Truth\" section\n const ctMatch = body.match(/(?:^|\\n)## Compiled Truth\\s*\\n([\\s\\S]*?)(?=\\n## |\\n*$)/);\n const intro = ctMatch ? ctMatch[1].trim().slice(0, 200) : body.trim().slice(0, 200);\n\n return `${title}: ${intro}`;\n}\n","/**\n * vectordb/schema.ts — sqlite schema (DDL) + 数据库打开 + 动态加载 sqlite-vec\n *\n * 批次 22a strangler fig 第一步:从 src/lib/vectordb.ts copy 出 schema 层。\n * 原 vectordb.ts 同名定义仍保留,commands/*.ts 暂未切换,本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 常量:EMBEDDING_DIM / MODE_THRESHOLD_FILES\n * - 类型:Db / StatusInfo / QueryResult\n * - DDL:4 个 SQL 表 + 3 个 vec0 虚表 + 3 个 fts5 虚表\n * - 入口:loadSqlite() 动态加载 better-sqlite3 + sqlite-vec;openDb() 一站建库\n *\n * 不含 sync / query / build-layered(后续子批负责)。\n */\n\nimport { existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport type DatabaseNS from 'better-sqlite3';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface QueryResult {\n file: string;\n chunk: string;\n score: number;\n section: string;\n}\n\nexport interface StatusInfo {\n indexed: boolean;\n total_indexable_files?: number;\n indexed_files?: number;\n chunks?: number;\n layered?: { dirs: number; pages: number };\n embedding_dim?: number;\n last_sync?: string | null;\n model?: string | null;\n backend?: string;\n message?: string;\n /**\n * 检索模式推荐(wiki-query skill 直接读这个字段决定路径,不做数值判断)\n * - \"text\" → 走 Read 三层(corpus/index.md → {dir}/_INDEX.md → 具体文件)\n * - \"vector\" → 走向量 layered 召回\n */\n mode?: 'text' | 'vector';\n mode_threshold?: number;\n mode_reason?: string;\n}\n\n// **23c 改**:原 `Db = any` → `DatabaseNS.Database` 精确类型(来自 @types/better-sqlite3\n// 的 namespace 别名 `BetterSqlite3.Database`)。`import type DatabaseNS` 是类型 only,\n// 不引入 runtime 依赖(runtime 仍走 schema.ts 内的 dynamic import 兜可选 dep 缺失)。\n// **重命名 NS 后缀**:避开 loadSqlite() 内部 `let Database = ...` 局部变量 shadowing。\n// 编辑器对 db.prepare/get/run/exec/pragma/close 的智能补全和参数校验恢复正常。\nexport type Db = DatabaseNS.Database;\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 1024;\n\n/**\n * 文本模式 ↔ 向量模式的切换阈值(按 indexed_files 计数,不按 chunks)。\n *\n * 为什么按文档数不按 chunks:chunks 会被单文档长度扭曲(一篇 2 万字可能切出 30+ chunks,\n * 但它仍然只是\"一份材料\")。Karpathy 原文也是按 pages/sources 计数:\n * \"works surprisingly well at moderate scale (~100 sources, ~hundreds of pages)\n * and avoids the need for embedding-based RAG infrastructure.\"\n *\n * 100 = 按 Karpathy 原文的 moderate scale 上界。到这规模之前,Read 三层精度最高;\n * 超过后扁平 Read 太慢,切向量 layered 召回。\n *\n * 先生要调:改这里的数字即可,所有 skill 通过 `lorekit vector status` 读 `mode` 字段\n * 自动跟随。\n */\nexport const MODE_THRESHOLD_FILES = 100;\n\n// 排除 / 包含规则集中在 lib/paths.ts,本文件不再硬编码(CONVENTIONS Do Not #11)。\n\n// ---------------------------------------------------------------------------\n// DDL\n// ---------------------------------------------------------------------------\n\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS documents (\n id INTEGER PRIMARY KEY,\n path TEXT UNIQUE NOT NULL,\n sha256 TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS chunks (\n id INTEGER PRIMARY KEY,\n doc_id INTEGER NOT NULL,\n section TEXT,\n content TEXT NOT NULL,\n embedding BLOB NOT NULL,\n FOREIGN KEY (doc_id) REFERENCES documents(id) ON DELETE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT\n);\n\nCREATE TABLE IF NOT EXISTS dir_summaries (\n id INTEGER PRIMARY KEY,\n dir_path TEXT UNIQUE NOT NULL,\n summary TEXT NOT NULL,\n embedding BLOB NOT NULL,\n slug_list TEXT NOT NULL DEFAULT '[]'\n);\n\nCREATE TABLE IF NOT EXISTS page_summaries (\n id INTEGER PRIMARY KEY,\n doc_id INTEGER NOT NULL REFERENCES documents(id),\n summary TEXT NOT NULL,\n embedding BLOB NOT NULL\n);\n`;\n\n/**\n * 生成 vec0 虚表 DDL。dim 是 embedding 维度(默认 EMBEDDING_DIM=1024 / bge-m3 模型)。\n *\n * 三个虚表平行存在:\n * - vec_chunks:文档切块向量(rowid 对齐 chunks.id)\n * - vec_dirs:目录摘要向量(rowid 对齐 dir_summaries.id)\n * - vec_pages:页摘要向量(rowid 对齐 page_summaries.id)\n *\n * cosine 距离用于召回时按相似度排序。\n */\nexport function vecDdl(dim: number): string {\n return `\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_chunks USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_dirs USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS vec_pages USING vec0(\n embedding float[${dim}] distance_metric=cosine\n);\n`;\n}\n\n/**\n * FTS5 虚表 DDL:跟 vec_* 平行存在于同一份 vector.sqlite 里。\n * tokenize='trigram':对中文友好(每 3 字符滑动窗口),专有名词和日期精确命中。\n * rowid 跟对应 SQL 表的 id 对齐(chunks.id / dir_summaries.id / page_summaries.id)。\n */\nexport const FTS_DDL = `\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_chunks USING fts5(\n content,\n tokenize='trigram'\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_dirs USING fts5(\n summary,\n tokenize='trigram'\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS fts_pages USING fts5(\n summary,\n tokenize='trigram'\n);\n`;\n\n// ---------------------------------------------------------------------------\n// Dynamic import helper\n// ---------------------------------------------------------------------------\n\n/**\n * 动态加载 better-sqlite3 + sqlite-vec。两者都是 optionalDependencies,\n * 缺失时抛带安装提示的 Error,由调用方决定怎么报给用户。\n *\n * 单独抽出来是为了让其他模块(query / sync / build-layered)也能复用同一份动态加载,\n * 不重复 try/catch。\n */\nexport async function loadSqlite(): Promise<{\n Database: typeof import('better-sqlite3');\n sqliteVec: { load: (db: Db) => void };\n}> {\n let Database: typeof import('better-sqlite3');\n try {\n Database = (await import('better-sqlite3'))\n .default as unknown as typeof import('better-sqlite3');\n } catch {\n throw new Error(\n 'better-sqlite3 is required for the vector engine.\\n' +\n ' Install it: npm install better-sqlite3',\n );\n }\n\n let sqliteVec: { load: (db: Db) => void };\n try {\n const vecMod = await import('sqlite-vec');\n sqliteVec = vecMod as unknown as { load: (db: Db) => void };\n } catch {\n throw new Error(\n 'sqlite-vec is required for the vector engine.\\n' + ' Install it: npm install sqlite-vec',\n );\n }\n\n return { Database, sqliteVec };\n}\n\n// ---------------------------------------------------------------------------\n// openDb — 一站建库 + DDL 应用 + migration\n// ---------------------------------------------------------------------------\n\n/**\n * 打开(必要时创建)`<corpus>/.wiki/vector.sqlite`,应用所有 DDL,加载 sqlite-vec。\n *\n * 副作用:\n * - 创建 `<corpus>/.wiki/` 目录(若不存在)\n * - WAL 模式 + 外键开启\n * - migration:dir_summaries.slug_list 字段(21 之前老库可能缺)补 ALTER TABLE\n *\n * 调用方拿到 Db 之后是同步 better-sqlite3 句柄,可直接 prepare/exec/transaction。\n */\nexport async function openDb(corpus: string, dim = EMBEDDING_DIM): Promise<Db> {\n const { Database, sqliteVec } = await loadSqlite();\n\n const wikiDir = join(corpus, '.wiki');\n if (!existsSync(wikiDir)) mkdirSync(wikiDir, { recursive: true });\n\n const dbPath = join(wikiDir, 'vector.sqlite');\n // `Database` 的实际 runtime 值是 `BetterSqlite3.DatabaseConstructor`(可 new 的\n // class);dynamic import 拿回的 `default` 已是构造器本体,直接 new 即可。\n const db = new Database(dbPath);\n sqliteVec.load(db);\n\n db.pragma('journal_mode = WAL');\n db.pragma('foreign_keys = ON');\n db.exec(DDL);\n db.exec(vecDdl(dim));\n db.exec(FTS_DDL);\n\n // Migration: dir_summaries.slug_list 字段是后加的,老库要补。\n // 数据每次 buildLayeredIndex 都会全量重建,ALTER 完留空数组即可。\n const dirCols = db.prepare('PRAGMA table_info(dir_summaries)').all() as {\n name: string;\n }[];\n if (!dirCols.some((c) => c.name === 'slug_list')) {\n db.exec(`ALTER TABLE dir_summaries ADD COLUMN slug_list TEXT NOT NULL DEFAULT '[]'`);\n }\n\n return db;\n}\n","/**\n * Ollama embedding client — calls local ollama /api/embed endpoint.\n */\n\nconst OLLAMA_URL = 'http://localhost:11434/api/embed';\nconst DEFAULT_MODEL = 'bge-m3';\n\nexport async function embed(texts: string[], model = DEFAULT_MODEL): Promise<Float32Array[]> {\n const payload = JSON.stringify({ model, input: texts });\n\n let resp: Response;\n try {\n resp = await fetch(OLLAMA_URL, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: payload,\n signal: AbortSignal.timeout(120_000),\n });\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e);\n throw new Error(\n `Cannot connect to ollama at ${OLLAMA_URL}: ${msg}\\n` +\n ` Make sure ollama is running: ollama serve\\n` +\n ` And the model is pulled: ollama pull ${model}`,\n );\n }\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => '');\n throw new Error(`ollama returned ${resp.status}: ${body}`);\n }\n\n const data = (await resp.json()) as { embeddings?: number[][] };\n const embeddings = data.embeddings ?? [];\n return embeddings.map((e) => new Float32Array(e));\n}\n\nexport async function embedSingle(text: string, model = DEFAULT_MODEL): Promise<Float32Array> {\n const results = await embed([text], model);\n return results[0];\n}\n","/**\n * Markdown file chunker — splits .md files into embeddable chunks.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { basename, relative } from 'node:path';\nimport matter from 'gray-matter';\n\nconst MAX_CHUNK_CHARS = 800;\nconst MIN_CHUNK_CHARS = 20;\n\nexport interface Chunk {\n section: string;\n content: string;\n}\n\nexport function chunkFile(filePath: string, corpusRoot: string): Chunk[] {\n const raw = readFileSync(filePath, 'utf-8');\n const { data: fm, content: body } = matter(raw);\n\n let title = (fm.title as string) || '';\n const type = (fm.type as string) || '';\n\n if (!title) {\n const m = body.match(/^#\\s+(.+)/m);\n title = m ? m[1].trim() : basename(filePath, '.md');\n }\n\n // Split body by ## headings\n const parts = body.split(/^(## .+)$/m);\n\n const sections: Array<[string, string]> = [];\n if (parts[0].trim()) {\n sections.push(['_intro', parts[0]]);\n }\n for (let i = 1; i < parts.length - 1; i += 2) {\n const heading = parts[i].replace(/^#+\\s*/, '').trim();\n const secBody = i + 1 < parts.length ? parts[i + 1] : '';\n sections.push([heading, secBody]);\n }\n\n let prefix = '';\n if (title) prefix += `[${title}] `;\n if (type) prefix += `[${type}] `;\n\n const chunks: Chunk[] = [];\n\n for (const [heading, secBody] of sections) {\n const trimmed = secBody.trim();\n if (!trimmed || trimmed.length < MIN_CHUNK_CHARS) continue;\n\n if (trimmed.length > MAX_CHUNK_CHARS) {\n const paragraphs = trimmed.split('\\n\\n');\n let current = '';\n for (const p of paragraphs) {\n if (current.length + p.length > MAX_CHUNK_CHARS && current) {\n chunks.push({ section: heading, content: prefix + current.trim() });\n current = p;\n } else {\n current = current ? current + '\\n\\n' + p : p;\n }\n }\n if (current.trim()) {\n chunks.push({ section: heading, content: prefix + current.trim() });\n }\n } else {\n chunks.push({ section: heading, content: prefix + trimmed });\n }\n }\n\n return chunks;\n}\n","/**\n * vectordb/sync.ts — 单文件增量同步(写路径)\n *\n * 批次 22b strangler fig 第二步:从 src/lib/vectordb.ts copy 出 syncFile。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 拿到一个 corpus 内的 markdown 路径 + db 句柄 + embed 回调,把该文件的 chunks\n * 全量重建(先级联清旧 doc → 重新切块 → embed → 写 documents/chunks/vec_chunks/fts_chunks)\n * - **不**重建 page_summaries / vec_pages / fts_pages(那是 buildLayeredIndex 的活)\n *\n * 调用流程上由 commands/vector.ts 的 sync 子命令在 collectFiles 列表上 batch 调用。\n */\n\nimport { relative } from 'node:path';\n\nimport { sha256, float32ToBuffer } from './files.js';\nimport type { Db } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * embedding 回调签名。inline 定义保持 22b 不依赖未上提的共享类型;22e/22f 收尾时\n * 若 query 系列子批也用到再考虑上提到 schema.ts(当前 grep 仅 sync/build-layered 用到)。\n */\ntype EmbedFn = (texts: string[]) => Promise<Float32Array[]>;\n\n// ---------------------------------------------------------------------------\n// syncFile\n// ---------------------------------------------------------------------------\n\n/**\n * 单文件全量重建:\n * 1. 查 documents.path == rel 的旧 doc,存在则先级联清 chunks/vec_chunks/fts_chunks\n * + page_summaries/vec_pages/fts_pages,再删 documents\n * - **顺序敏感**:page_summaries 有 FK ON documents(id),必须先于 documents 删\n * - vec_* / fts_* 是 sqlite-vec / FTS5 的虚表,rowid 跟对应 SQL 表 id 对齐,\n * 手动 DELETE WHERE rowid = ?;不能依赖外键级联\n * 2. INSERT documents(path / sha256 / updated_at)\n * 3. chunkFile() 切块;空 chunks 直接返回\n * 4. embedFn(texts) 批量 embed\n * 5. 逐 chunk INSERT chunks → 拿 last_insert_rowid → INSERT vec_chunks(rowid) 同 id\n * → INSERT fts_chunks(rowid) 同 id\n *\n * 副作用:仅写入;返回 `{ chunks: number }` 让调用方汇总。\n */\nexport async function syncFile(\n db: Db,\n filePath: string,\n corpus: string,\n embedFn: EmbedFn,\n): Promise<{ chunks: number }> {\n const { chunkFile } = await import('../chunker.js');\n\n const rel = relative(corpus, filePath);\n const sha = sha256(filePath);\n\n // Remove old data — chunks / page_summaries 都引用 documents.id,要级联清;\n // 每张 virtual table(vec_*, fts_*)的 rowid 跟对应 SQL 表的 id 一一对应。\n const old = db.prepare('SELECT id FROM documents WHERE path = ?').get(rel) as\n | { id: number }\n | undefined;\n if (old) {\n // 1. chunks + vec_chunks + fts_chunks\n const chunkIds = db.prepare('SELECT id FROM chunks WHERE doc_id = ?').all(old.id) as {\n id: number;\n }[];\n const delVecChunk = db.prepare('DELETE FROM vec_chunks WHERE rowid = ?');\n const delFtsChunk = db.prepare('DELETE FROM fts_chunks WHERE rowid = ?');\n for (const { id } of chunkIds) {\n delVecChunk.run(id);\n delFtsChunk.run(id);\n }\n db.prepare('DELETE FROM chunks WHERE doc_id = ?').run(old.id);\n\n // 2. page_summaries + vec_pages + fts_pages(外键拦 documents 删除,必须先清)\n const pageIds = db.prepare('SELECT id FROM page_summaries WHERE doc_id = ?').all(old.id) as {\n id: number;\n }[];\n const delVecPage = db.prepare('DELETE FROM vec_pages WHERE rowid = ?');\n const delFtsPage = db.prepare('DELETE FROM fts_pages WHERE rowid = ?');\n for (const { id } of pageIds) {\n delVecPage.run(id);\n delFtsPage.run(id);\n }\n db.prepare('DELETE FROM page_summaries WHERE doc_id = ?').run(old.id);\n\n // 3. 最后删 documents\n db.prepare('DELETE FROM documents WHERE id = ?').run(old.id);\n }\n\n const now = new Date().toISOString();\n db.prepare('INSERT INTO documents (path, sha256, updated_at) VALUES (?, ?, ?)').run(\n rel,\n sha,\n now,\n );\n const docRow = db.prepare('SELECT id FROM documents WHERE path = ?').get(rel) as {\n id: number;\n };\n const docId = docRow.id;\n\n const chunks = chunkFile(filePath, corpus);\n if (chunks.length === 0) return { chunks: 0 };\n\n const texts = chunks.map((c) => c.content);\n const embeddings = await embedFn(texts);\n\n const insertChunk = db.prepare(\n 'INSERT INTO chunks (doc_id, section, content, embedding) VALUES (?, ?, ?, ?)',\n );\n const insertFts = db.prepare('INSERT INTO fts_chunks(rowid, content) VALUES (?, ?)');\n for (let i = 0; i < chunks.length; i++) {\n const blob = float32ToBuffer(embeddings[i]);\n insertChunk.run(docId, chunks[i].section, chunks[i].content, blob);\n const chunkId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n // vec0 doesn't support bound params for rowid — must inline\n db.prepare(`INSERT INTO vec_chunks (rowid, embedding) VALUES (${chunkId}, ?)`).run(blob);\n insertFts.run(chunkId, chunks[i].content);\n }\n\n return { chunks: chunks.length };\n}\n","/**\n * vectordb/build-layered-index.ts — L0/L1 分层索引重建(写路径)\n *\n * 批次 22b strangler fig 第二步:从 src/lib/vectordb.ts copy 出 buildLayeredIndex\n * + 3 个内部 helper(parseIndexSections / parseIndexEntries / findAllIndexFiles)。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - L0: 从 `corpus/index.md` 按 `## section` 切,每段一条 vec_dirs / fts_dirs 行\n * - L1: 从所有 `{dir}/_INDEX.md` 表格行解析,每条目一条 vec_pages / fts_pages 行\n * (summary 入向量;slug + summary 入 FTS)\n * - 重建是\"全量替换\":先 DELETE FROM dir_summaries / vec_dirs / fts_dirs / page_*\n *\n * 依赖 syncFile 已先跑过:因为 L1 用 `slug → doc_id` 映射,doc_id 来自 documents 表\n * (sync 阶段建立)。typical commands/vector.ts sync 流程:collectFiles → syncFile loop\n * → buildLayeredIndex。\n *\n * **23a 改动**:7 处进度提示 `console.log` → `logger.info`(stderr,不污染\n * `lorekit sync | jq` 管道)。findAllIndexFiles 的沉默 catch → `logger.warn` +\n * 注释说明为何可以继续。对应 LEGACY P2-2 / P2-4 vectordb 残留清零。\n */\n\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\n\nimport matter from 'gray-matter';\n\nimport { vectorExcludePrefixes } from '../paths.js';\nimport * as logger from '../../utils/logger.js';\nimport { float32ToBuffer } from './files.js';\nimport type { Db } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * embedding 回调签名。inline 同 sync.ts;22e/22f 收尾决定是否上提到 schema.ts。\n */\ntype EmbedFn = (texts: string[]) => Promise<Float32Array[]>;\n\n// ---------------------------------------------------------------------------\n// parseIndexSections — 解析 corpus/index.md 的 ## 分区\n// ---------------------------------------------------------------------------\n\n/**\n * 把 `corpus/index.md` 按 `## section name` 切成多段。\n *\n * 仅返回\"含至少一行 `- [[xxx]]` 或 `* [[xxx]]` 条目\"的分区——纯说明性段(如\n * \"## How to use this wiki\" 不含 wikilink 列表)会被滤掉,不进 L0 向量。\n *\n * 每段返回:\n * - `name`:section 标题(去 ## 与首尾空白)\n * - `text`:完整段文本(含 `## name` 行 + 后续所有行 join('\\n').trim())\n * - `slugs`:去重后的主 slug 列表(每行 `- [[slug]]` 或 `* [[slug]]` 取 wikilink 第一个)\n */\nfunction parseIndexSections(\n content: string,\n): Array<{ name: string; text: string; slugs: string[] }> {\n const lines = content.split('\\n');\n const sections: Array<{ name: string; lines: string[] }> = [];\n let current: { name: string; lines: string[] } | null = null;\n\n for (const line of lines) {\n const m = line.match(/^##\\s+(.+?)\\s*$/);\n if (m) {\n if (current) sections.push(current);\n current = { name: m[1].trim(), lines: [line] };\n } else if (current) {\n current.lines.push(line);\n }\n }\n if (current) sections.push(current);\n\n // 每条目主 slug:行首 `- [[slug]]`(或 `* [[slug]]`),只取第一个 wikilink\n const entrySlugRe = /^\\s*[-*]\\s*\\[\\[([^\\]|#]+?)\\]\\]/;\n\n return sections\n .filter((s) => /^\\s*[-*]\\s/m.test(s.lines.slice(1).join('\\n')))\n .map((s) => {\n const slugs: string[] = [];\n for (const line of s.lines.slice(1)) {\n const m = line.match(entrySlugRe);\n if (m) slugs.push(m[1].trim());\n }\n return {\n name: s.name,\n text: s.lines.join('\\n').trim(),\n slugs: [...new Set(slugs)],\n };\n });\n}\n\n// ---------------------------------------------------------------------------\n// parseIndexEntries — 解析 {dir}/_INDEX.md 的表格行\n// ---------------------------------------------------------------------------\n\n/**\n * 解析 `{dir}/_INDEX.md` 表格行:`| [[slug]] | summary | updated |`\n * 跳过表头(含\"条目\"二字)和分隔行(全是 - 和 |)\n */\nfunction parseIndexEntries(content: string): Array<{ slug: string; summary: string }> {\n const lines = content.split('\\n');\n const entries: Array<{ slug: string; summary: string }> = [];\n\n for (const line of lines) {\n if (/^\\|\\s*条目\\s*\\|/.test(line)) continue;\n if (/^\\|[\\s\\-|]+\\|?\\s*$/.test(line)) continue;\n\n const m = line.match(/^\\|\\s*\\[\\[([^\\]|#]+?)\\]\\]\\s*\\|\\s*(.*?)\\s*\\|\\s*(.*?)\\s*\\|/);\n if (!m) continue;\n const slug = m[1].trim();\n const summary = m[2].replace(/\\\\\\|/g, '|').trim();\n entries.push({ slug, summary });\n }\n\n return entries;\n}\n\n// ---------------------------------------------------------------------------\n// findAllIndexFiles — 递归找所有 _INDEX.md\n// ---------------------------------------------------------------------------\n\n/**\n * 递归扫 corpus 找所有 `_INDEX.md`,复用 paths.ts 的 vectorExcludePrefixes 排除规则。\n * 同时跳过 `.` 起头的目录(.git / .wiki / .obsidian 等约定隐藏目录)。\n */\nfunction findAllIndexFiles(corpus: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string) {\n let entries;\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch (e) {\n // 目录读不到(权限 / 临时被删 / 扫到 symlink 循环)就跳,整体扫描继续。\n logger.warn(`findAllIndexFiles: skip ${dir} (${(e as Error).message})`);\n return;\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue;\n const full = join(dir, entry.name);\n const rel = relative(corpus, full);\n if (vectorExcludePrefixes.some((p) => rel === p || rel.startsWith(p + '/'))) continue;\n\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name === '_INDEX.md') {\n results.push(full);\n }\n }\n }\n walk(corpus);\n return results.sort();\n}\n\n// ---------------------------------------------------------------------------\n// buildLayeredIndex — L0 + L1 全量重建\n// ---------------------------------------------------------------------------\n\nexport async function buildLayeredIndex(db: Db, corpus: string, embedFn: EmbedFn): Promise<void> {\n // --- L0: 从 corpus/index.md 按 ## 分区切,每区一条向量 + 一条 FTS ---\n db.prepare('DELETE FROM dir_summaries').run();\n db.prepare('DELETE FROM vec_dirs').run();\n db.prepare('DELETE FROM fts_dirs').run();\n\n const indexPath = join(corpus, 'index.md');\n if (!existsSync(indexPath)) {\n logger.info(' L0: corpus/index.md not found, skipped');\n } else {\n const raw = readFileSync(indexPath, 'utf-8');\n const { content } = matter(raw);\n const sections = parseIndexSections(content);\n\n if (sections.length === 0) {\n logger.info(' L0: no sections with entries in index.md, skipped');\n } else {\n const texts = sections.map((s) => s.text);\n const embeddings = await embedFn(texts);\n\n const insertDir = db.prepare(\n 'INSERT INTO dir_summaries (dir_path, summary, embedding, slug_list) VALUES (?, ?, ?, ?)',\n );\n const insertFtsDir = db.prepare('INSERT INTO fts_dirs(rowid, summary) VALUES (?, ?)');\n for (let i = 0; i < sections.length; i++) {\n const blob = float32ToBuffer(embeddings[i]);\n const slugListJson = JSON.stringify(sections[i].slugs);\n insertDir.run(sections[i].name, sections[i].text, blob, slugListJson);\n const dirId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n db.prepare(`INSERT INTO vec_dirs (rowid, embedding) VALUES (${dirId}, ?)`).run(blob);\n insertFtsDir.run(dirId, sections[i].text);\n }\n const totalSlugs = sections.reduce((a, s) => a + s.slugs.length, 0);\n logger.info(\n ` L0: indexed ${sections.length} sections from index.md (${totalSlugs} slugs tracked)`,\n );\n }\n }\n\n // --- L1: 从各 _INDEX.md 的每行条目,每条一条向量 + 一条 FTS ---\n db.prepare('DELETE FROM page_summaries').run();\n db.prepare('DELETE FROM vec_pages').run();\n db.prepare('DELETE FROM fts_pages').run();\n\n const indexFiles = findAllIndexFiles(corpus);\n if (indexFiles.length === 0) {\n logger.info(' L1: no _INDEX.md found, skipped');\n return;\n }\n\n const allEntries: Array<{ slug: string; summary: string }> = [];\n for (const f of indexFiles) {\n const raw = readFileSync(f, 'utf-8');\n allEntries.push(...parseIndexEntries(raw));\n }\n\n if (allEntries.length === 0) {\n logger.info(' L1: no entries parsed from _INDEX.md, skipped');\n return;\n }\n\n // 建 slug → doc_id 映射(兼容目录包装式和去/不去 .md 后缀)\n const docRows = db.prepare('SELECT id, path FROM documents').all() as {\n id: number;\n path: string;\n }[];\n const slugToDocId = new Map<string, number>();\n for (const { id, path } of docRows) {\n slugToDocId.set(path, id);\n slugToDocId.set(path.replace(/\\.md$/, ''), id);\n if (path.endsWith('/article.md')) {\n slugToDocId.set(path.replace(/\\/article\\.md$/, ''), id);\n }\n }\n\n const matched: Array<{ docId: number; text: string; slug: string }> = [];\n let unmatched = 0;\n for (const e of allEntries) {\n const docId = slugToDocId.get(e.slug);\n if (docId === undefined) {\n unmatched++;\n continue;\n }\n // 向量输入用 summary;summary 缺失时退回 slug(至少有语义路径)\n const text =\n e.summary && e.summary !== '—' && e.summary !== '(缺少 frontmatter)' ? e.summary : e.slug;\n matched.push({ docId, text, slug: e.slug });\n }\n\n if (matched.length === 0) {\n logger.info(' L1: no _INDEX.md entries matched documents, skipped');\n return;\n }\n\n const BATCH = 64;\n const insertPage = db.prepare(\n 'INSERT INTO page_summaries (doc_id, summary, embedding) VALUES (?, ?, ?)',\n );\n const insertFtsPage = db.prepare('INSERT INTO fts_pages(rowid, summary) VALUES (?, ?)');\n for (let i = 0; i < matched.length; i += BATCH) {\n const batch = matched.slice(i, i + BATCH);\n const texts = batch.map((m) => m.text);\n const embeddings = await embedFn(texts);\n for (let j = 0; j < batch.length; j++) {\n const blob = float32ToBuffer(embeddings[j]);\n insertPage.run(batch[j].docId, batch[j].text, blob);\n const pageId = Number(\n (db.prepare('SELECT last_insert_rowid() as id').get() as { id: bigint }).id,\n );\n db.prepare(`INSERT INTO vec_pages (rowid, embedding) VALUES (${pageId}, ?)`).run(blob);\n // FTS 索引内容 = slug + summary,让 BM25 也能通过路径(含实体名)命中;\n // 向量只索引 summary,保持语义纯净不被路径噪声污染。\n insertFtsPage.run(pageId, `${batch[j].slug} ${batch[j].text}`);\n }\n }\n\n let msg = ` L1: indexed ${matched.length} entries from ${indexFiles.length} _INDEX.md`;\n if (unmatched > 0) msg += ` (${unmatched} unmatched slug, skipped)`;\n logger.info(msg);\n}\n","/**\n * vectordb/query-flat.ts — 单层向量召回(最简实现,对照 layered 的 baseline)\n *\n * 批次 22c strangler fig 第三步:从 src/lib/vectordb.ts copy 出 queryFlat。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - 单一 vec_chunks 表 ANN top-K 召回\n * - threshold 过滤后 JOIN documents 拿 file path 与 section 元数据\n * - 不做分层、不做 parent boost、不做 BM25 融合(那些在 query-layered / query-bm25 /\n * query-hybrid 各自负责)\n *\n * 适用:调试 / 小语料 / 不想要 layered 复杂逻辑时;commands/vector.ts 的\n * `vector query` 默认走 queryHybrid,flat 通过显式 flag 触发。\n */\n\nimport { distanceToScore, float32ToBuffer } from './files.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// queryFlat\n// ---------------------------------------------------------------------------\n\n/**\n * 单层向量 query:\n * - vec_chunks MATCH ? ORDER BY distance LIMIT topK\n * - 距离 → 相似度(distanceToScore:1 - d²/2)\n * - 低于 threshold 的 chunk 丢弃\n * - 用 chunks.id JOIN documents 拿 path / section\n *\n * 返回的 QueryResult.score 保留 4 位小数(`Math.round(score * 10000) / 10000`),\n * 跟 layered / bm25 / hybrid 的输出保持一致便于下游对比。\n */\nexport function queryFlat(\n db: Db,\n embedding: Float32Array,\n topK: number,\n threshold: number,\n): QueryResult[] {\n const blob = float32ToBuffer(embedding);\n\n const rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_chunks v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, topK) as { id: number; distance: number }[];\n\n const results: QueryResult[] = [];\n const getChunk = db.prepare(\n `SELECT c.content, c.section, d.path\n FROM chunks c JOIN documents d ON c.doc_id = d.id\n WHERE c.id = ?`,\n );\n\n for (const row of rows) {\n const score = distanceToScore(row.distance);\n if (score < threshold) continue;\n const cr = getChunk.get(row.id) as\n | { content: string; section: string; path: string }\n | undefined;\n if (cr) {\n results.push({\n file: cr.path,\n chunk: cr.content,\n score: Math.round(score * 10000) / 10000,\n section: cr.section,\n });\n }\n }\n\n return results;\n}\n","/**\n * vectordb/query-layered.ts — L0/L1/L2 三层向量召回\n *\n * 批次 22c strangler fig 第三步:从 src/lib/vectordb.ts copy 出 queryLayered。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 三层结构(基于 buildLayeredIndex 建立的索引):\n *\n * - **L0** vec_dirs:top-3 sections(index.md 的 ## 分区向量)\n * → 用 dir_summaries.slug_list 收集\"覆盖到的所有候选 slug\"\n *\n * - **L1** vec_pages:在 L0 候选 doc_id 范围内召回 top-5 pages\n * → searchK = min(候选页数, 50);vec0 只能限 K 不能限范围,先全召回再 filter\n *\n * - **L2** vec_chunks:在 L1 命中页的 chunks 范围内召回 topK\n * → searchK2 = min(候选 chunks 数, topK*5);同样先召回再 filter\n *\n * 每层任意阶段命中为空 → 短路返回 `[]`(不退化到全库 flat 召回)。\n *\n * 设计取舍:分层把\"语义召回\"分阶段约束在\"结构相关\"的子集,避免大库 ANN\n * 误命中冷门文档。代价是命中阈值越低 / 库规模越小时,可能比 flat 漏召。\n */\n\nimport { distanceToScore, float32ToBuffer } from './files.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// queryLayered\n// ---------------------------------------------------------------------------\n\nexport function queryLayered(\n db: Db,\n embedding: Float32Array,\n topK: number,\n threshold: number,\n): QueryResult[] {\n const blob = float32ToBuffer(embedding);\n\n // **23c 改:cap 联动 topK** —— 3/5 是 Karpathy 原文 baseline(小 topK 精度优先),\n // topK 上来时按比例放大避免过早收窄。topK=10 默认:L0_K=3, L1_CAP=5(向后兼容)。\n // topK=100 时:L0_K=10, L1_CAP=20,让 L0 覆盖更多分区 / L1 保留更多候选页。\n const L0_K = Math.max(3, Math.ceil(topK / 10));\n const L1_CAP = Math.max(5, Math.ceil(topK / 5));\n\n // L0: top-L0_K sections(分区向量,每个分区 = index.md 里一个 ## 区块)\n const l0Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_dirs v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, L0_K) as { id: number; distance: number }[];\n\n if (l0Rows.length === 0) return [];\n\n // 从 dir_summaries.slug_list 拿 L0 命中分区覆盖的所有 slug\n const dirIds = l0Rows.map((r) => r.id);\n const dirRows = db\n .prepare(`SELECT slug_list FROM dir_summaries WHERE id IN (${dirIds.map(() => '?').join(',')})`)\n .all(...dirIds) as { slug_list: string }[];\n\n const candidateSlugs = new Set<string>();\n for (const row of dirRows) {\n try {\n const list = JSON.parse(row.slug_list) as string[];\n for (const s of list) candidateSlugs.add(s);\n } catch {\n // slug_list 异常(老库未迁移时可能是空串)→ 跳过\n }\n }\n\n if (candidateSlugs.size === 0) return [];\n\n // 把 slug 映射成 doc_id(兼容目录包装式 slug、去/不去 .md 后缀)\n // **23c 改:变量 rename** `candidateDocIds` → `L0CandidateDocIds` 按层语义命名\n const docRows = db.prepare('SELECT id, path FROM documents').all() as {\n id: number;\n path: string;\n }[];\n const L0CandidateDocIds = new Set<number>();\n for (const { id, path } of docRows) {\n const stem = path.replace(/\\.md$/, '');\n const folderSlug = path.endsWith('/article.md') ? path.replace(/\\/article\\.md$/, '') : null;\n if (candidateSlugs.has(path) || candidateSlugs.has(stem)) {\n L0CandidateDocIds.add(id);\n } else if (folderSlug && candidateSlugs.has(folderSlug)) {\n L0CandidateDocIds.add(id);\n }\n }\n\n if (L0CandidateDocIds.size === 0) return [];\n\n // L1: top-L1_CAP pages,候选限定在 L0 命中分区覆盖的 doc_id\n // **23c 改:变量 rename** `docIdArr` → `L0CandidateDocIdArr`(L0 doc_id 的 array view)\n const L0CandidateDocIdArr = [...L0CandidateDocIds];\n const candidatePageIds = db\n .prepare(\n `SELECT id FROM page_summaries WHERE doc_id IN (${L0CandidateDocIdArr.map(() => '?').join(',')})`,\n )\n .all(...L0CandidateDocIdArr) as { id: number }[];\n\n if (candidatePageIds.length === 0) return [];\n\n const searchK = Math.min(candidatePageIds.length, 50);\n const l1Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_pages v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, searchK) as { id: number; distance: number }[];\n\n const candidateSet = new Set(candidatePageIds.map((r) => r.id));\n const l1Filtered = l1Rows.filter((r) => candidateSet.has(r.id)).slice(0, L1_CAP);\n\n if (l1Filtered.length === 0) return [];\n\n // Get doc_ids from matched page summaries\n // **23c 改:变量 rename** `docIds` → `L1CandidateDocIds`(L1 命中页对应的 doc_id 对象数组)\n const pageIds = l1Filtered.map((r) => r.id);\n const L1CandidateDocIds = db\n .prepare(\n `SELECT DISTINCT doc_id FROM page_summaries WHERE id IN (${pageIds.map(() => '?').join(',')})`,\n )\n .all(...pageIds) as { doc_id: number }[];\n\n if (L1CandidateDocIds.length === 0) return [];\n\n // L2: chunks within matched docs\n // **23c 改:变量 rename** `docIdList` → `L1CandidateDocIdList`(plain number[] 形式)\n const L1CandidateDocIdList = L1CandidateDocIds.map((r) => r.doc_id);\n const candidateChunkIds = db\n .prepare(\n `SELECT id FROM chunks WHERE doc_id IN (${L1CandidateDocIdList.map(() => '?').join(',')})`,\n )\n .all(...L1CandidateDocIdList) as { id: number }[];\n\n if (candidateChunkIds.length === 0) return [];\n\n const searchK2 = Math.min(candidateChunkIds.length, topK * 5);\n const l2Rows = db\n .prepare(\n `SELECT v.rowid as id, v.distance\n FROM vec_chunks v\n WHERE v.embedding MATCH ? AND k = ?\n ORDER BY v.distance`,\n )\n .all(blob, searchK2) as { id: number; distance: number }[];\n\n const chunkSet = new Set(candidateChunkIds.map((r) => r.id));\n const l2Filtered = l2Rows.filter((r) => chunkSet.has(r.id)).slice(0, topK);\n\n const results: QueryResult[] = [];\n const getChunk = db.prepare(\n `SELECT c.content, c.section, d.path\n FROM chunks c JOIN documents d ON c.doc_id = d.id\n WHERE c.id = ?`,\n );\n\n for (const row of l2Filtered) {\n const score = distanceToScore(row.distance);\n if (score < threshold) continue;\n const cr = getChunk.get(row.id) as\n | { content: string; section: string; path: string }\n | undefined;\n if (cr) {\n results.push({\n file: cr.path,\n chunk: cr.content,\n score: Math.round(score * 10000) / 10000,\n section: cr.section,\n });\n }\n }\n\n return results;\n}\n","/**\n * vectordb/query-bm25.ts — BM25(FTS5)chunk 层直查\n *\n * **批次 24-fix(2026-04-19)重写**:原 layered 三层 (fts_dirs → fts_pages →\n * fts_chunks) 在 21 引入时设计错误——L0 的 dir 摘要只含目录标题 + wikilink 列表,\n * **不含正文**,用户关键词几乎永不命中 L0;一旦 L0 空集整条链路 `return []`,\n * 导致 `lorekit vector query --bm25 --text \"browser-use\"` 这种最朴素的调用\n * 返回空。这个缺陷隐藏在 hybrid 融合后(向量路补救),22 系列 byte-level 拆分\n * 验证没抓到,22f 真实 ingest 验收时先生发现真实 corpus 里 BM25 永远空才暴露。\n *\n * 方案 X(规划方批准):BM25 不分层,直接 fts_chunks MATCH + rank 排 topK。\n * 依据:\n * - BM25 本身就用 rank 排精度,不需要 L0/L1 预先缩候选集\n * - dir / page 摘要不含正文是架构事实(buildLayeredIndex 写入语义),不是 bug\n * - 向量路的 queryLayered 保留 L0 gate(向量相似度下 L0 能做语义 gate)\n *\n * 函数名 / 签名 / 返回类型全部保留,commands / query-hybrid 无需改 import。\n *\n * 适用:精确实体名 / 日期 / 专有名词召回(\"Harness\" / \"Anthropic\" / \"2026-04-15\")。\n * 中英混合复合短语建议走向量或 hybrid。\n */\n\nimport * as logger from '../../utils/logger.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// sanitizeFtsQuery — FTS5 查询字符串清洗\n// ---------------------------------------------------------------------------\n\n/**\n * FTS5 查询字符串清洗:\n * - 去掉 FTS5 运算符字符(\" * : ^ ( ))和保留关键字(OR/AND/NOT/NEAR)\n * - trigram tokenizer 下短于 3 字符的 token 无法命中,过滤掉\n * - 多 token 之间用空格连接(FTS5 默认 AND 语义)\n *\n * 为什么不用短语搜索(\"...\"):短语要求整条 trigram 序列完全连续匹配,中英混合\n * 或带空格的 query 几乎永远命中不上。默认 AND 更宽松,跟 BM25 的\"精确关键词\"\n * 定位一致——先生查\"Harness\"/\"Anthropic\"这种精确实体名能命中;查\"Harness 五版\n * 演化\"这种复合短语 BM25 空是合理的(语义匹配应该走向量或 Hybrid)。\n *\n * **23b 修**:`\\d{4}-\\d{2}-\\d{2}` 完整 ISO 日期 protect-and-restore,避免被\n * `-` 拆 token 退化为 `2026`。流程:\n * 1. 提取所有 ISO 日期,用 `__DATE0__` / `__DATE1__` 占位符替换(前后空格保证分词)\n * 2. 跑现有 sanitize(占位符 `_` 不在 FTS5 运算符 set 里,整串 9 字符 > 3 通过过滤)\n * 3. 把占位符还原为 `\"YYYY-MM-DD\"`(双引号包裹让 FTS5 当 phrase token,\n * 避免 `-` 被解析成 NOT 前缀)\n *\n * 不识别 `2026/04/15`(`/` 不在原 sanitize 拆分字符里,本来就 OK 不需要保护)。\n * 不识别 `2026-4-15`(年月日不补 0 的非标准格式,避免误识别行内 hyphenated 词如\n * `self-hosted`)。\n */\nfunction sanitizeFtsQuery(q: string): string {\n // 1. protect ISO dates\n const dates: string[] = [];\n const protectedQ = q.replace(/\\d{4}-\\d{2}-\\d{2}/g, (m) => {\n const i = dates.length;\n dates.push(m);\n return ` __DATE${i}__ `;\n });\n\n // 2. 现有 sanitize 流程\n // FTS5 运算符:\" * : ^ ( ) - +(`-` 前缀是 NOT,内部的 `-` 也会让日期类 query 失效)\n let s = protectedQ.replace(/[\"*:^()\\-+]/g, ' ');\n s = s.replace(/\\b(OR|AND|NOT|NEAR)\\b/gi, ' ');\n s = s.replace(/\\s+/g, ' ').trim();\n if (!s) return '';\n const tokens = s.split(' ').filter((t) => t.length >= 3);\n if (tokens.length === 0) return '';\n\n // 3. 还原占位符为 quoted 完整日期(FTS5 phrase syntax)\n const restored = tokens.map((t) => {\n const m = t.match(/^__DATE(\\d+)__$/);\n return m ? `\"${dates[Number(m[1])]}\"` : t;\n });\n return restored.join(' ');\n}\n\n// ---------------------------------------------------------------------------\n// queryBM25Layered — BM25 chunk 层直查(名字保留,语义改为单层)\n// ---------------------------------------------------------------------------\n\n/**\n * BM25 召回:fts_chunks MATCH + rank 排序,topK 截断。\n *\n * **不再走 layered 三层**——L0/L1 的 dir/page 摘要不含正文,永不命中用户关键词\n * (详见文件头注释)。改为对 fts_chunks 直接 MATCH,rank 列就是 BM25 负数,\n * 越小越相关;返回时归一为正数。\n *\n * 命名保留 `queryBM25Layered` 是为了兼容现有 import(query-hybrid / commands/vector /\n * lib/vectordb/index)。后续批次若要改名,整个 import 图一起改。\n *\n * 失败路径:FTS5 对 sanitizeFtsQuery 后的 query 仍可能因边界 token 抛错(纯 trigram\n * 不可分串),catch 后返回 `[]` 让上层 hybrid 优雅降级到纯向量;失败原因走 stderr\n * 便于 debug。\n */\nexport function queryBM25Layered(db: Db, queryText: string, topK: number): QueryResult[] {\n const ftsQ = sanitizeFtsQuery(queryText);\n if (!ftsQ) return [];\n\n let rows: {\n rank: number;\n content: string;\n section: string | null;\n path: string;\n }[] = [];\n try {\n rows = db\n .prepare(\n `SELECT fc.rank as rank, c.content as content, c.section as section, d.path as path\n FROM fts_chunks fc\n JOIN chunks c ON fc.rowid = c.id\n JOIN documents d ON c.doc_id = d.id\n WHERE fc.fts_chunks MATCH ?\n ORDER BY fc.rank\n LIMIT ?`,\n )\n .all(ftsQ, topK) as {\n rank: number;\n content: string;\n section: string | null;\n path: string;\n }[];\n } catch (e) {\n // FTS5 边界 token 失败 → BM25 整体降级为空,上层 hybrid 回退纯向量\n logger.warn(`queryBM25Layered fts5 match: ${(e as Error).message}`);\n return [];\n }\n\n return rows.map((r) => ({\n file: r.path,\n chunk: r.content,\n // FTS5 rank 是负数(越小越相关),取绝对值作为正向分数;归一化留给 RRF\n score: Math.round(-r.rank * 10000) / 10000,\n section: r.section ?? '',\n }));\n}\n","/**\n * vectordb/query-hybrid.ts — 向量 + BM25 双路 RRF 融合\n *\n * 批次 22d strangler fig 第四步:从 src/lib/vectordb.ts copy 出 rrfMerge + queryHybrid。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 这是 4 路 query(flat / layered / bm25 / hybrid)里的\"组合\"端,自身不写 db query:\n * - 调 22c 的 `queryLayered` 拿向量三层结果\n * - 调本批的 `queryBM25Layered` 拿 BM25 三层结果\n * - 用 `rrfMerge` 把两路按 Reciprocal Rank Fusion 合并\n *\n * commands/vector.ts 的 `vector query --hybrid` 走这里;不带 flag 默认也是 hybrid\n * (根据当前 cli.ts 设定)。\n */\n\nimport { createHash } from 'node:crypto';\n\nimport { queryBM25Layered } from './query-bm25.js';\nimport { queryLayered } from './query-layered.js';\nimport type { Db, QueryResult } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// rrfMerge — Reciprocal Rank Fusion\n// ---------------------------------------------------------------------------\n\n/**\n * Reciprocal Rank Fusion — 多路召回结果的排名合并。\n * 公式:score(item) = Σ 1 / (k + rank_i) (rank 从 1 开始,k 默认 60)\n * 在两路都靠前的 item 最终 score 最高。\n *\n * **23b 修**:dedup key 从 `${file}::${chunk.slice(0, 80)}` 改为\n * `${file}::${sha256(chunk).slice(0, 16)}`。\n *\n * 原代码用前 80 字做 dedup,中文长文档 chunk 段首固定开场白时(如\n * \"[title] [type]\\n\\n# title\\n## section\\n...\")两个真实不同的 chunk 前 80 字\n * 可能完全相同 → 被错误合并为 1 条,丢正确召回(22d B6 mock case 复现)。\n *\n * 改用 sha256 前 16 hex char(64 bits)作为 chunk 内容指纹,避碰概率 ≈ 2^-32 全文档级\n * 仍极低,且对 chunk 全文敏感不再依赖前缀重复程度。规划方决策:64 bits 足够,\n * 不用全 64 hex 节省 key 空间。\n */\nexport function rrfMerge(lists: QueryResult[][], topK: number, k: number = 60): QueryResult[] {\n // key = file + chunk sha256 前 16 hex(64 bits 指纹),value = { item, rrf }\n const merged = new Map<string, { item: QueryResult; rrf: number }>();\n for (const list of lists) {\n list.forEach((item, i) => {\n const fingerprint = createHash('sha256').update(item.chunk).digest('hex').slice(0, 16);\n const key = `${item.file}::${fingerprint}`;\n const rrf = 1 / (k + i + 1);\n const prev = merged.get(key);\n if (prev) {\n prev.rrf += rrf;\n } else {\n merged.set(key, { item, rrf });\n }\n });\n }\n return [...merged.values()]\n .sort((a, b) => b.rrf - a.rrf)\n .slice(0, topK)\n .map(({ item, rrf }) => ({\n ...item,\n score: Math.round(rrf * 10000) / 10000,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// queryHybrid — 4 路 dispatcher\n// ---------------------------------------------------------------------------\n\n/**\n * Hybrid 分层召回:向量三层 + BM25 三层 + RRF 融合。\n * 跟 queryLayered 同签名(多一个可选 `k` 参数),可在上层命令里用 `--hybrid` flag 切换。\n * 不做 LLM re-rank(先生决定本轮不上,留给未来)。\n *\n * **23c 改**:新增可选 `k` 参数透传到 rrfMerge 的 RRF 公式常数(默认 60)。\n * 暴露此参数是给实验调优用(小 k 让排名靠前 item 权重更突出,大 k 更平滑),\n * cli 暂不暴露 flag(保持 surface 简洁向后兼容)。\n */\nexport function queryHybrid(\n db: Db,\n embedding: Float32Array,\n queryText: string,\n topK: number,\n threshold: number,\n k?: number,\n): QueryResult[] {\n // 两路各召回 topK * 2 作为候选,给 RRF 足够的排名信息\n const candN = topK * 2;\n const vecResults = queryLayered(db, embedding, candN, threshold);\n const bm25Results = queryBM25Layered(db, queryText, candN);\n return rrfMerge([vecResults, bm25Results], topK, k);\n}\n","/**\n * vectordb/status.ts — 检索模式推荐 + 全库 status 元数据\n *\n * 批次 22e strangler fig 第五步:从 src/lib/vectordb.ts copy 出 computeMode + getStatus。\n * 原 vectordb.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import。22f 才切换 dispatcher 并删旧。\n *\n * 职责:\n * - `computeMode`:纯函数,按 indexed_files 对比 MODE_THRESHOLD_FILES 决定推荐 text / vector\n * - `getStatus`:读路径,打开 db → COUNT 各表 → 读 meta(last_sync/model/dim) → 调\n * collectFiles 拿 total_indexable_files → 调 computeMode 拼 StatusInfo 返回\n *\n * commands/vector.ts 的 `vector status` 子命令直接调 getStatus 拿 JSON 给 wiki-query\n * skill 读 mode 字段决定 text/vector 检索路径。\n */\n\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport * as logger from '../../utils/logger.js';\nimport { collectFiles } from './files.js';\nimport { EMBEDDING_DIM, MODE_THRESHOLD_FILES, openDb } from './schema.js';\nimport type { StatusInfo } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// computeMode — 纯函数:indexed + indexedFiles → mode + reason\n// ---------------------------------------------------------------------------\n\n/**\n * 根据 indexed_files 决定检索模式推荐。\n * - 向量库未建 → text(没得选)\n * - indexed_files < 阈值 → text(Read 三层精度最高)\n * - indexed_files >= 阈值 → vector(扁平 Read 太慢)\n */\nfunction computeMode(\n indexed: boolean,\n indexedFiles: number,\n): { mode: 'text' | 'vector'; reason: string } {\n if (!indexed) {\n return {\n mode: 'text',\n reason: 'vector index not built; text Read is the only option',\n };\n }\n if (indexedFiles < MODE_THRESHOLD_FILES) {\n return {\n mode: 'text',\n reason: `indexed_files=${indexedFiles} < ${MODE_THRESHOLD_FILES}; Read three-tier is sharpest at small scale`,\n };\n }\n return {\n mode: 'vector',\n reason: `indexed_files=${indexedFiles} >= ${MODE_THRESHOLD_FILES}; flat Read too slow, switch to layered vector retrieval`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// getStatus — 读全库元数据,拼 StatusInfo\n// ---------------------------------------------------------------------------\n\n/**\n * 读 `<corpus>/.wiki/vector.sqlite` 元数据:\n * - 不存在 → 返回 `{indexed: false, message, mode: 'text'}`(mode_threshold + mode_reason 仍填)\n * - 存在 → openDb → COUNT documents/chunks/dir_summaries/page_summaries\n * + 读 meta(last_sync/model/dim) + collectFiles 算 total_indexable_files\n * + computeMode 决定 mode / mode_reason\n *\n * **23a 改动**:原沉默 catch(老 db 缺 dir_summaries / page_summaries 表的兼容兜底)\n * 改为 `logger.warn(...)` + 明确注释。dirCount / pageCount 留 0 不阻塞 status 输出。\n */\nexport async function getStatus(corpus: string): Promise<StatusInfo> {\n const dbPath = join(corpus, '.wiki', 'vector.sqlite');\n if (!existsSync(dbPath)) {\n const rec = computeMode(false, 0);\n return {\n indexed: false,\n message: \"No vector index found. Run 'lorekit vector sync' first.\",\n mode: rec.mode,\n mode_threshold: MODE_THRESHOLD_FILES,\n mode_reason: rec.reason,\n };\n }\n\n const db = await openDb(corpus);\n\n const docCount = (db.prepare('SELECT COUNT(*) as n FROM documents').get() as { n: number }).n;\n const chunkCount = (db.prepare('SELECT COUNT(*) as n FROM chunks').get() as { n: number }).n;\n const lastSync = db.prepare(\"SELECT value FROM meta WHERE key = 'last_sync'\").get() as\n | { value: string }\n | undefined;\n const model = db.prepare(\"SELECT value FROM meta WHERE key = 'model'\").get() as\n | { value: string }\n | undefined;\n const dim = db.prepare(\"SELECT value FROM meta WHERE key = 'dim'\").get() as\n | { value: string }\n | undefined;\n\n const totalFiles = collectFiles(corpus).length;\n\n let dirCount = 0;\n let pageCount = 0;\n try {\n dirCount = (db.prepare('SELECT COUNT(*) as n FROM dir_summaries').get() as { n: number }).n;\n pageCount = (db.prepare('SELECT COUNT(*) as n FROM page_summaries').get() as { n: number }).n;\n } catch (e) {\n // 老 db(批次 22 之前的版本)可能缺 dir_summaries / page_summaries 表,\n // 留 dirCount/pageCount=0 不阻塞 status 输出;下次 lorekit sync 会通过 openDb\n // 的 DDL CREATE IF NOT EXISTS 自动建表。\n logger.warn(`getStatus: layered tables missing, treat as 0 (${(e as Error).message})`);\n }\n\n db.close();\n\n const rec = computeMode(true, docCount);\n\n return {\n indexed: true,\n total_indexable_files: totalFiles,\n indexed_files: docCount,\n chunks: chunkCount,\n layered: { dirs: dirCount, pages: pageCount },\n embedding_dim: dim ? parseInt(dim.value, 10) : EMBEDDING_DIM,\n last_sync: lastSync?.value ?? null,\n model: model?.value ?? null,\n backend: 'ollama',\n mode: rec.mode,\n mode_threshold: MODE_THRESHOLD_FILES,\n mode_reason: rec.reason,\n };\n}\n","/**\n * vectordb/index.ts — 子模块对外主入口(barrel re-export)\n *\n * 批次 22e strangler fig 第五步:建主入口模块。本文件**不含 runtime 代码**,\n * 仅 re-export 公开 API;实现散在 22a-22d + 22e 的 9 个子文件里。\n *\n * 22e 阶段:本文件目前未被任何调用方 import;commands/vector.ts 仍 dynamic\n * import 旧 src/lib/vectordb.ts。22f 才切换 + 删旧。\n *\n * **公开 surface**(commands/*.ts 真正用到的 API):\n * - 9 个 value:openDb / syncFile / buildLayeredIndex / collectFiles\n * + queryFlat / queryLayered / queryBM25Layered / queryHybrid + getStatus\n * - 1 个额外算法 export:rrfMerge(commands 暂未用,但作为 hybrid 配套算法暴露)\n * - 2 个常量:EMBEDDING_DIM / MODE_THRESHOLD_FILES\n * - 3 个 type:Db / StatusInfo / QueryResult\n *\n * **不 re-export 的内部 helper**(保持封装,commands 不应直接调用):\n * - sha256 / float32ToBuffer / distanceToScore / shouldIndex / extractPageSummary(files.ts)\n * - sanitizeFtsQuery(query-bm25.ts,私有)\n * - parseIndexSections / parseIndexEntries / findAllIndexFiles(build-layered-index.ts,私有)\n * - DDL / FTS_DDL / vecDdl / loadSqlite(schema.ts,仅 openDb 内部用)\n * - EmbedFn type(sync.ts + build-layered-index.ts 双 inline,commands 自定义实现喂入)\n *\n * **EmbedFn 决策**(22e 拍板):保持双 inline 不上提到 schema.ts。理由:\n * (a) commands 不用 EmbedFn type(commands 自定义实现喂入 syncFile / buildLayeredIndex)\n * (b) 上提引入 sync/build-layered → schema.ts 的额外耦合\n * (c) 仅 2 处 inline,type 定义 1 行 + 注释 2 行,复制成本极小\n */\n\n// ---------------------------------------------------------------------------\n// 公开 value(commands/*.ts 用)\n// ---------------------------------------------------------------------------\n\nexport { openDb } from './schema.js';\nexport { syncFile } from './sync.js';\nexport { buildLayeredIndex } from './build-layered-index.js';\nexport { queryFlat } from './query-flat.js';\nexport { queryLayered } from './query-layered.js';\nexport { queryBM25Layered } from './query-bm25.js';\nexport { queryHybrid, rrfMerge } from './query-hybrid.js';\nexport { getStatus } from './status.js';\nexport { collectFiles } from './files.js';\n\n// ---------------------------------------------------------------------------\n// 公开常量\n// ---------------------------------------------------------------------------\n\nexport { EMBEDDING_DIM, MODE_THRESHOLD_FILES } from './schema.js';\n\n// ---------------------------------------------------------------------------\n// 公开 type\n// ---------------------------------------------------------------------------\n\nexport type { Db, StatusInfo, QueryResult } from './schema.js';\n","#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport Database from 'better-sqlite3';\nimport { findCorpus, collectMdFiles } from './lib/corpus.js';\nimport { debug, print } from './utils/logger.js';\nimport { readVersion } from './utils/fs.js';\n\n// commands\nimport { initCommand } from './commands/init.js';\nimport { doctorCommand } from './commands/doctor.js';\nimport { statsCommand } from './commands/stats.js';\nimport { lintCommand } from './commands/lint.js';\nimport { auditCommand } from './commands/audit.js';\nimport { indexCommand } from './commands/dir-index.js';\nimport { installSkillsCommand } from './commands/install-skills.js';\nimport { snapshotCommand } from './commands/snapshot.js';\nimport { restoreCommand } from './commands/restore.js';\nimport { searchCommand } from './commands/search.js';\nimport { vectorCommand } from './commands/vector.js';\nimport { fetchCommand } from './commands/fetch.js';\nimport { ingestCommand } from './commands/ingest.js';\nimport { syncCommand } from './commands/sync.js';\nimport { obsidianTuneCommand } from './commands/obsidian-tune.js';\nimport { removeCommand } from './commands/remove.js';\nimport { gbrainCommand } from './commands/gbrain.js';\n\nconst version = readVersion();\n\nfunction showBanner() {\n const corpus = findCorpus();\n let pages = '—';\n let indexed = '0';\n let model = '—';\n\n if (corpus) {\n try {\n pages = String(collectMdFiles(corpus).length);\n } catch (e) {\n // banner 是 best-effort 装饰,corpus 扫失败时不阻塞用户操作 — 仅 debug 留痕\n debug(`banner: collectMdFiles failed: ${(e as Error).message}`);\n }\n\n try {\n const dbPath = `${corpus}/.wiki/vector.sqlite`;\n if (existsSync(dbPath)) {\n const db = new Database(dbPath, { readonly: true });\n const cntRow = db.prepare('SELECT COUNT(*) as c FROM documents').get() as\n | { c: number }\n | undefined;\n indexed = String(cntRow?.c ?? 0);\n const row = db.prepare(\"SELECT value FROM meta WHERE key='model'\").get() as\n | { value: string }\n | undefined;\n model = row?.value ?? '—';\n db.close();\n }\n } catch (e) {\n // 向量库读失败(坏文件 / 锁 / native 加载错)不该阻断 banner 显示\n debug(`banner: vector.sqlite read failed: ${(e as Error).message}`);\n }\n }\n\n const short = corpus && corpus.length > 45 ? '...' + corpus.slice(-42) : (corpus ?? '—');\n const B = chalk.blue;\n const BB = chalk.blueBright.bold;\n const C = chalk.cyan;\n const D = chalk.dim;\n const W = chalk.white.bold;\n\n print();\n print(` ${BB('██╗ ██████╗ ██████╗ ███████╗██╗ ██╗██╗████████╗')}`);\n print(` ${BB('██║ ██╔═══██╗██╔══██╗██╔════╝██║ ██╔╝██║╚══██╔══╝')}`);\n print(` ${BB('██║ ██║ ██║██████╔╝█████╗ █████╔╝ ██║ ██║ ')}`);\n print(` ${B('██║ ██║ ██║██╔══██╗██╔══╝ ██╔═██╗ ██║ ██║ ')}`);\n print(` ${B('███████╗╚██████╔╝██║ ██║███████╗██║ ██╗██║ ██║ ')}`);\n print(` ${D('╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ')}`);\n print(` ${D('Personal LLM Wiki Toolkit')} ${C(`v${version}`)}`);\n print();\n print(` ${C('corpus')} ${short}`);\n print(` ${C('pages')} ${pages.padEnd(10)} ${C('indexed')} ${indexed}`);\n if (model !== '—') print(` ${C('model')} ${model}`);\n print();\n print(` ${W('$ lorekit doctor')} 健康检查`);\n print(` ${W('$ lorekit fetch')} 抓取网页`);\n print(` ${W('$ lorekit search')} 搜索`);\n print(` ${W('$ lorekit --help')} 所有命令`);\n print();\n}\n\nconst program = new Command();\n\n// CONVENTIONS #4:commander 默认对 missing arg / unknown command 都退出 1,\n// 跟我们\"参数错→2\"的语义不匹配。改用 exitOverride 拦截后按错误码分类。\nconst ARG_ERROR_CODES = new Set([\n 'commander.missingArgument',\n 'commander.missingMandatoryOptionValue',\n 'commander.invalidArgument',\n 'commander.invalidOptionArgument',\n 'commander.unknownCommand',\n 'commander.unknownOption',\n 'commander.excessArguments',\n]);\nprogram.exitOverride((cmdErr) => {\n // help / version 是正常退出\n if (\n cmdErr.code === 'commander.help' ||\n cmdErr.code === 'commander.version' ||\n cmdErr.code === 'commander.helpDisplayed'\n ) {\n process.exit(0);\n }\n if (ARG_ERROR_CODES.has(cmdErr.code)) {\n process.exit(2);\n }\n process.exit(cmdErr.exitCode || 1);\n});\n\nprogram.name('lorekit').version(version).description('Personal LLM Wiki Toolkit');\n\n// register commands\ninitCommand(program);\ndoctorCommand(program);\nstatsCommand(program);\nlintCommand(program);\nauditCommand(program);\nindexCommand(program);\ninstallSkillsCommand(program);\nsnapshotCommand(program);\nrestoreCommand(program);\nsearchCommand(program);\nvectorCommand(program);\nfetchCommand(program);\ningestCommand(program);\nsyncCommand(program);\nobsidianTuneCommand(program);\nremoveCommand(program);\ngbrainCommand(program);\n\n// no subcommand → show banner\nif (process.argv.length <= 2) {\n showBanner();\n} else {\n program.parse();\n}\n","import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';\nimport { join, dirname, basename } from 'node:path';\nimport matter from 'gray-matter';\nimport { alwaysExcludeNames } from './paths.js';\nimport { debug } from '../utils/logger.js';\n\nexport function findCorpus(startDir?: string): string | null {\n let dir = startDir || process.cwd();\n while (dir !== '/' && dir) {\n if (existsSync(join(dir, '.wiki')) || existsSync(join(dir, 'CLAUDE.md'))) {\n return dir;\n }\n dir = dirname(dir);\n }\n return null;\n}\n\nexport function requireCorpus(startDir?: string): string {\n const corpus = findCorpus(startDir);\n if (!corpus) {\n throw new Error('not inside a corpus (no .wiki/ or CLAUDE.md found)');\n }\n return corpus;\n}\n\nexport interface Frontmatter {\n type?: string;\n title?: string;\n slug?: string;\n created?: string;\n updated?: string | Date;\n [key: string]: unknown;\n}\n\nexport function extractFrontmatter(filePath: string): Frontmatter {\n try {\n const content = readFileSync(filePath, 'utf-8');\n const { data } = matter(content);\n return data as Frontmatter;\n } catch (e) {\n // 文件读不到 / YAML 损坏时返回空对象。在 lint / index 等命令里被大量\n // 调用,warn 会刷屏,所以走 debug;真有异常先生开 LOREKIT_DEBUG=1 复现\n debug(`extractFrontmatter(${filePath}) failed: ${(e as Error).message}`);\n return {};\n }\n}\n\nexport function hasFrontmatter(filePath: string): boolean {\n try {\n const first = readFileSync(filePath, 'utf-8').slice(0, 4);\n return first === '---\\n' || first === '---\\r';\n } catch (e) {\n // 同 extractFrontmatter:批量调用,走 debug\n debug(`hasFrontmatter(${filePath}) failed: ${(e as Error).message}`);\n return false;\n }\n}\n\nexport function extractFrontmatterField(filePath: string, key: string): string | undefined {\n const fm = extractFrontmatter(filePath);\n const val = fm[key];\n return typeof val === 'string' ? val : undefined;\n}\n\n/**\n * Find an existing source page in 原料/ that has the given source_url.\n * Returns the absolute path or null.\n */\nexport function findSourceByUrl(corpus: string, url: string): string | null {\n const sourcesRoot = join(corpus, '原料');\n if (!existsSync(sourcesRoot)) return null;\n for (const mdPath of collectMdFiles(sourcesRoot)) {\n const fm = extractFrontmatter(mdPath);\n if (fm.source_url === url || fm.url === url) return mdPath;\n }\n return null;\n}\n\nexport function collectMdFiles(dir: string, opts?: { excludeIndex?: boolean }): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n function walk(d: string) {\n for (const entry of readdirSync(d, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.name.endsWith('.md') && !alwaysExcludeNames.has(entry.name)) {\n results.push(full);\n }\n }\n }\n\n walk(dir);\n return results.sort();\n}\n","import { createHash } from 'node:crypto';\nimport { readFileSync, statSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { warn } from './logger.js';\n\nexport function sha256(filePath: string): string {\n const content = readFileSync(filePath);\n return createHash('sha256').update(content).digest('hex');\n}\n\nexport function fileMtime(filePath: string): Date {\n return statSync(filePath).mtime;\n}\n\nexport function lorekitRoot(): string {\n const thisFile = fileURLToPath(import.meta.url);\n // tsup bundles everything to dist/cli.js — dirname(thisFile) === dist/,\n // so the package root is one level up.\n return join(dirname(thisFile), '..');\n}\n\nexport function readVersion(): string {\n try {\n return readFileSync(join(lorekitRoot(), 'VERSION'), 'utf-8').trim();\n } catch (e) {\n // VERSION 文件应当随 lorekit 包发布,缺了说明安装环境异常 — 用 warn 不静默\n warn(`VERSION file missing or unreadable: ${(e as Error).message}`);\n return 'unknown';\n }\n}\n","import type { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n cpSync,\n readFileSync,\n writeFileSync,\n statSync,\n} from 'node:fs';\nimport { join, relative, resolve } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport chalk from 'chalk';\nimport { ok, bad, warn, print } from '../utils/logger.js';\nimport { readVersion, lorekitRoot } from '../utils/fs.js';\n\nconst MINIMAL_DIRS = ['原料', '知识库/概念', '知识库/实体', '知识库/摘要', '每日', '系统', '.wiki'];\n\nfunction ask(question: string): Promise<string> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nfunction isDirEmpty(dir: string): boolean {\n if (!existsSync(dir)) return true;\n const entries = readdirSync(dir).filter((n) => n !== '.DS_Store' && n !== '.git');\n return entries.length === 0;\n}\n\n/**\n * Recursively copy files from src to dest, skipping files that already exist.\n *\n * 顶层目录 `.obsidian/` 由 `deployObsidianGraphConfig` 与 `deployObsidianPlugin`\n * 单独处理(safe-write + 用户提示),这里跳过避免重复/越权覆盖。\n */\nfunction copyTemplateFiles(src: string, dest: string, isRoot = true) {\n if (!existsSync(dest)) mkdirSync(dest, { recursive: true });\n\n for (const entry of readdirSync(src, { withFileTypes: true })) {\n // 顶层 `.obsidian/` 单独由 deployObsidian* 处理\n if (isRoot && entry.isDirectory() && entry.name === '.obsidian') continue;\n\n const srcPath = join(src, entry.name);\n const destPath = join(dest, entry.name);\n\n if (entry.isDirectory()) {\n copyTemplateFiles(srcPath, destPath, false);\n } else {\n if (!existsSync(destPath)) {\n mkdirSync(join(destPath, '..'), { recursive: true });\n cpSync(srcPath, destPath);\n }\n }\n }\n}\n\nfunction deployObsidianPlugin(corpusPath: string) {\n const pluginSrc = join(lorekitRoot(), 'plugins', 'obsidian-audit');\n const pluginDest = join(corpusPath, '.obsidian', 'plugins', 'lorekit-audit');\n\n if (!existsSync(pluginSrc)) {\n warn('obsidian-audit plugin not found in lorekit install, skipping');\n return;\n }\n\n mkdirSync(pluginDest, { recursive: true });\n for (const file of readdirSync(pluginSrc)) {\n cpSync(join(pluginSrc, file), join(pluginDest, file));\n }\n ok('deployed obsidian-audit plugin → .obsidian/plugins/lorekit-audit/');\n}\n\n/**\n * safe-write `.obsidian/graph.json`:\n * - 已存在 → 跳过 + stderr 警告(保留用户自定义的 colorGroups / forceGravity 等)\n * - 目标已有 `.obsidian/` 但缺 graph.json → 只写 graph.json\n * - 目标完全没 `.obsidian/` → 建目录 + 写 graph.json\n *\n * 批次 25 引入:把\"lorekit 决定的结构(_工作台 / _INDEX / 系统 ...)\"\n * 对应的 Obsidian graph filter 作为预设内置,用户无需自己发明一遍。\n */\nfunction deployObsidianGraphConfig(corpusPath: string) {\n const src = join(lorekitRoot(), 'templates', 'default-corpus', '.obsidian', 'graph.json');\n if (!existsSync(src)) {\n // 模板缺失不阻塞 init;warn 即可\n warn('templates/default-corpus/.obsidian/graph.json not found, skipping graph config');\n return;\n }\n\n const destDir = join(corpusPath, '.obsidian');\n const dest = join(destDir, 'graph.json');\n\n if (existsSync(dest)) {\n // 绝对不许覆盖用户已有的 graph.json(可能含 colorGroups / forceGravity 等个性化字段)\n warn('.obsidian/graph.json 已存在,跳过写入。推荐 filter 见 docs/QUICKSTART.md');\n return;\n }\n\n if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });\n cpSync(src, dest);\n ok('deployed Obsidian graph filter → .obsidian/graph.json');\n}\n\nfunction createWikiMeta(corpusPath: string) {\n const wikiDir = join(corpusPath, '.wiki');\n mkdirSync(wikiDir, { recursive: true });\n\n const version = readVersion();\n writeFileSync(join(wikiDir, 'version'), version + '\\n');\n\n const configPath = join(wikiDir, 'config.yaml');\n if (!existsSync(configPath)) {\n writeFileSync(\n configPath,\n [\n '# lorekit corpus config',\n `version: \"${version}\"`,\n 'lang: zh-CN',\n 'frontmatter_required: true',\n '',\n ].join('\\n'),\n );\n }\n ok(`created .wiki/version (${version}) + config.yaml`);\n}\n\nexport function initCommand(program: Command) {\n program\n .command('init')\n .argument('[path]', 'target directory', '.')\n .option('--in-place', 'initialize in-place even if directory is non-empty')\n .option('--minimal', 'only create core directories (no template files)')\n .description('initialize a new lorekit corpus')\n .action(async (targetPath: string, opts: { inPlace?: boolean; minimal?: boolean }) => {\n const resolved = resolve(targetPath);\n const templateDir = join(lorekitRoot(), 'templates', 'default-corpus');\n\n if (opts.minimal) {\n // Minimal mode: just create core directories\n for (const dir of MINIMAL_DIRS) {\n mkdirSync(join(resolved, dir), { recursive: true });\n }\n createWikiMeta(resolved);\n ok(`minimal corpus initialized at ${resolved}`);\n return;\n }\n\n if (!isDirEmpty(resolved) && !opts.inPlace) {\n print(chalk.yellow(`\\n target directory is not empty: ${resolved}\\n`));\n const answer = await ask(\n ' [b] backup & init [i] in-place (skip existing) [c] cancel\\n > ',\n );\n\n if (answer === 'c' || answer === 'C' || answer === '') {\n bad('cancelled');\n return;\n }\n if (answer === 'b' || answer === 'B') {\n const backupDir = resolved + '.bak.' + Date.now();\n cpSync(resolved, backupDir, { recursive: true });\n ok(`backed up to ${backupDir}`);\n }\n // answer 'i'/'b' both fall through to in-place copy\n }\n\n // Copy template files (skip existing)\n if (existsSync(templateDir)) {\n copyTemplateFiles(templateDir, resolved);\n ok('template files copied (skipped existing)');\n } else {\n warn('template directory not found, creating minimal structure');\n for (const dir of MINIMAL_DIRS) {\n mkdirSync(join(resolved, dir), { recursive: true });\n }\n }\n\n createWikiMeta(resolved);\n deployObsidianGraphConfig(resolved);\n deployObsidianPlugin(resolved);\n\n print();\n ok(chalk.bold(`corpus initialized at ${resolved}`));\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport chalk from 'chalk';\nimport { ok, bad, warn, print, out } from '../utils/logger.js';\nimport { requireCorpus, collectMdFiles, hasFrontmatter } from '../lib/corpus.js';\nimport { isIndexExcluded, isFolderPackage } from '../lib/paths.js';\nimport {\n getRecommendedFilter,\n readCorpusFilter,\n isFilterComplete,\n} from '../lib/obsidian.js';\nimport {\n doctorGbrain,\n type GbrainDoctorIssue,\n type GbrainDoctorResult,\n} from '../lib/integrations/gbrain.js';\n\nconst EXPECTED_DIRS = [\n '每日',\n '知识库/实体',\n '知识库/概念',\n '知识库/专题',\n '原料',\n '原料/录音',\n '写作',\n '系统',\n '_工作台',\n];\n\ntype DoctorStatus = 'ok' | 'warn' | 'error';\ntype DoctorSectionName =\n | 'directories'\n | 'wikiMetadata'\n | 'frontmatter'\n | 'indexFiles'\n | 'archive'\n | 'obsidian'\n | 'integrations';\n\ninterface DoctorIssue {\n section: DoctorSectionName | 'gbrain';\n severity: 'warn' | 'error';\n message: string;\n recommendation?: string;\n}\n\ninterface DoctorSectionReport {\n status: DoctorStatus;\n [key: string]: unknown;\n}\n\nexport interface DoctorRunReport {\n status: DoctorStatus;\n generatedAt: string;\n corpus: string;\n sections: Partial<Record<DoctorSectionName, DoctorSectionReport>>;\n issues: DoctorIssue[];\n hardIssues: number;\n}\n\nexport interface DoctorOptions {\n section?: 'all' | 'integrations';\n}\n\nfunction inspectDirs(corpus: string): { missing: string[] } {\n const missing: string[] = [];\n for (const dir of EXPECTED_DIRS) {\n const full = join(corpus, dir);\n if (!existsSync(full)) missing.push(dir);\n }\n return { missing };\n}\n\nfunction checkDirs(corpus: string): number {\n const { missing } = inspectDirs(corpus);\n for (const dir of EXPECTED_DIRS) {\n if (missing.includes(dir)) bad(`${dir}/ ${chalk.dim('missing')}`);\n else ok(`${dir}/`);\n }\n return missing.length;\n}\n\nfunction inspectWikiVersion(corpus: string): { exists: boolean; version: string | null } {\n const versionFile = join(corpus, '.wiki', 'version');\n if (existsSync(versionFile)) {\n const ver = readFileSync(versionFile, 'utf-8').trim();\n return { exists: true, version: ver };\n }\n return { exists: false, version: null };\n}\n\nfunction checkWikiVersion(corpus: string): number {\n const result = inspectWikiVersion(corpus);\n if (result.exists) {\n ok(`.wiki/version → ${result.version}`);\n return 0;\n }\n bad('.wiki/version missing');\n return 1;\n}\n\nfunction inspectFrontmatterCoverage(corpus: string) {\n const files = collectMdFiles(corpus);\n const withFm = files.filter((f) => hasFrontmatter(f)).length;\n const total = files.length;\n const pct = total === 0 ? 100 : Math.round((withFm / total) * 100);\n return { withFrontmatter: withFm, total, pct };\n}\n\nfunction checkFrontmatterCoverage(corpus: string) {\n const { withFrontmatter, total, pct } = inspectFrontmatterCoverage(corpus);\n const color = pct >= 90 ? chalk.green : pct >= 60 ? chalk.yellow : chalk.red;\n const icon = pct >= 90 ? '✓' : pct >= 60 ? '⚠' : '✗';\n print(`${color(icon)} frontmatter coverage: ${withFrontmatter}/${total} (${pct}%)`);\n}\n\nfunction findMissingIndexDirs(corpus: string): string[] {\n const missing: string[] = [];\n\n function walk(dir: string) {\n if (!existsSync(dir)) return;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n if (!entry.isDirectory()) continue;\n\n const full = join(dir, entry.name);\n const rel = relative(corpus, full);\n\n // 复用 index 命令的排除规则:不对这些目录要求 _INDEX.md\n if (isIndexExcluded(rel)) continue;\n // 目录包装式原料(xxx/article.md)是一个 entry,不是容器——不需要 _INDEX.md\n if (isFolderPackage(full)) continue;\n\n // 本目录是否应该有 _INDEX.md:\n // 有直接 .md 文件 或 有目录包装式原料子目录\n let shouldHaveIndex = false;\n for (const name of readdirSync(full)) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n const childPath = join(full, name);\n let stat;\n try {\n stat = lstatSync(childPath);\n } catch {\n continue;\n }\n if (stat.isFile() && name.endsWith('.md')) {\n shouldHaveIndex = true;\n break;\n }\n if (stat.isDirectory() && isFolderPackage(childPath)) {\n shouldHaveIndex = true;\n break;\n }\n }\n\n if (shouldHaveIndex && !existsSync(join(full, '_INDEX.md'))) {\n missing.push(rel);\n }\n walk(full);\n }\n }\n\n walk(corpus);\n return missing;\n}\n\nfunction checkIndexFiles(corpus: string): number {\n const missing = findMissingIndexDirs(corpus);\n for (const rel of missing) warn(`_INDEX.md missing in ${rel}/`);\n if (missing.length === 0) {\n ok('all directories with .md files have _INDEX.md');\n }\n return missing.length;\n}\n\n/**\n * 检查 .obsidian/graph.json filter 是否含推荐项(批次 26 触达老用户)。\n * obsidian 是可选用途,不阻塞 doctor 整体绿 —— 故意不计入 issues 总数。\n */\nfunction inspectObsidianGraph(corpus: string): DoctorSectionReport {\n try {\n const recommended = getRecommendedFilter();\n const cur = readCorpusFilter(corpus);\n if (!cur.exists) {\n return {\n status: 'warn',\n message: 'graph filter 不完整,运行 lorekit obsidian-tune 查看详情',\n };\n }\n if (isFilterComplete(cur.search, recommended)) {\n return { status: 'ok', message: 'graph filter 完整' };\n }\n return {\n status: 'warn',\n message: 'graph filter 不完整,运行 lorekit obsidian-tune 查看详情',\n };\n } catch (e) {\n return { status: 'warn', message: `检查 graph filter 失败: ${(e as Error).message}` };\n }\n}\n\nfunction checkObsidianGraph(corpus: string): void {\n const result = inspectObsidianGraph(corpus);\n if (result.status === 'ok') ok(`obsidian: ${result.message}`);\n else warn(`obsidian: ${result.message}`);\n}\n\nfunction inspectArchive(corpus: string): DoctorSectionReport {\n const archiveDir = join(corpus, '_归档');\n if (existsSync(archiveDir)) {\n return { status: 'ok', exists: true };\n }\n return { status: 'warn', exists: false, message: '_归档/ not found (optional)' };\n}\n\nfunction checkArchive(corpus: string): number {\n const result = inspectArchive(corpus);\n if (result.status === 'ok') ok('_归档/ exists');\n else warn(String(result.message));\n return 0; // not a hard failure\n}\n\nfunction statusFromIssues(issues: DoctorIssue[]): DoctorStatus {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.length > 0) return 'warn';\n return 'ok';\n}\n\nfunction convertGbrainIssue(issue: GbrainDoctorIssue): DoctorIssue {\n return {\n section: 'gbrain',\n severity: issue.severity,\n message: issue.message,\n recommendation: issue.recommendation,\n };\n}\n\nfunction gbrainSection(gbrain: GbrainDoctorResult): DoctorSectionReport {\n return {\n status: gbrain.status,\n gbrain: {\n status: gbrain.status,\n installed: gbrain.gbrain.installed,\n binary: gbrain.gbrain.binary,\n version: gbrain.gbrain.version,\n brainInitialized: gbrain.gbrain.brainInitialized,\n manifestPath: gbrain.manifestPath,\n syncReportPath: gbrain.syncReportPath,\n issues: gbrain.issues,\n },\n };\n}\n\nexport async function runDoctorReport(\n corpus: string,\n opts: DoctorOptions = {},\n): Promise<DoctorRunReport> {\n const section = opts.section ?? 'all';\n if (section !== 'all' && section !== 'integrations') {\n throw new Error(`unsupported doctor section: ${section}`);\n }\n\n const report: DoctorRunReport = {\n status: 'ok',\n generatedAt: new Date().toISOString(),\n corpus,\n sections: {},\n issues: [],\n hardIssues: 0,\n };\n\n if (section === 'all') {\n const dirs = inspectDirs(corpus);\n report.sections.directories = {\n status: dirs.missing.length > 0 ? 'error' : 'ok',\n expected: EXPECTED_DIRS,\n missing: dirs.missing,\n };\n for (const dir of dirs.missing) {\n report.issues.push({\n section: 'directories',\n severity: 'error',\n message: `${dir}/ missing`,\n });\n }\n\n const wiki = inspectWikiVersion(corpus);\n report.sections.wikiMetadata = {\n status: wiki.exists ? 'ok' : 'error',\n version: wiki.version,\n versionFileExists: wiki.exists,\n };\n if (!wiki.exists) {\n report.issues.push({\n section: 'wikiMetadata',\n severity: 'error',\n message: '.wiki/version missing',\n });\n }\n\n const fm = inspectFrontmatterCoverage(corpus);\n report.sections.frontmatter = {\n status: fm.pct >= 90 ? 'ok' : fm.pct >= 60 ? 'warn' : 'error',\n ...fm,\n };\n\n const missingIndexes = findMissingIndexDirs(corpus);\n report.sections.indexFiles = {\n status: missingIndexes.length > 0 ? 'warn' : 'ok',\n missing: missingIndexes,\n };\n for (const rel of missingIndexes) {\n report.issues.push({\n section: 'indexFiles',\n severity: 'warn',\n message: `_INDEX.md missing in ${rel}/`,\n });\n }\n\n report.sections.archive = inspectArchive(corpus);\n report.sections.obsidian = inspectObsidianGraph(corpus);\n }\n\n const gbrain = await doctorGbrain(corpus);\n report.sections.integrations = gbrainSection(gbrain);\n report.issues.push(...gbrain.issues.map(convertGbrainIssue));\n\n report.hardIssues = report.issues.filter((issue) => issue.severity === 'error').length;\n report.status = statusFromIssues(report.issues);\n return report;\n}\n\n/**\n * 程序内复用入口:跑健康体检。\n * 返回 issue 总数。调用方自行决定要不要把退出码设成非零。\n */\nexport async function runDoctor(corpus: string): Promise<number> {\n print(chalk.bold(`\\nlorekit doctor — ${corpus}\\n`));\n\n let issues = 0;\n\n print(chalk.cyan('── directories ──'));\n issues += checkDirs(corpus);\n print();\n\n print(chalk.cyan('── wiki metadata ──'));\n issues += checkWikiVersion(corpus);\n print();\n\n print(chalk.cyan('── frontmatter ──'));\n checkFrontmatterCoverage(corpus);\n print();\n\n print(chalk.cyan('── index files ──'));\n issues += checkIndexFiles(corpus);\n print();\n\n print(chalk.cyan('── archive ──'));\n checkArchive(corpus);\n print();\n\n print(chalk.cyan('── obsidian ──'));\n checkObsidianGraph(corpus);\n print();\n\n print(chalk.cyan('── integrations ──'));\n const gbrain = await doctorGbrain(corpus);\n if (gbrain.status === 'ok') {\n ok('gbrain: integration healthy');\n } else {\n for (const issue of gbrain.issues) {\n const line = `gbrain: ${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n }\n const integrationErrors = gbrain.issues.filter((issue) => issue.severity === 'error').length;\n issues += integrationErrors;\n print();\n\n if (issues === 0) {\n print(chalk.green.bold('all checks passed ✓'));\n } else {\n print(chalk.yellow(`${issues} issue(s) found`));\n }\n print();\n\n return issues;\n}\n\nexport function doctorCommand(program: Command) {\n program\n .command('doctor')\n .description('run health checks on the corpus')\n .option('--json', 'output machine-readable doctor report', false)\n .option('--section <name>', 'only run a doctor section (currently: integrations)', 'all')\n .action(async (opts: { json?: boolean; section?: 'all' | 'integrations' | string }) => {\n const corpus = requireCorpus();\n if (opts.json) {\n const report = await runDoctorReport(corpus, {\n section: opts.section === 'integrations' ? 'integrations' : 'all',\n });\n out(JSON.stringify(report, null, 2));\n process.exitCode = report.hardIssues > 0 ? 1 : 0;\n return;\n }\n if (opts.section === 'integrations') {\n const report = await runDoctorReport(corpus, { section: 'integrations' });\n print(chalk.bold(`\\nlorekit doctor — ${corpus}\\n`));\n print(chalk.cyan('── integrations ──'));\n const integration = report.sections.integrations?.gbrain as\n | { issues?: GbrainDoctorIssue[] }\n | undefined;\n const issues = integration?.issues ?? [];\n if (issues.length === 0) ok('gbrain: integration healthy');\n for (const issue of issues) {\n const line = `gbrain: ${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n process.exitCode = report.hardIssues > 0 ? 1 : 0;\n return;\n }\n const issues = await runDoctor(corpus);\n process.exitCode = issues > 0 ? 1 : 0;\n });\n}\n","/**\n * obsidian.ts — Obsidian graph filter SSOT helper(批次 26)\n *\n * 推荐 filter 的单一事实源是 `templates/default-corpus/.obsidian/graph.json`。\n * obsidian-tune 命令与 doctor 集成都从这里读,避免和模板漂移。\n *\n * filter 完整性判断采用\"包含所有推荐 token\"——用户可能加了自己的\n * colorGroups / 额外 filter,只要把推荐项都覆盖到就算 OK,不要求完全相等。\n */\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { lorekitRoot } from '../utils/fs.js';\n\nexport interface GraphConfig {\n search?: string;\n showTags?: boolean;\n showAttachments?: boolean;\n hideUnresolved?: boolean;\n showOrphans?: boolean;\n [key: string]: unknown;\n}\n\nexport interface CorpusFilterReadResult {\n exists: boolean;\n search?: string;\n raw?: GraphConfig;\n}\n\n/** 模板里的推荐 graph.json(完整对象)。读不到会 throw —— 模板属于包发布产物 */\nexport function getRecommendedGraphConfig(): GraphConfig {\n const tpl = join(lorekitRoot(), 'templates', 'default-corpus', '.obsidian', 'graph.json');\n const raw = readFileSync(tpl, 'utf-8');\n return JSON.parse(raw) as GraphConfig;\n}\n\n/** 推荐 filter 的 search 字符串(SSOT) */\nexport function getRecommendedFilter(): string {\n const cfg = getRecommendedGraphConfig();\n return cfg.search ?? '';\n}\n\n/** 读 corpus 内的 .obsidian/graph.json。不存在不抛错,返回 exists=false */\nexport function readCorpusFilter(corpus: string): CorpusFilterReadResult {\n const dest = join(corpus, '.obsidian', 'graph.json');\n if (!existsSync(dest)) return { exists: false };\n try {\n const raw = readFileSync(dest, 'utf-8');\n const parsed = JSON.parse(raw) as GraphConfig;\n return { exists: true, search: parsed.search, raw: parsed };\n } catch {\n // JSON 损坏视为存在但 filter 不可读 —— 当作 search 缺失处理\n return { exists: true, search: undefined };\n }\n}\n\n/**\n * 把 search 字符串拆 token(按空白)。\n * Obsidian search 语法 token 形如 `-path:\"_工作台\"` / `-file:\"_INDEX\"`,\n * 引号内含中文也安全:split 走简单空白,引号内不会含空白。\n */\nfunction tokenize(search: string): string[] {\n return search\n .split(/\\s+/)\n .map((t) => t.trim())\n .filter(Boolean);\n}\n\n/**\n * 完整性判断:actual 必须包含 recommended 拆出的全部 token(顺序无关、可有额外项)。\n * actual 缺失 / 空字符串都视为不完整。\n */\nexport function isFilterComplete(actual: string | undefined, recommended: string): boolean {\n if (!actual) return false;\n const want = new Set(tokenize(recommended));\n const have = new Set(tokenize(actual));\n for (const t of want) {\n if (!have.has(t)) return false;\n }\n return true;\n}\n\n/** 列出 actual 缺少哪些推荐 token(用于 diff 输出) */\nexport function missingTokens(actual: string | undefined, recommended: string): string[] {\n const have = new Set(actual ? tokenize(actual) : []);\n return tokenize(recommended).filter((t) => !have.has(t));\n}\n","import { existsSync, mkdirSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { sha256 } from '../../utils/fs.js';\nimport { getGbrainStatus, type GbrainStatusResult } from './gbrain-status.js';\nimport { exportForGbrain, type GbrainExportResult } from './gbrain-export.js';\nimport { readJsonFile, writeJsonFile, type GbrainExportManifest } from './manifest.js';\nimport { runExternalCommand, type ExternalCommandResult } from './process.js';\n\nexport { getGbrainStatus, exportForGbrain };\n\nexport interface GbrainSyncOptions {\n dryRun?: boolean;\n json?: boolean;\n exportEvenIfMissing?: boolean;\n}\n\nexport interface GbrainSyncResult {\n status: 'ok' | 'error';\n dryRun: boolean;\n startedAt: string;\n finishedAt: string;\n corpus: string;\n export: GbrainExportResult | null;\n gbrain: {\n binary: string;\n version: string | null;\n command: string[];\n exitCode: number | null;\n stdout: string;\n stderr: string;\n durationMs: number;\n } | null;\n gbrainImport?: { skipped: true; reason: string };\n warnings: string[];\n errors: string[];\n}\n\nexport interface GbrainDoctorIssue {\n section: 'gbrain';\n severity: 'warn' | 'error';\n message: string;\n recommendation: string;\n}\n\nexport interface GbrainDoctorResult {\n status: 'ok' | 'warn' | 'error';\n corpus: string;\n gbrain: GbrainStatusResult;\n manifestPath: string;\n syncReportPath: string;\n issues: GbrainDoctorIssue[];\n}\n\nexport interface GbrainQueryResult {\n status: 'ok' | 'error';\n source: 'gbrain';\n message: string;\n staleCheck: {\n skipped: boolean;\n status: GbrainDoctorResult['status'] | null;\n issues: GbrainDoctorIssue[];\n };\n gbrain: ExternalCommandResult | null;\n warnings: string[];\n errors: string[];\n}\n\nfunction syncReportPath(corpus: string): string {\n return join(corpus, '.wiki', 'integrations', 'gbrain', 'sync-report.json');\n}\n\nfunction writeSyncReport(corpus: string, result: GbrainSyncResult): void {\n const path = syncReportPath(corpus);\n mkdirSync(join(corpus, '.wiki', 'integrations', 'gbrain'), { recursive: true });\n writeJsonFile(path, result);\n}\n\nfunction commandSummary(binary: string, pagesDir: string): string[] {\n return [binary, 'import', pagesDir];\n}\n\nexport async function syncGbrain(\n corpus: string,\n opts: GbrainSyncOptions = {},\n): Promise<GbrainSyncResult> {\n const dryRun = opts.dryRun ?? false;\n const startedAt = new Date().toISOString();\n\n if (dryRun) {\n const exportResult = exportForGbrain(corpus, { dryRun: true });\n return {\n status: 'ok',\n dryRun: true,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: null,\n gbrainImport: { skipped: true, reason: 'dry-run' },\n warnings: exportResult.warnings,\n errors: [],\n };\n }\n\n const gbrainStatus = await getGbrainStatus();\n if (!gbrainStatus.installed) {\n const exportResult = opts.exportEvenIfMissing\n ? exportForGbrain(corpus, { dryRun: false })\n : null;\n const result: GbrainSyncResult = {\n status: 'error',\n dryRun: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: null,\n gbrainImport: { skipped: true, reason: 'gbrain-missing' },\n warnings: exportResult?.warnings ?? [],\n errors: ['gbrain is not installed', ...gbrainStatus.errors],\n };\n writeSyncReport(corpus, result);\n return result;\n }\n\n const exportResult = exportForGbrain(corpus, { dryRun: false });\n const importCommand = commandSummary(gbrainStatus.binary, exportResult.pagesDir);\n const external = await runExternalCommand({\n command: gbrainStatus.binary,\n args: ['import', exportResult.pagesDir],\n cwd: corpus,\n timeoutMs: 120_000,\n });\n\n const result: GbrainSyncResult = {\n status: external.exitCode === 0 ? 'ok' : 'error',\n dryRun: false,\n startedAt,\n finishedAt: new Date().toISOString(),\n corpus,\n export: exportResult,\n gbrain: {\n binary: gbrainStatus.binary,\n version: gbrainStatus.version,\n command: importCommand,\n exitCode: external.exitCode,\n stdout: external.stdout,\n stderr: external.stderr,\n durationMs: external.durationMs,\n },\n warnings: exportResult.warnings,\n errors: external.exitCode === 0 ? [] : [external.error || external.stderr || 'gbrain import failed'],\n };\n writeSyncReport(corpus, result);\n return result;\n}\n\nexport async function doctorGbrain(corpus: string): Promise<GbrainDoctorResult> {\n const issues: GbrainDoctorIssue[] = [];\n const gbrain = await getGbrainStatus();\n if (!gbrain.installed) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain binary is not installed',\n recommendation: 'Install GBrain only if you want graph retrieval: git clone + bun install + bun link',\n });\n }\n\n const manifestPath = join(corpus, '.wiki', 'integrations', 'gbrain-export', 'manifest.json');\n const syncPath = syncReportPath(corpus);\n const manifest = readJsonFile<GbrainExportManifest>(manifestPath);\n if (!manifest) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain export manifest is missing',\n recommendation: 'Run lorekit gbrain export',\n });\n } else {\n for (const page of manifest.pages) {\n const sourcePath = join(corpus, page.sourcePath);\n if (!existsSync(sourcePath)) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: `Exported page source is missing: ${page.sourcePath}`,\n recommendation: 'Run lorekit gbrain export to refresh the staging directory',\n });\n continue;\n }\n const currentHash = 'sha256:' + sha256(sourcePath);\n if (currentHash !== page.hash) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: `GBrain export is stale: ${page.sourcePath}`,\n recommendation: 'Run lorekit gbrain export or lorekit gbrain sync',\n });\n }\n }\n }\n\n if (!existsSync(syncPath)) {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'GBrain sync report is missing',\n recommendation: 'Run lorekit gbrain sync after export when GBrain is installed',\n });\n } else {\n try {\n const report = JSON.parse(readFileSync(syncPath, 'utf-8')) as { status?: string };\n if (report.status !== 'ok') {\n issues.push({\n section: 'gbrain',\n severity: 'warn',\n message: 'Last GBrain sync did not finish successfully',\n recommendation: 'Inspect .wiki/integrations/gbrain/sync-report.json and rerun sync',\n });\n }\n } catch (e) {\n issues.push({\n section: 'gbrain',\n severity: 'error',\n message: `GBrain sync report is unreadable: ${(e as Error).message}`,\n recommendation: 'Regenerate it with lorekit gbrain sync',\n });\n }\n }\n\n const hasError = issues.some((i) => i.severity === 'error');\n return {\n status: hasError ? 'error' : issues.length > 0 ? 'warn' : 'ok',\n corpus,\n gbrain,\n manifestPath,\n syncReportPath: syncPath,\n issues,\n };\n}\n\nexport interface GbrainQueryOptions {\n staleCheck?: boolean;\n}\n\nexport async function queryGbrain(\n corpus: string,\n text: string,\n opts: GbrainQueryOptions = {},\n): Promise<GbrainQueryResult> {\n const message =\n 'This answer comes from GBrain index generated from lorekit export. To persist new knowledge, use wiki-fileback / lorekit audit.';\n const shouldCheck = opts.staleCheck !== false;\n if (shouldCheck) {\n const check = await doctorGbrain(corpus);\n if (!check.gbrain.installed) {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: false, status: check.status, issues: check.issues },\n gbrain: null,\n warnings: check.issues.map((i) => i.message),\n errors: ['gbrain is not installed', ...check.gbrain.errors],\n };\n }\n if (check.status !== 'ok') {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: false, status: check.status, issues: check.issues },\n gbrain: null,\n warnings: check.issues.map((i) => i.message),\n errors: [\n 'GBrain export is not ready. Run lorekit gbrain sync first, or pass --no-stale-check to query anyway.',\n ...check.issues.map((i) => i.message),\n ],\n };\n }\n }\n\n const status = await getGbrainStatus();\n if (!status.installed) {\n return {\n status: 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: !shouldCheck, status: null, issues: [] },\n gbrain: null,\n warnings: [],\n errors: ['gbrain is not installed', ...status.errors],\n };\n }\n\n const r = await runExternalCommand({\n command: status.binary,\n args: ['query', text],\n timeoutMs: 120_000,\n });\n return {\n status: r.exitCode === 0 ? 'ok' : 'error',\n source: 'gbrain',\n message,\n staleCheck: { skipped: !shouldCheck, status: shouldCheck ? 'ok' : null, issues: [] },\n gbrain: r,\n warnings: [],\n errors: r.exitCode === 0 ? [] : [r.error || r.stderr || 'gbrain query failed'],\n };\n}\n","import { existsSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { runExternalCommand } from './process.js';\n\nexport const GBRAIN_INSTALL_HINT = [\n 'git clone https://github.com/garrytan/gbrain.git ~/code/gbrain',\n 'cd ~/code/gbrain',\n 'bun install',\n 'bun link',\n 'gbrain init',\n].join('\\n');\n\nexport interface GbrainStatusResult {\n installed: boolean;\n binary: string;\n version: string | null;\n brainInitialized: boolean;\n installHint: string;\n errors: string[];\n}\n\nexport async function getGbrainStatus(): Promise<GbrainStatusResult> {\n const binary = process.env.LOREKIT_GBRAIN_BIN || 'gbrain';\n const errors: string[] = [];\n const versionProbe = await runExternalCommand({\n command: binary,\n args: ['--version'],\n timeoutMs: 10_000,\n });\n\n if (versionProbe.exitCode !== 0) {\n errors.push(versionProbe.error || versionProbe.stderr.trim() || 'gbrain binary not installed');\n return {\n installed: false,\n binary,\n version: null,\n brainInitialized: existsSync(join(homedir(), '.gbrain')),\n installHint: GBRAIN_INSTALL_HINT,\n errors,\n };\n }\n\n const version = (versionProbe.stdout || versionProbe.stderr).trim() || null;\n return {\n installed: true,\n binary,\n version,\n brainInitialized: existsSync(join(homedir(), '.gbrain')),\n installHint: GBRAIN_INSTALL_HINT,\n errors,\n };\n}\n","import { spawn } from 'node:child_process';\n\nexport interface ExternalCommandArgs {\n command: string;\n args: string[];\n cwd?: string;\n timeoutMs?: number;\n}\n\nexport interface ExternalCommandResult {\n command: string;\n args: string[];\n exitCode: number;\n stdout: string;\n stderr: string;\n durationMs: number;\n timedOut: boolean;\n error?: string;\n}\n\nexport function runExternalCommand(opts: ExternalCommandArgs): Promise<ExternalCommandResult> {\n const startedAt = Date.now();\n const timeoutMs = opts.timeoutMs ?? 120_000;\n\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let settled = false;\n let timedOut = false;\n\n const child = spawn(opts.command, opts.args, {\n cwd: opts.cwd,\n shell: false,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill('SIGTERM');\n }, timeoutMs);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf-8');\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n\n child.on('error', (e: NodeJS.ErrnoException) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n command: opts.command,\n args: opts.args,\n exitCode: -1,\n stdout,\n stderr,\n durationMs: Date.now() - startedAt,\n timedOut,\n error: e.message,\n });\n });\n\n child.on('close', (code) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve({\n command: opts.command,\n args: opts.args,\n exitCode: code ?? (timedOut ? 124 : 1),\n stdout,\n stderr,\n durationMs: Date.now() - startedAt,\n timedOut,\n });\n });\n });\n}\n","import { createHash } from 'node:crypto';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n statSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport matter from 'gray-matter';\nimport {\n type GbrainExportManifest,\n type GbrainExportManifestPage,\n type GbrainExportManifestSkipped,\n writeJsonFile,\n} from './manifest.js';\n\nexport interface GbrainExportOptions {\n out?: string;\n dryRun?: boolean;\n}\n\nexport interface GbrainExportResult {\n status: 'ok' | 'warn';\n dryRun: boolean;\n corpus: string;\n exportDir: string;\n pagesDir: string;\n manifestPath: string;\n exportedAt: string;\n pagesExported: number;\n pagesSkipped: number;\n pages: GbrainExportManifestPage[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n}\n\ninterface Candidate {\n absPath: string;\n sourcePath: string;\n}\n\nfunction toPosixPath(path: string): string {\n return path.split('\\\\').join('/');\n}\n\nfunction sha256Content(content: Buffer | string): string {\n return 'sha256:' + createHash('sha256').update(content).digest('hex');\n}\n\nfunction exportRoot(corpus: string, out?: string): string {\n if (!out) return join(corpus, '.wiki', 'integrations', 'gbrain-export');\n return resolve(corpus, out);\n}\n\nfunction collectKnowledgeMarkdown(corpus: string): {\n candidates: Candidate[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n} {\n const root = join(corpus, '知识库');\n const candidates: Candidate[] = [];\n const skipped: GbrainExportManifestSkipped[] = [];\n const warnings: string[] = [];\n\n if (!existsSync(root)) {\n warnings.push('知识库/ not found; no pages exported');\n return { candidates, skipped, warnings };\n }\n\n function walk(dir: string): void {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith('.')) continue;\n const absPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n walk(absPath);\n continue;\n }\n if (!entry.isFile() || !entry.name.endsWith('.md')) continue;\n\n const sourcePath = toPosixPath(relative(corpus, absPath));\n if (sourcePath === '知识库/模板' || sourcePath.startsWith('知识库/模板/')) {\n skipped.push({ sourcePath, reason: 'template file skipped by default' });\n continue;\n }\n if (entry.name === '_INDEX.md') {\n skipped.push({ sourcePath, reason: 'index file skipped by default' });\n continue;\n }\n if (entry.name === 'index.md') {\n skipped.push({ sourcePath, reason: 'local index file skipped by default' });\n continue;\n }\n candidates.push({ absPath, sourcePath });\n }\n }\n\n walk(root);\n candidates.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));\n skipped.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));\n return { candidates, skipped, warnings };\n}\n\nfunction ensureFreshExportDir(root: string, exportedAt: string): void {\n mkdirSync(root, { recursive: true });\n const backupRoot = join(root, 'backups', exportedAt.replace(/[:.]/g, '-'));\n let moved = false;\n\n for (const name of ['pages', 'manifest.json', 'README.md']) {\n const current = join(root, name);\n if (!existsSync(current)) continue;\n if (!moved) {\n mkdirSync(backupRoot, { recursive: true });\n moved = true;\n }\n renameSync(current, join(backupRoot, name));\n }\n}\n\nfunction normalizeForGbrain(raw: string, sourcePath: string, exportedAt: string): string {\n const parsed = matter(raw);\n const data: Record<string, unknown> = { ...parsed.data };\n delete data.slug;\n data.lorekit_source_path = sourcePath;\n data.lorekit_layer = 'artifact';\n data.lorekit_hash = sha256Content(raw);\n data.lorekit_exported_at = exportedAt;\n return matter.stringify(parsed.content, data);\n}\n\nfunction pageMeta(raw: string): { title: string | null; type: string | null } {\n const parsed = matter(raw);\n return {\n title: typeof parsed.data.title === 'string' ? parsed.data.title : null,\n type: typeof parsed.data.type === 'string' ? parsed.data.type : null,\n };\n}\n\nexport function exportForGbrain(corpus: string, opts: GbrainExportOptions = {}): GbrainExportResult {\n const dryRun = opts.dryRun ?? false;\n const exportedAt = new Date().toISOString();\n const root = exportRoot(corpus, opts.out);\n const pagesDir = join(root, 'pages');\n const manifestPath = join(root, 'manifest.json');\n const { candidates, skipped, warnings } = collectKnowledgeMarkdown(corpus);\n\n const pages: GbrainExportManifestPage[] = [];\n for (const candidate of candidates) {\n const rawBuffer = readFileSync(candidate.absPath);\n const raw = rawBuffer.toString('utf-8');\n const relUnderKnowledge = toPosixPath(relative(join(corpus, '知识库'), candidate.absPath));\n const exportPath = toPosixPath(join('pages', relUnderKnowledge));\n const meta = pageMeta(raw);\n pages.push({\n sourcePath: candidate.sourcePath,\n exportPath,\n title: meta.title,\n type: meta.type,\n hash: sha256Content(rawBuffer),\n bytes: statSync(candidate.absPath).size,\n status: 'exported',\n });\n }\n\n if (!dryRun) {\n ensureFreshExportDir(root, exportedAt);\n for (const candidate of candidates) {\n const raw = readFileSync(candidate.absPath, 'utf-8');\n const relUnderKnowledge = relative(join(corpus, '知识库'), candidate.absPath);\n const target = join(pagesDir, relUnderKnowledge);\n mkdirSync(dirname(target), { recursive: true });\n writeFileSync(target, normalizeForGbrain(raw, candidate.sourcePath, exportedAt), 'utf-8');\n }\n const manifest: GbrainExportManifest = {\n version: 1,\n integration: 'gbrain',\n source: 'lorekit',\n corpus,\n exportedAt,\n pages,\n skipped,\n warnings,\n };\n writeJsonFile(manifestPath, manifest);\n writeFileSync(\n join(root, 'README.md'),\n [\n '# GBrain export',\n '',\n 'Generated by `lorekit gbrain export`.',\n 'This directory is a staging copy for GBrain import; lorekit 知识库/ remains the source of truth.',\n '',\n ].join('\\n'),\n 'utf-8',\n );\n }\n\n return {\n status: warnings.length > 0 ? 'warn' : 'ok',\n dryRun,\n corpus,\n exportDir: root,\n pagesDir,\n manifestPath,\n exportedAt,\n pagesExported: pages.length,\n pagesSkipped: skipped.length,\n pages,\n skipped,\n warnings,\n };\n}\n","import { existsSync, readFileSync, writeFileSync } from 'node:fs';\n\nexport interface GbrainExportManifestPage {\n sourcePath: string;\n exportPath: string;\n title: string | null;\n type: string | null;\n hash: string;\n bytes: number;\n status: 'exported';\n}\n\nexport interface GbrainExportManifestSkipped {\n sourcePath: string;\n reason: string;\n}\n\nexport interface GbrainExportManifest {\n version: 1;\n integration: 'gbrain';\n source: 'lorekit';\n corpus: string;\n exportedAt: string;\n pages: GbrainExportManifestPage[];\n skipped: GbrainExportManifestSkipped[];\n warnings: string[];\n}\n\nexport function writeJsonFile(path: string, data: unknown): void {\n writeFileSync(path, JSON.stringify(data, null, 2) + '\\n', 'utf-8');\n}\n\nexport function readJsonFile<T>(path: string): T | null {\n if (!existsSync(path)) return null;\n return JSON.parse(readFileSync(path, 'utf-8')) as T;\n}\n","import type { Command } from 'commander';\nimport { readFileSync, statSync } from 'node:fs';\nimport { relative } from 'node:path';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport { debug, out } from '../utils/logger.js';\n\nexport function statsCommand(program: Command) {\n program\n .command('stats')\n .description('output corpus statistics as JSON')\n .action(() => {\n const corpus = requireCorpus();\n const files = collectMdFiles(corpus);\n const now = Date.now();\n const sevenDays = 7 * 24 * 60 * 60 * 1000;\n\n const byType: Record<string, number> = {};\n const byDir: Record<string, number> = {};\n const inboundLinks = new Set<string>();\n let recentActive7d = 0;\n let lastUpdated = '';\n\n for (const file of files) {\n // by_type\n const fm = extractFrontmatter(file);\n const type = fm.type || 'unknown';\n byType[type] = (byType[type] || 0) + 1;\n\n // by_dir (top-level directory relative to corpus)\n const rel = relative(corpus, file);\n const topDir = rel.split('/')[0] || '.';\n byDir[topDir] = (byDir[topDir] || 0) + 1;\n\n // recent_active_7d\n try {\n const mtime = statSync(file).mtime;\n if (now - mtime.getTime() < sevenDays) {\n recentActive7d++;\n }\n const iso = mtime.toISOString();\n if (iso > lastUpdated) lastUpdated = iso;\n } catch (e) {\n // 单文件 stat 失败不应中断整体统计;走 debug\n debug(`stats: stat(${file}) failed: ${(e as Error).message}`);\n }\n\n // Collect wikilink targets to identify orphans later\n try {\n const content = readFileSync(file, 'utf-8');\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) {\n inboundLinks.add(m[1].trim());\n }\n } catch (e) {\n // 单文件读失败时该文件的 wikilinks 漏掉,但不影响全局统计;走 debug\n debug(`stats: readFileSync(${file}) failed: ${(e as Error).message}`);\n }\n }\n\n // Compute orphans: pages that receive zero inbound links\n const orphans: string[] = [];\n for (const file of files) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n const baseName = stem.split('/').pop()!;\n // A page is an orphan if neither its full relative stem nor its base name\n // appears as a wikilink target\n if (!inboundLinks.has(stem) && !inboundLinks.has(baseName)) {\n orphans.push(rel);\n }\n }\n\n const result = {\n total_pages: files.length,\n by_type: byType,\n by_dir: byDir,\n recent_active_7d: recentActive7d,\n orphans: orphans.length,\n last_updated: lastUpdated || null,\n };\n\n out(JSON.stringify(result, null, 2));\n });\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { relative, basename } from 'node:path';\nimport chalk from 'chalk';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport {\n lintSkipFrontmatterBasenames,\n lintRootOnlySkipBasenames,\n lintSkipOrphanPrefixes,\n lintSkipFrontmatterPrefixes,\n lintSkipBrokenLinkPrefixes,\n} from '../lib/paths.js';\nimport { bad, ok, print } from '../utils/logger.js';\n\nconst REQUIRED_FIELDS = ['type', 'title', 'slug', 'created', 'updated'] as const;\n\nfunction isRootLevel(rel: string): boolean {\n return !rel.includes('/');\n}\n\nfunction shouldSkipFrontmatter(rel: string): boolean {\n const base = basename(rel);\n if (lintSkipFrontmatterBasenames.has(base)) return true;\n if (isRootLevel(rel) && lintRootOnlySkipBasenames.has(base)) return true;\n for (const prefix of lintSkipFrontmatterPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\nfunction shouldSkipOrphan(rel: string): boolean {\n const base = basename(rel);\n if (lintSkipFrontmatterBasenames.has(base)) return true;\n if (isRootLevel(rel) && lintRootOnlySkipBasenames.has(base)) return true;\n for (const prefix of lintSkipOrphanPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\nfunction shouldSkipBrokenLink(rel: string): boolean {\n for (const prefix of lintSkipBrokenLinkPrefixes) {\n if (rel.startsWith(prefix)) return true;\n }\n return false;\n}\n\n// 系统隔离:frontmatter `graph-excluded: true` 的页面不入 Obsidian 图谱,\n// 所以也不应被 orphan 检查报\"无入链\"。典型:QUESTIONS.md / overview.md / 输出/*\nfunction isGraphExcluded(fm: Record<string, unknown>): boolean {\n return fm['graph-excluded'] === true || fm['graph_excluded'] === true;\n}\n\n// 去掉围栏代码块和行内代码,避免文档里 `[[Page]]` 这类占位符被当作真 wikilink\nfunction stripCodeBlocks(content: string): string {\n content = content.replace(/```[\\s\\S]*?```/g, '');\n content = content.replace(/`[^`\\n]+`/g, '');\n return content;\n}\n\ninterface LintIssue {\n file: string;\n kind: 'missing-field' | 'broken-link' | 'orphan';\n detail: string;\n}\n\nexport function runLint(corpus: string): LintIssue[] {\n const files = collectMdFiles(corpus);\n const issues: LintIssue[] = [];\n\n // Build lookup sets for wikilink resolution\n // Map: base name (no ext) → relative path, and full relative stem → relative path\n const stemSet = new Set<string>();\n const baseNameSet = new Set<string>();\n // Track inbound links per base name / stem for orphan detection\n const inboundLinks = new Set<string>();\n\n for (const file of files) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n stemSet.add(stem);\n baseNameSet.add(stem.split('/').pop()!);\n\n // 文件夹包装式原料:`原料/文章/xxx/article.md` 的规范引用是 `[[原料/文章/xxx]]`\n // 把父目录路径也登记为有效链接目标\n if (stem.endsWith('/article')) {\n const folderStem = stem.replace(/\\/article$/, '');\n stemSet.add(folderStem);\n baseNameSet.add(folderStem.split('/').pop()!);\n }\n }\n\n // Pass 1: frontmatter + collect wikilinks\n const fileLinks = new Map<string, string[]>();\n const fileFrontmatter = new Map<string, Record<string, unknown>>();\n\n for (const file of files) {\n const rel = relative(corpus, file);\n\n // 总是提取 fm 存起来(Pass 3 orphan 检查用 graph-excluded 判断)\n let fm: Record<string, unknown> = {};\n try {\n fm = extractFrontmatter(file);\n } catch {\n /* 无 frontmatter / 读不到都按空对象处理 */\n }\n fileFrontmatter.set(rel, fm);\n\n // Check required frontmatter fields (skip top-level config/index files)\n if (!shouldSkipFrontmatter(rel)) {\n for (const field of REQUIRED_FIELDS) {\n if (!fm[field]) {\n issues.push({\n file: rel,\n kind: 'missing-field',\n detail: `missing frontmatter field: ${field}`,\n });\n }\n }\n }\n\n // Extract wikilinks (ignore matches inside code blocks)\n try {\n const content = stripCodeBlocks(readFileSync(file, 'utf-8'));\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n const targets: string[] = [];\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) {\n const target = m[1].trim();\n targets.push(target);\n inboundLinks.add(target);\n }\n fileLinks.set(rel, targets);\n } catch {\n /* skip unreadable files */\n }\n }\n\n // Pass 2: broken links\n for (const [rel, targets] of fileLinks) {\n if (shouldSkipBrokenLink(rel)) continue; // 模板占位符不算死链\n for (const target of targets) {\n if (!stemSet.has(target) && !baseNameSet.has(target)) {\n issues.push({\n file: rel,\n kind: 'broken-link',\n detail: `broken link: [[${target}]]`,\n });\n }\n }\n }\n\n // Pass 3: orphan pages (no inbound links)\n for (const file of files) {\n const rel = relative(corpus, file);\n if (shouldSkipOrphan(rel)) continue;\n\n // graph-excluded 系统文件(QUESTIONS.md / overview.md / 输出/* 等)不入 Obsidian 图谱,\n // 天然\"无入链\"合理,不应报 orphan\n const fm = fileFrontmatter.get(rel) ?? {};\n if (isGraphExcluded(fm)) continue;\n\n const stem = rel.replace(/\\.md$/, '');\n const baseName = stem.split('/').pop()!;\n\n let hasInbound = inboundLinks.has(stem) || inboundLinks.has(baseName);\n\n // 文件夹包装式原料:父目录形式的引用也算入链\n if (!hasInbound && stem.endsWith('/article')) {\n const folderStem = stem.replace(/\\/article$/, '');\n const folderName = folderStem.split('/').pop()!;\n hasInbound = inboundLinks.has(folderStem) || inboundLinks.has(folderName);\n }\n\n if (!hasInbound) {\n issues.push({\n file: rel,\n kind: 'orphan',\n detail: 'orphan page (no inbound links)',\n });\n }\n }\n\n return issues;\n}\n\nexport function printLintReport(corpus: string, issues: LintIssue[]): void {\n print(chalk.bold(`\\nlorekit lint — ${corpus}\\n`));\n\n if (issues.length === 0) {\n ok('no issues found');\n print();\n return;\n }\n\n // Group by kind\n const grouped: Record<string, LintIssue[]> = {};\n for (const issue of issues) {\n (grouped[issue.kind] ??= []).push(issue);\n }\n\n const kindLabels: Record<string, string> = {\n 'missing-field': 'frontmatter',\n 'broken-link': 'broken links',\n orphan: 'orphan pages',\n };\n\n for (const [kind, items] of Object.entries(grouped)) {\n print(chalk.cyan(`── ${kindLabels[kind] ?? kind} (${items.length}) ──`));\n for (const item of items) {\n bad(`${item.file}: ${item.detail}`);\n }\n print();\n }\n\n print(chalk.yellow(`${issues.length} issue(s) total\\n`));\n}\n\nexport function lintCommand(program: Command) {\n program\n .command('lint')\n .description('check frontmatter, broken wikilinks, and orphan pages')\n .action(() => {\n const corpus = requireCorpus();\n const issues = runLint(corpus);\n printLintReport(corpus, issues);\n if (issues.length > 0) process.exitCode = 1;\n });\n}\n","import { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, basename } from 'node:path';\nimport {\n requireCorpus,\n collectMdFiles,\n hasFrontmatter,\n extractFrontmatter,\n} from '../lib/corpus.js';\nimport { tsCompact, tsMinute } from '../lib/date.js';\nimport { ok, err, print } from '../utils/logger.js';\n\nconst SEVERITY_ORDER: Record<string, number> = { high: 3, medium: 2, low: 1 };\n\ninterface AuditEntry {\n severity: string;\n sevOrder: number;\n target: string;\n status: string;\n created: string;\n preview: string;\n}\n\nfunction extractPreview(filePath: string): string {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n let inFm = false;\n for (const line of lines) {\n if (line.trimEnd() === '---') {\n if (!inFm) {\n inFm = true;\n continue;\n } else {\n inFm = false;\n continue;\n }\n }\n if (inFm) continue;\n if (line.trim() === '') continue;\n return line.trim();\n }\n return '';\n}\n\nfunction listAudit(root: string, filter: 'all' | 'open' | 'resolved'): void {\n const dirs: string[] = [];\n if (filter === 'open' || filter === 'all') dirs.push(join(root, '反馈', '待处理'));\n if (filter === 'resolved' || filter === 'all') dirs.push(join(root, '反馈', '已处理'));\n\n const entries: AuditEntry[] = [];\n\n for (const dir of dirs) {\n if (!existsSync(dir)) continue;\n const files = collectMdFiles(dir);\n for (const f of files) {\n if (basename(f) === '.gitkeep') continue;\n if (!hasFrontmatter(f)) continue;\n\n const fm = extractFrontmatter(f);\n const severity = (fm.severity as string) ?? '';\n const target = (fm.target as string) ?? '';\n const created = (fm.created as string) ?? '';\n const status = (fm.status as string) ?? '';\n const preview = extractPreview(f);\n\n entries.push({\n severity,\n sevOrder: SEVERITY_ORDER[severity] ?? 0,\n target,\n status,\n created,\n preview,\n });\n }\n }\n\n if (entries.length === 0) {\n print('No audit entries found.');\n return;\n }\n\n // Sort by severity descending\n entries.sort((a, b) => b.sevOrder - a.sevOrder);\n\n for (const e of entries) {\n print(`[${e.severity}] ${e.target} — ${e.preview} (${e.created}) [${e.status}]`);\n }\n print();\n print(`Total: ${entries.length} entries`);\n}\n\nfunction createAudit(root: string, target: string, severity: string, text: string): void {\n if (!target) {\n err('audit --create requires --target');\n process.exit(2);\n }\n if (!severity) {\n err('audit --create requires --severity');\n process.exit(2);\n }\n if (!text) {\n err('audit --create requires --text');\n process.exit(2);\n }\n\n if (!['low', 'medium', 'high'].includes(severity)) {\n err(`severity must be low|medium|high, got: ${severity}`);\n process.exit(2);\n }\n\n const slug = basename(target, '.md').replace(/[\\s/]/g, '-').toLowerCase();\n\n const now = new Date();\n const filename = `${tsCompact(now)}-${slug}.md`;\n const tsFm = tsMinute(now);\n\n const destDir = join(root, '反馈', '待处理');\n mkdirSync(destDir, { recursive: true });\n\n const dest = join(destDir, filename);\n const content = `---\ntype: audit\ntarget: ${target}\nseverity: ${severity}\nstatus: open\ncreated: ${tsFm}\n---\n\n${text}\n`;\n\n writeFileSync(dest, content, 'utf-8');\n ok(`created: 反馈/待处理/${filename}`);\n print(` target: ${target}`);\n print(` severity: ${severity}`);\n}\n\nexport function auditCommand(program: Command): void {\n const cmd = program\n .command('audit')\n .description('Human feedback loop for corpus content')\n .option('--list', 'List entries (default)')\n .option('--open', 'Only show open (待处理) entries')\n .option('--resolved', 'Only show resolved (已处理) entries')\n .option('--create', 'Create a new audit entry')\n .option('--target <file>', 'Target file path (relative to corpus root)')\n .option('--severity <level>', 'Severity: low | medium | high')\n .option('--text <text>', 'Feedback text');\n\n cmd.action((opts) => {\n const root = requireCorpus();\n\n if (opts.create) {\n createAudit(root, opts.target ?? '', opts.severity ?? '', opts.text ?? '');\n } else {\n let filter: 'all' | 'open' | 'resolved' = 'all';\n if (opts.open) filter = 'open';\n else if (opts.resolved) filter = 'resolved';\n listAudit(root, filter);\n }\n });\n}\n","/**\n * date.ts — 日期 / 时间戳 helper 集中处。\n *\n * 历史背景(LEGACY P1-2):`pad(n)`、`today()`、`todayYMD()` 等小工具散落在\n * init / index / audit / snapshot / ingest / fetcher 各文件内重复实现。本文件\n * 把它们集中起来,下游 import 即可。\n *\n * 时区策略:除 `*Shanghai*` 后缀的函数显式按 Asia/Shanghai 偏移外,其他函数\n * 默认走 JS 系统时区(在先生这台机器上 = Asia/Shanghai)。\n *\n * 后续批次(9 / 21)继续往本文件追加;先生不要在其他文件里\"另起炉灶\"。\n */\n\nconst SHANGHAI_TZ_OFFSET_MS = 8 * 60 * 60 * 1000;\n\n/** 把数字补足到 2 位(如月 / 日 / 时 / 分 / 秒) */\nexport function pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/** 今天的 YYYY-MM-DD(按 Asia/Shanghai 偏移)—— 适合记录 source_date / created / updated */\nexport function todayYMDShanghai(): string {\n const d = new Date(Date.now() + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/** 把任意 Date 格式化成 YYYY-MM-DD(**UTC**,常用于 frontmatter 解析回写) */\nexport function dateToYMDUtc(d: Date): string {\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\n/** 把任意 Date 格式化成 YYYY-MM-DD(**本地**时区,常用于 mtime 显示) */\nexport function dateToYMDLocal(d: Date): string {\n return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;\n}\n\n/**\n * 紧凑文件名时间戳:YYYYMMDD-HHMMSS(本地时区)。\n * 用在 snapshot 文件名、audit 反馈条目文件名等不希望出现冒号 / 空格的位置。\n */\nexport function tsCompact(d: Date = new Date()): string {\n return [\n d.getFullYear(),\n pad2(d.getMonth() + 1),\n pad2(d.getDate()),\n '-',\n pad2(d.getHours()),\n pad2(d.getMinutes()),\n pad2(d.getSeconds()),\n ].join('');\n}\n\n/** YYYY-MM-DD HH:MM(本地时区,audit frontmatter 用) */\nexport function tsMinute(d: Date = new Date()): string {\n return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;\n}\n","import { Command } from 'commander';\nimport { existsSync, readdirSync, readFileSync, statSync, writeFileSync, lstatSync } from 'node:fs';\nimport { join, basename, relative, resolve } from 'node:path';\nimport { requireCorpus, hasFrontmatter, extractFrontmatter } from '../lib/corpus.js';\nimport {\n indexExcludeDirPrefixes,\n isIndexExcluded,\n isFolderPackage,\n} from '../lib/paths.js';\nimport { dateToYMDUtc, dateToYMDLocal } from '../lib/date.js';\nimport { ok, warn, err } from '../utils/logger.js';\n\nfunction extractSummary(filePath: string): string {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n let found = false;\n for (const line of lines) {\n if (/^## Compiled Truth/.test(line)) {\n found = true;\n continue;\n }\n if (!found) continue;\n if (/^---\\s*$/.test(line)) break;\n if (/^## /.test(line)) break;\n if (line.trim() === '') continue;\n\n let text = line.trim().replace(/^\\*\\*[^*]*\\*\\*\\s*/, '');\n const periodMatch = text.match(/^([^。.]*[。.])/);\n if (periodMatch && periodMatch[1].length <= 50) return periodMatch[1];\n return text.slice(0, 50);\n }\n return '';\n}\n\ninterface IndexEntry {\n slug: string; // corpus 根相对路径,不含 .md;对目录包装式原料用父目录路径\n title: string; // frontmatter.title 或 basename\n summary: string; // Compiled Truth 首句或 \"—\"\n updated: string; // YYYY-MM-DD\n}\n\nfunction readEntryFromFile(filePath: string, slug: string): IndexEntry {\n let title = '';\n let updated = '';\n let summary = '';\n\n if (hasFrontmatter(filePath)) {\n const fm = extractFrontmatter(filePath);\n title = typeof fm.title === 'string' ? fm.title : fm.title != null ? String(fm.title) : '';\n\n if (fm.updated instanceof Date) {\n updated = dateToYMDUtc(fm.updated);\n } else {\n updated = fm.updated != null ? String(fm.updated) : '';\n }\n\n summary = extractSummary(filePath);\n if (!summary) summary = '—';\n } else {\n summary = '(缺少 frontmatter)';\n }\n\n if (!title) title = basename(filePath, '.md');\n\n if (!updated) {\n try {\n updated = dateToYMDLocal(statSync(filePath).mtime);\n } catch {\n updated = 'unknown';\n }\n }\n\n return { slug, title, summary, updated };\n}\n\n// 转义表格单元格里的 | 字符(防止撑散 markdown 表格)\nfunction escapeCell(s: string): string {\n return s.replace(/\\|/g, '\\\\|');\n}\n\nfunction buildIndex(dir: string, root: string): boolean {\n const reldir = dir === root ? '' : relative(root, dir);\n const dirName = reldir === '' ? basename(root) : basename(dir);\n const indexFile = join(dir, '_INDEX.md');\n\n let names: string[];\n try {\n names = readdirSync(dir, { encoding: 'utf-8' });\n } catch {\n return false;\n }\n\n const entries: IndexEntry[] = [];\n\n for (const name of names) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n\n if (stat.isFile() && name.endsWith('.md')) {\n // 普通 .md 文件:slug = 完整相对路径去 .md\n const slug = relative(root, full).replace(/\\.md$/, '');\n entries.push(readEntryFromFile(full, slug));\n } else if (stat.isDirectory() && isFolderPackage(full)) {\n // 目录包装式原料:xxx/article.md → slug = xxx 父目录路径\n const articlePath = join(full, 'article.md');\n const slug = relative(root, full);\n entries.push(readEntryFromFile(articlePath, slug));\n }\n }\n\n if (entries.length === 0) return false;\n\n entries.sort((a, b) => b.updated.localeCompare(a.updated));\n\n const lines: string[] = [];\n lines.push(`# ${dirName}`);\n lines.push('');\n lines.push(`> 本目录共 ${entries.length} 个条目。由 \\`lorekit index\\` 自动生成。`);\n lines.push('');\n lines.push('| 条目 | 摘要 | 更新 |');\n lines.push('|---|---|---|');\n for (const e of entries) {\n lines.push(`| [[${e.slug}]] | ${escapeCell(e.summary)} | ${e.updated} |`);\n }\n lines.push('');\n\n writeFileSync(indexFile, lines.join('\\n'), 'utf-8');\n const display = reldir === '' ? '_INDEX.md' : `${reldir}/_INDEX.md`;\n ok(`${display} (${entries.length} entries)`);\n return true;\n}\n\n/**\n * 递归发现\"可索引目录\":\n * - 目录下有直接 .md 文件(非 _INDEX.md / 隐藏)\n * - 或目录下有\"目录包装式原料\"子目录(xxx/article.md 形式)\n *\n * 排除规则:\n * - indexExcludeDirPrefixes 开头的目录整枝跳过\n * - corpus 根本身不索引(L0 = index.md 已承担其职能)\n * - 目录包装式原料的内部目录不递归(它们是条目,不是容器)\n */\nfunction findIndexableDirs(root: string): string[] {\n const results: string[] = [];\n\n function walk(dir: string, isRoot: boolean) {\n const rel = dir === root ? '' : relative(root, dir);\n if (rel && isIndexExcluded(rel)) return;\n\n let names: string[];\n try {\n names = readdirSync(dir, { encoding: 'utf-8' });\n } catch {\n return;\n }\n\n if (!isRoot) {\n let hasIndexable = false;\n for (const name of names) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md' || name === '.gitkeep') continue;\n\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n\n if (stat.isFile() && name.endsWith('.md')) {\n hasIndexable = true;\n break;\n }\n if (stat.isDirectory() && isFolderPackage(full)) {\n hasIndexable = true;\n break;\n }\n }\n if (hasIndexable) results.push(dir);\n }\n\n // 递归子目录(跳过目录包装式原料的内部)\n for (const name of names) {\n if (name.startsWith('.')) continue;\n const full = join(dir, name);\n let stat;\n try {\n stat = lstatSync(full);\n } catch {\n continue;\n }\n if (!stat.isDirectory()) continue;\n if (isFolderPackage(full)) continue;\n walk(full, false);\n }\n }\n\n walk(root, true);\n return results.sort();\n}\n\n/**\n * 程序内复用入口:扫 corpus 生成所有 _INDEX.md。\n * 返回生成的文件数。specificDir 限定在单个子目录(相对 root 的路径)。\n *\n * 铁律:corpus 根不建 _INDEX.md —— L0 `corpus/index.md` 已经承担根级索引职能。\n * `--dir .` / `--dir \"\"` / `--dir ./` 这类 bypass 会被拒绝。\n */\nexport function runIndex(root: string, specificDir?: string): number {\n if (specificDir) {\n const full = join(root, specificDir);\n if (!existsSync(full)) {\n throw new Error(`directory not found: ${specificDir}`);\n }\n // 防止 --dir . / --dir \"\" / --dir ./ 等写法绕过根排除,在 corpus 根生成 _INDEX.md\n if (resolve(full) === resolve(root)) {\n throw new Error(\n `cannot index the corpus root itself — L0 corpus/index.md already serves this role`,\n );\n }\n // 子目录也要守住排除规则(避免 --dir _工作台 / --dir 系统 等强行生成)\n const rel = relative(root, full);\n if (isIndexExcluded(rel)) {\n throw new Error(\n `directory \"${rel}\" is in the exclude list (${indexExcludeDirPrefixes.join(' / ')})`,\n );\n }\n return buildIndex(full, root) ? 1 : 0;\n }\n const dirs = findIndexableDirs(root);\n if (dirs.length === 0) return 0;\n let generated = 0;\n for (const d of dirs) {\n if (buildIndex(d, root)) generated++;\n }\n return generated;\n}\n\nexport function indexCommand(program: Command): void {\n const cmd = program\n .command('index')\n .description('Generate _INDEX.md recursively for corpus directories')\n .option('--dir <subdir>', 'Only update a specific subdirectory');\n\n cmd.action((opts) => {\n const root = requireCorpus();\n\n try {\n if (opts.dir) {\n runIndex(root, opts.dir);\n } else {\n const generated = runIndex(root);\n if (generated === 0) {\n warn('no indexable directories found');\n } else {\n ok(`generated ${generated} _INDEX.md file(s)`);\n }\n }\n } catch (e) {\n err((e as Error).message);\n process.exit(1);\n }\n });\n}\n","import { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n symlinkSync,\n unlinkSync,\n readlinkSync,\n lstatSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport { lorekitRoot } from '../utils/fs.js';\nimport { ok, err, out, print } from '../utils/logger.js';\n\nfunction isSymlink(path: string): boolean {\n try {\n return lstatSync(path).isSymbolicLink();\n } catch {\n return false;\n }\n}\n\nexport function installSkillsCommand(program: Command): void {\n const cmd = program\n .command('install-skills')\n .description('Install lorekit skills into a harness (e.g. Claude Code)')\n .option('--target <harness>', 'Target harness (currently only \"claude-code\")')\n .option('--list', 'List currently installed wiki-* skill symlinks')\n .option('--uninstall', 'Remove installed skill symlinks');\n\n cmd.action((opts) => {\n const skillsDest = join(process.env.HOME ?? '', '.claude', 'skills');\n\n // --list mode\n if (opts.list) {\n if (!existsSync(skillsDest)) return;\n const names = readdirSync(skillsDest, { encoding: 'utf-8' });\n for (const name of names) {\n if (!name.startsWith('wiki-')) continue;\n const full = join(skillsDest, name);\n if (!isSymlink(full)) continue;\n const target = readlinkSync(full);\n out(`${name} -> ${target}`);\n }\n return;\n }\n\n // Require --target\n if (!opts.target) {\n err('install-skills: --target required');\n process.exit(2);\n }\n if (opts.target !== 'claude-code') {\n err(`target '${opts.target}' not supported; only 'claude-code' is available`);\n process.exit(2);\n }\n\n mkdirSync(skillsDest, { recursive: true });\n\n const skillsSrc = join(lorekitRoot(), 'skills');\n if (!existsSync(skillsSrc)) {\n err(`skills directory not found: ${skillsSrc}`);\n process.exit(1);\n }\n\n // Find wiki-* skill directories\n const allNames = readdirSync(skillsSrc, { encoding: 'utf-8' });\n const skillNames = allNames.filter((name) => {\n if (!name.startsWith('wiki-')) return false;\n try {\n return lstatSync(join(skillsSrc, name)).isDirectory();\n } catch {\n return false;\n }\n });\n\n let count = 0;\n for (const name of skillNames) {\n const srcDir = join(skillsSrc, name);\n const skillFile = join(srcDir, 'SKILL.md');\n if (!existsSync(skillFile)) continue;\n\n const dest = join(skillsDest, name);\n\n if (opts.uninstall) {\n if (isSymlink(dest)) {\n unlinkSync(dest);\n ok(`removed ${name}`);\n count++;\n }\n } else {\n // Remove existing symlink if present\n if (isSymlink(dest)) unlinkSync(dest);\n\n symlinkSync(srcDir, dest);\n ok(`linked ${name}`);\n count++;\n }\n }\n\n if (count === 0) {\n print('No skills found to install.');\n } else if (!opts.uninstall) {\n print(`\\nInstalled ${count} skill(s). Restart Claude Code to load them.`);\n }\n });\n}\n","import type { Command } from 'commander';\nimport {\n existsSync,\n mkdirSync,\n writeFileSync,\n unlinkSync,\n readdirSync,\n statSync,\n} from 'node:fs';\nimport { join, relative } from 'node:path';\nimport * as tar from 'tar';\nimport { ok, bad, err } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { snapshotExcludeNames } from '../lib/paths.js';\nimport { tsCompact } from '../lib/date.js';\nimport { sha256 } from '../utils/fs.js';\n\ninterface ManifestEntry {\n path: string;\n sha256: string;\n bytes: number;\n mtime: string;\n}\n\nfunction collectAllFiles(dir: string, base: string): string[] {\n const results: string[] = [];\n\n function walk(d: string) {\n for (const entry of readdirSync(d, { withFileTypes: true })) {\n if (snapshotExcludeNames.has(entry.name)) continue;\n const full = join(d, entry.name);\n if (entry.isDirectory()) {\n walk(full);\n } else {\n results.push(relative(base, full));\n }\n }\n }\n\n walk(dir);\n return results.sort();\n}\n\nexport async function createSnapshot(corpus: string, opts: { tag?: string } = {}): Promise<string> {\n const snapshotsDir = join(corpus, '.wiki', 'snapshots');\n mkdirSync(snapshotsDir, { recursive: true });\n\n // Collect files\n const files = collectAllFiles(corpus, corpus);\n if (files.length === 0) {\n throw new Error('no files found in corpus');\n }\n\n // Build manifest\n const manifest: ManifestEntry[] = files.map((relPath) => {\n const full = join(corpus, relPath);\n const st = statSync(full);\n return {\n path: relPath,\n sha256: sha256(full),\n bytes: st.size,\n mtime: st.mtime.toISOString(),\n };\n });\n\n // Write temporary manifest into snapshots dir\n const manifestPath = join(snapshotsDir, 'manifest.json');\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n');\n\n try {\n // Build filename: YYYYMMDD-HHMMSS[-tag].tar.gz\n const tag = opts.tag ? `-${opts.tag}` : '';\n const tarName = `${tsCompact()}${tag}.tar.gz`;\n const tarPath = join(snapshotsDir, tarName);\n\n // Create tarball\n // Include all corpus files + the manifest\n const allEntries = [...files, relative(corpus, manifestPath)];\n\n await tar.create(\n {\n gzip: true,\n file: tarPath,\n cwd: corpus,\n prefix: '',\n },\n allEntries,\n );\n\n return tarPath;\n } finally {\n // LEGACY P4-2:tar.create 若抛错,manifest 原先会残留在 .wiki/snapshots/;\n // 放 finally 保证无论成功 / 失败都清掉。\n if (existsSync(manifestPath)) unlinkSync(manifestPath);\n }\n}\n\nexport function snapshotCommand(program: Command) {\n program\n .command('snapshot')\n .option('--tag <name>', 'optional tag appended to filename')\n .description('create a tarball snapshot of the corpus')\n .action(async (opts: { tag?: string }) => {\n const corpus = requireCorpus();\n try {\n const tarPath = await createSnapshot(corpus, opts);\n const tarStat = statSync(tarPath);\n const sizeMB = (tarStat.size / 1024 / 1024).toFixed(1);\n const count = collectAllFiles(corpus, corpus).length;\n ok(`snapshot saved: ${tarPath} (${count} files, ${sizeMB} MB)`);\n } catch (e) {\n const message = (e as Error).message;\n if (message === 'no files found in corpus') {\n bad(message);\n } else {\n err(message);\n process.exitCode = 1;\n }\n return;\n }\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, rmSync } from 'node:fs';\nimport { join, dirname, relative } from 'node:path';\nimport { createInterface } from 'node:readline';\nimport { tmpdir } from 'node:os';\nimport * as tar from 'tar';\nimport chalk from 'chalk';\nimport { ok, bad, err, warn, print } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { sha256 } from '../utils/fs.js';\n\ninterface ManifestEntry {\n path: string;\n sha256: string;\n bytes: number;\n mtime: string;\n}\n\ntype DiffKind = 'MISSING' | 'CHANGED';\n\ninterface DiffEntry {\n kind: DiffKind;\n path: string;\n snapshotSha: string;\n currentSha: string | null;\n}\n\nfunction ask(question: string): Promise<string> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\n/**\n * 仅限 os.tmpdir() 子目录,不许扩展到任何用户数据路径。\n *\n * LEGACY P4-3 / 先生全局 CLAUDE.md 数据安全红线:lorekit 源码里的任何\n * `rm -rf` 等价操作都必须锁定在 tmpdir 下。本函数的唯一调用方 restore\n * action 中,`tmpDir` 由 `join(tmpdir(), 'lorekit-restore-' + Date.now())`\n * 构造——禁止把 user corpus 路径传进来。\n */\nfunction rmDirRecursive(dir: string) {\n rmSync(dir, { recursive: true, force: true });\n}\n\nexport function restoreCommand(program: Command) {\n program\n .command('restore')\n .requiredOption('--from <snapshot>', 'path to snapshot .tar.gz')\n .option('--dry-run', 'only list differences, do not restore')\n .option('--file <path>', 'restore only this specific file')\n .description('restore files from a snapshot')\n .action(async (opts: { from: string; dryRun?: boolean; file?: string }) => {\n const corpus = requireCorpus();\n\n if (!existsSync(opts.from)) {\n bad(`snapshot not found: ${opts.from}`);\n // CONVENTIONS #4:用户提供的 --from 路径不存在 → 参数错 → exit 2\n process.exitCode = 2;\n return;\n }\n\n // Extract to temp dir\n const tmpDir = join(tmpdir(), `lorekit-restore-${Date.now()}`);\n mkdirSync(tmpDir, { recursive: true });\n\n try {\n await tar.extract({\n file: opts.from,\n cwd: tmpDir,\n });\n\n // Read manifest\n const manifestPath = join(tmpDir, '.wiki', 'snapshots', 'manifest.json');\n if (!existsSync(manifestPath)) {\n bad('manifest.json not found in snapshot');\n process.exitCode = 1;\n return;\n }\n\n const manifest: ManifestEntry[] = JSON.parse(readFileSync(manifestPath, 'utf-8'));\n\n // Compute diffs\n const diffs: DiffEntry[] = [];\n\n for (const entry of manifest) {\n // Skip if --file is specified and doesn't match\n if (opts.file && entry.path !== opts.file) continue;\n\n const corpusPath = join(corpus, entry.path);\n if (!existsSync(corpusPath)) {\n diffs.push({\n kind: 'MISSING',\n path: entry.path,\n snapshotSha: entry.sha256,\n currentSha: null,\n });\n } else {\n const currentSha = sha256(corpusPath);\n if (currentSha !== entry.sha256) {\n diffs.push({\n kind: 'CHANGED',\n path: entry.path,\n snapshotSha: entry.sha256,\n currentSha,\n });\n }\n }\n }\n\n if (diffs.length === 0) {\n ok('corpus matches snapshot — nothing to restore');\n return;\n }\n\n // Display diffs\n const missing = diffs.filter((d) => d.kind === 'MISSING');\n const changed = diffs.filter((d) => d.kind === 'CHANGED');\n\n if (missing.length > 0) {\n print(chalk.yellow(`\\n MISSING (${missing.length}):`));\n for (const d of missing) {\n print(` + ${d.path}`);\n }\n }\n if (changed.length > 0) {\n print(chalk.cyan(`\\n CHANGED (${changed.length}):`));\n for (const d of changed) {\n print(` ~ ${d.path}`);\n }\n }\n print();\n\n if (opts.dryRun) {\n warn(`dry-run: ${diffs.length} file(s) would be restored`);\n return;\n }\n\n // Confirm\n const answer = await ask(` restore ${diffs.length} file(s)? [y/N] `);\n if (answer.toLowerCase() !== 'y') {\n bad('cancelled');\n return;\n }\n\n // Copy files from tmpDir to corpus\n let restored = 0;\n for (const d of diffs) {\n const src = join(tmpDir, d.path);\n const dest = join(corpus, d.path);\n if (!existsSync(src)) {\n warn(`file not in snapshot archive: ${d.path}`);\n continue;\n }\n mkdirSync(dirname(dest), { recursive: true });\n copyFileSync(src, dest);\n restored++;\n }\n\n ok(`restored ${restored} file(s) from snapshot`);\n } finally {\n // Clean up temp dir\n rmDirRecursive(tmpDir);\n }\n });\n}\n","import type { Command } from 'commander';\nimport { readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { ok, bad, warn, out } from '../utils/logger.js';\nimport { requireCorpus, collectMdFiles } from '../lib/corpus.js';\n\ninterface SearchResult {\n file: string;\n line: number;\n text: string;\n}\n\nfunction searchWithRipgrep(\n query: string,\n corpus: string,\n opts: { type?: string; dir?: string },\n): SearchResult[] {\n const searchDir = opts.dir ? join(corpus, opts.dir) : corpus;\n const args: string[] = ['--json', '--no-heading', '-i'];\n\n if (opts.type) {\n args.push('--type', opts.type);\n }\n\n // Exclude internal dirs\n args.push('--glob', '!.wiki/**', '--glob', '!.git/**');\n args.push(query, searchDir);\n\n const result = spawnSync('rg', args, {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024,\n // 30s 上限:规避恶意 / 退化 regex 在巨型 corpus 上拖垮 CLI(PR #5 review M3)。\n // ripgrep 正常扫几 GB markdown 也只要几秒,30s 是宽松值。\n timeout: 30_000,\n });\n\n if (result.error) {\n // rg 未安装 → 返回空让上层 fallback;\n // 也可能是 timeout(result.signal === 'SIGTERM'),同样返回空 + 上层 fallback\n if ((result as { signal?: string }).signal === 'SIGTERM') {\n warn(`rg timed out after 30s, falling back to built-in scan`);\n }\n return [];\n }\n\n const results: SearchResult[] = [];\n for (const line of (result.stdout || '').split('\\n')) {\n if (!line.trim()) continue;\n try {\n const obj = JSON.parse(line);\n if (obj.type === 'match') {\n results.push({\n file: relative(corpus, obj.data.path.text),\n line: obj.data.line_number,\n text: obj.data.lines.text.trimEnd(),\n });\n }\n } catch {\n // skip malformed lines\n }\n }\n return results;\n}\n\nfunction searchFallback(query: string, corpus: string, opts: { dir?: string }): SearchResult[] {\n const searchDir = opts.dir ? join(corpus, opts.dir) : corpus;\n const files = collectMdFiles(searchDir);\n const pattern = new RegExp(query, 'i');\n const results: SearchResult[] = [];\n\n for (const filePath of files) {\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n if (pattern.test(lines[i])) {\n results.push({\n file: relative(corpus, filePath),\n line: i + 1,\n text: lines[i].trimEnd(),\n });\n }\n }\n }\n return results;\n}\n\nfunction hasRipgrep(): boolean {\n const result = spawnSync('rg', ['--version'], { encoding: 'utf-8' });\n return !result.error && result.status === 0;\n}\n\nexport function searchCommand(program: Command) {\n program\n .command('search')\n .argument('<query>', 'search query (regex supported)')\n .option('--type <t>', 'file type filter (passed to rg --type)')\n .option('--dir <d>', 'subdirectory within corpus to search')\n .description('search the corpus with ripgrep (fallback: built-in)')\n .action((query: string, opts: { type?: string; dir?: string }) => {\n const corpus = requireCorpus();\n\n let results: SearchResult[];\n\n if (hasRipgrep()) {\n results = searchWithRipgrep(query, corpus, opts);\n } else {\n warn('rg (ripgrep) not found, using built-in fallback');\n results = searchFallback(query, corpus, { dir: opts.dir });\n }\n\n // TODO Phase 3: if .wiki/vector.sqlite exists, also run vector similarity\n // search and merge results with text search hits.\n\n // Output JSON lines\n for (const r of results) {\n out(JSON.stringify(r));\n }\n\n if (results.length === 0) {\n warn('no results');\n }\n });\n}\n","import type { Command } from 'commander';\nimport { createHash } from 'node:crypto';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { ok, warn, err, out, print } from '../utils/logger.js';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { pruneMissingDocuments } from '../lib/vectordb/prune.js';\n\nexport interface VectorSyncOptions {\n force?: boolean;\n layered?: boolean;\n model?: string;\n}\n\nexport interface VectorSyncResult {\n synced: number;\n skipped: number;\n totalChunks: number;\n layered: boolean;\n pruned: number;\n}\n\n/**\n * 程序内复用入口:增量同步向量库。\n * - 每个 .md 文件用 sha256 对比跳过未变更\n * - --force 全量重嵌入\n * - --layered 额外刷 L0/L1(默认 true——lorekit sync 需要)\n */\nexport async function runVectorSync(\n corpus: string,\n opts: VectorSyncOptions = {},\n): Promise<VectorSyncResult> {\n const force = opts.force ?? false;\n const layered = opts.layered ?? true;\n const model = opts.model ?? 'bge-m3';\n\n const { embed, embedSingle } = await import('../lib/ollama.js');\n const { openDb, syncFile, buildLayeredIndex, collectFiles } = await import('../lib/vectordb/index.js');\n\n const testEmb = await embedSingle('test', model);\n const dim = testEmb.length;\n\n const db = await openDb(corpus, dim);\n const files = collectFiles(corpus);\n const existingRelPaths = new Set(files.map((filePath) => relative(corpus, filePath)));\n const pruned = pruneMissingDocuments(db, existingRelPaths);\n if (pruned > 0) warn(`vector sync pruned ${pruned} missing file(s)`);\n\n let synced = 0;\n let skipped = 0;\n let totalChunks = 0;\n\n for (const filePath of files) {\n // LEGACY P4-5:原先 `filePath.replace(corpus + '/', '')` 字符串替换,\n // 若 corpus 路径在 filePath 内出现多次(罕见)会替换错位;用 path.relative 更稳。\n const rel = relative(corpus, filePath);\n\n if (!force) {\n const row = db.prepare('SELECT sha256 FROM documents WHERE path = ?').get(rel) as\n | { sha256: string }\n | undefined;\n if (row) {\n const sha = createHash('sha256').update(readFileSync(filePath)).digest('hex');\n if (row.sha256 === sha) {\n skipped++;\n continue;\n }\n }\n }\n\n const embedFn = (texts: string[]) => embed(texts, model);\n const result = await syncFile(db, filePath, corpus, embedFn);\n totalChunks += result.chunks;\n synced++;\n }\n\n const now = new Date().toISOString();\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('last_sync', ?)\").run(now);\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('model', ?)\").run(model);\n db.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('dim', ?)\").run(String(dim));\n\n if (layered || force) {\n print('Building layered index (L0/L1)...');\n const embedBatch = (texts: string[]) => embed(texts, model);\n await buildLayeredIndex(db, corpus, embedBatch);\n }\n\n db.close();\n\n return { synced, skipped, totalChunks, layered: layered || force, pruned };\n}\n\nexport function vectorCommand(program: Command) {\n const vec = program\n .command('vector')\n .description('vector search engine — embed & search via ollama + sqlite-vec');\n\n // --- sync ---\n vec\n .command('sync')\n .option('--force', 'full rebuild (re-embed all files)', false)\n .option('--layered', 'build L0/L1 layered index', false)\n .option('--model <name>', 'ollama model name', 'bge-m3')\n .description('index corpus into vector DB')\n .action(async (opts: { force: boolean; layered: boolean; model: string }) => {\n const corpus = requireCorpus();\n const r = await runVectorSync(corpus, opts);\n ok(`synced ${r.synced} files (${r.totalChunks} chunks), skipped ${r.skipped} unchanged`);\n });\n\n // --- query ---\n vec\n .command('query')\n .requiredOption('--text <text>', 'search query text')\n .option('--top-k <n>', 'number of results', '5')\n .option('--threshold <n>', 'minimum similarity score', '0.5')\n .option('--layered', 'use L0→L1→L2 layered vector retrieval', false)\n .option('--hybrid', 'BM25 + vector layered + RRF fusion (阶段 2 推荐,无 re-rank)', false)\n .option('--bm25', 'BM25 layered only (FTS5, 用于 debug BM25 单路)', false)\n .option('--model <name>', 'ollama model name', 'bge-m3')\n .description('search the vector/FTS index')\n .action(\n async (opts: {\n text: string;\n topK: string;\n threshold: string;\n layered: boolean;\n hybrid: boolean;\n bm25: boolean;\n model: string;\n }) => {\n const corpus = requireCorpus();\n const topK = parseInt(opts.topK, 10);\n const threshold = parseFloat(opts.threshold);\n\n // CONVENTIONS #4:参数解析失败 → exit 2\n if (!Number.isFinite(topK) || topK <= 0) {\n err(`--top-k must be a positive integer, got: \"${opts.topK}\"`);\n process.exit(2);\n }\n if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {\n err(`--threshold must be a number in [0, 1], got: \"${opts.threshold}\"`);\n process.exit(2);\n }\n\n const { embedSingle } = await import('../lib/ollama.js');\n const { openDb, queryFlat, queryLayered, queryBM25Layered, queryHybrid } =\n await import('../lib/vectordb/index.js');\n\n // Probe dim from existing db or model\n let dim = 1024;\n const dbPath = join(corpus, '.wiki', 'vector.sqlite');\n if (existsSync(dbPath)) {\n const tmpDb = await openDb(corpus);\n const row = tmpDb.prepare(\"SELECT value FROM meta WHERE key = 'dim'\").get() as\n | { value: string }\n | undefined;\n if (row) dim = parseInt(row.value, 10);\n tmpDb.close();\n }\n\n const db = await openDb(corpus, dim);\n\n let results;\n if (opts.bm25) {\n // BM25 单路(不需要 embedding,快)\n results = queryBM25Layered(db, opts.text, topK);\n } else if (opts.hybrid) {\n const embedding = await embedSingle(opts.text, opts.model);\n results = queryHybrid(db, embedding, opts.text, topK, threshold);\n } else {\n const embedding = await embedSingle(opts.text, opts.model);\n results = opts.layered\n ? queryLayered(db, embedding, topK, threshold)\n : queryFlat(db, embedding, topK, threshold);\n }\n\n db.close();\n out(JSON.stringify(results, null, 2));\n },\n );\n\n // --- status ---\n vec\n .command('status')\n .description('show vector index status')\n .action(async () => {\n const corpus = requireCorpus();\n const { getStatus } = await import('../lib/vectordb/index.js');\n const info = await getStatus(corpus);\n out(JSON.stringify(info, null, 2));\n });\n}\n","import { relative } from 'node:path';\nimport { collectFiles } from './files.js';\nimport { openDb, type Db } from './schema.js';\n\nexport function pruneMissingDocuments(db: Db, existingRelPaths: Set<string>): number {\n const rows = db.prepare('SELECT id, path FROM documents').all() as { id: number; path: string }[];\n const missing = rows.filter((row) => !existingRelPaths.has(row.path));\n if (missing.length === 0) return 0;\n\n const delVecChunk = db.prepare('DELETE FROM vec_chunks WHERE rowid = ?');\n const delFtsChunk = db.prepare('DELETE FROM fts_chunks WHERE rowid = ?');\n const delVecPage = db.prepare('DELETE FROM vec_pages WHERE rowid = ?');\n const delFtsPage = db.prepare('DELETE FROM fts_pages WHERE rowid = ?');\n const getChunkIds = db.prepare('SELECT id FROM chunks WHERE doc_id = ?');\n const getPageIds = db.prepare('SELECT id FROM page_summaries WHERE doc_id = ?');\n const deleteChunks = db.prepare('DELETE FROM chunks WHERE doc_id = ?');\n const deletePages = db.prepare('DELETE FROM page_summaries WHERE doc_id = ?');\n const deleteDoc = db.prepare('DELETE FROM documents WHERE id = ?');\n\n const tx = db.transaction((docs: typeof missing) => {\n for (const doc of docs) {\n const chunkIds = getChunkIds.all(doc.id) as { id: number }[];\n for (const { id } of chunkIds) {\n delVecChunk.run(id);\n delFtsChunk.run(id);\n }\n deleteChunks.run(doc.id);\n\n const pageIds = getPageIds.all(doc.id) as { id: number }[];\n for (const { id } of pageIds) {\n delVecPage.run(id);\n delFtsPage.run(id);\n }\n deletePages.run(doc.id);\n deleteDoc.run(doc.id);\n }\n });\n tx(missing);\n return missing.length;\n}\n\nexport async function pruneVectorDbMissingFiles(corpus: string): Promise<number> {\n const db = await openDb(corpus);\n try {\n const files = collectFiles(corpus);\n const existingRelPaths = new Set(files.map((filePath) => relative(corpus, filePath)));\n return pruneMissingDocuments(db, existingRelPaths);\n } finally {\n db.close();\n }\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { findCorpus, findSourceByUrl, extractFrontmatter } from '../lib/corpus.js';\nimport { fetchUrl, fetchGist, fetchGithubDoc } from '../lib/fetcher/index.js';\nimport type { FetchResult } from '../lib/fetcher/index.js';\nimport { getIngestRecord, upsertIngestRecord, nextStepHint } from '../lib/ingest-state.js';\n\n// ---------------------------------------------------------------------------\n// URL routing helpers\n// ---------------------------------------------------------------------------\n\nfunction suggestResult(route: string, url: string, suggest: string): FetchResult {\n return { status: 'unsupported', route, url, suggest };\n}\n\nfunction getHost(url: string): string {\n try {\n return new URL(url).hostname.toLowerCase();\n } catch {\n return '';\n }\n}\n\nfunction isPdfUrl(url: string): boolean {\n try {\n const path = new URL(url).pathname.toLowerCase();\n return path.endsWith('.pdf');\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Command\n// ---------------------------------------------------------------------------\n\nexport function fetchCommand(program: Command) {\n program\n .command('fetch')\n .argument('<url>', 'URL to fetch')\n .option('--out <dir>', 'output directory')\n .option('--force-rich', 'skip host routing, always use rich fetcher')\n .option('--no-images', 'skip image downloads')\n .option('--force', 'ignore duplicate-URL check and re-fetch anyway')\n .description('Fetch a URL into local markdown + images')\n .action(\n async (\n url: string,\n opts: { out?: string; forceRich?: boolean; images?: boolean; force?: boolean },\n ) => {\n // Resolve output root\n const corpus = findCorpus();\n let outRoot: string;\n if (opts.out) {\n outRoot = opts.out;\n } else {\n outRoot = corpus ? join(corpus, '_工作台', '收件', 'fetch') : '/tmp/lorekit-fetch';\n }\n if (!existsSync(outRoot)) {\n mkdirSync(outRoot, { recursive: true });\n }\n\n // Duplicate / resume detection: consult ingest-state.json first,\n // fall back to scanning 原料/*/*/article.md frontmatter for legacy ingests\n // without a state record.\n let duplicate: FetchResult['duplicate'] | undefined;\n if (corpus && !opts.force) {\n const state = getIngestRecord(corpus, url);\n\n if (state && state.status !== 'completed') {\n // Interrupted ingest — surface resume hint, do not re-fetch\n const hint = nextStepHint(state);\n console.error(\n `[lorekit fetch] in-progress ingest detected for ${url}\\n` +\n ` status: ${state.status} steps done: ${state.stepsDone.join(', ') || '(none)'}\\n` +\n ` started: ${state.startedAt}\\n` +\n ` next step → ${hint}\\n` +\n ` use --force to restart from scratch`,\n );\n console.log(\n JSON.stringify({\n status: 'in_progress',\n route: 'rich',\n url,\n ingestState: state,\n nextStep: hint,\n }),\n );\n return;\n }\n\n if (state && state.status === 'completed') {\n duplicate = {\n path: state.archivedTo ?? '(unknown)',\n sourceDate: state.sourceDate,\n title: state.title,\n };\n } else {\n // No state record — fall back to frontmatter scan\n const existing = findSourceByUrl(corpus, url);\n if (existing) {\n const fm = extractFrontmatter(existing);\n const sdRaw = fm.source_date;\n const sourceDate =\n typeof sdRaw === 'string'\n ? sdRaw\n : sdRaw instanceof Date\n ? sdRaw.toISOString().slice(0, 10)\n : undefined;\n duplicate = {\n path: relative(corpus, existing),\n sourceDate,\n title: typeof fm.title === 'string' ? fm.title : undefined,\n };\n }\n }\n\n if (duplicate) {\n console.error(\n `[lorekit fetch] duplicate url: ${url} already ingested at ${duplicate.path}` +\n (duplicate.sourceDate ? ` (source_date: ${duplicate.sourceDate})` : '') +\n `. Use --force to re-fetch anyway.`,\n );\n console.log(JSON.stringify({ status: 'duplicate', route: 'rich', url, duplicate }));\n return;\n }\n }\n\n // Route by host (unless --force-rich)\n const noImages = opts.images === false;\n let result: FetchResult;\n\n if (opts.forceRich) {\n result = await fetchUrl(url, { outRoot, noImages });\n } else {\n const host = getHost(url);\n\n if (host.includes('mp.weixin.qq.com')) {\n result = await fetchUrl(url, { outRoot, noImages });\n } else if (host.includes('feishu.cn') || host.includes('larkoffice.com')) {\n result = suggestResult('lark', url, 'lark-cli docs +read --as user --doc <url>');\n } else if (\n host === 'x.com' ||\n host === 'twitter.com' ||\n host.endsWith('.x.com') ||\n host.endsWith('.twitter.com')\n ) {\n result = suggestResult('x', url, 'paste screenshot or text (antibot too strong)');\n } else if (host === 'gist.github.com' || host === 'gist.githubusercontent.com') {\n result = await fetchGist(url, outRoot);\n } else if (host === 'github.com' || host === 'www.github.com') {\n result = await fetchGithubDoc(url, outRoot);\n } else if (isPdfUrl(url)) {\n result = suggestResult('pdf', url, 'pdf skill');\n } else {\n // Generic site\n result = await fetchUrl(url, { outRoot, noImages });\n }\n }\n\n // On successful fetch into a corpus, record state so subsequent runs\n // see this as an in-progress ingest until the agent marks it completed.\n if (corpus && result.status === 'ok' && result.markdown) {\n upsertIngestRecord(corpus, url, {\n title: result.title,\n sourceDate: result.publishDate,\n status: 'started',\n stepsDone: ['fetch'],\n workbenchMd: result.markdown,\n });\n }\n\n // Output single-line JSON\n console.log(JSON.stringify(result));\n\n // Exit with non-zero on error\n if (result.status === 'error') {\n process.exitCode = 1;\n }\n },\n );\n}\n","/**\n * fetcher/index.ts — fetcher 子模块对外主入口(barrel + fetchUrl 主流程)\n *\n * 批次 21g-pre strangler fig 第七步:建主入口模块。本文件含:\n * - `fetchUrl`:从 src/lib/fetcher.ts:493-606 copy,dispatcher 模式根据 site\n * 选 routes/web 或 routes/weixin 的 parser。frontmatter 拼装替换为 21b\n * buildFrontmatter(routeKind = 'article' | 'clipping')\n * - `fetchGist` / `fetchGithubDoc`:从 routes 直接 re-export\n * - `FetchResult` / `FetchOptions`:从 types.ts 直接 re-export(type-only)\n *\n * **不 re-export** parser / helpers / http / images / frontmatter —— 这些是\n * fetcher 子模块内部实现细节,对外 surface 只有 4 个公开 API。\n *\n * 21g-pre 阶段:本文件目前未被任何调用方 import;commands/fetch.ts 仍 import\n * 旧 src/lib/fetcher.ts。21g-final 才切换 + 删旧。\n *\n * **Strangler fig 双份代码状态**:\n * - 旧 src/lib/fetcher.ts 的 fetchUrl / fetchGist / fetchGithubDoc 完整保留\n * - 新 src/lib/fetcher/index.ts 的 fetchUrl 与旧版 byte 等价(21g-pre parity 验证)\n * - 21g-final commit 一旦完成切换 + 删旧,本文件就成为 SSOT\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { buildFrontmatter } from './frontmatter.js';\nimport { htmlToMarkdown, slugify, todayYMD } from './helpers.js';\nimport {\n buildHeaders,\n detectAntibot,\n detectSite,\n fetchHtmlL1,\n fetchHtmlL2,\n PrivateAddressError,\n} from './http.js';\nimport { downloadImages, rewriteMarkdownImages } from './images.js';\nimport { parseGeneric } from './routes/web.js';\nimport { parseWeixin } from './routes/weixin.js';\nimport type { FetchOptions, FetchResult } from './types.js';\n\n// ---------------------------------------------------------------------------\n// fetchUrl 主入口(dispatcher)\n// ---------------------------------------------------------------------------\n\n/**\n * 主入口:URL → 本地 markdown + 图片。\n *\n * Pipeline:\n * 1. detectSite 选 weixin / generic 的 headers / parser\n * 2. L1 fetch;命中 antibot 关键字 → 清空 html\n * 3. L1 失败或被拦 → L2 playwright fallback;仍失败 → 报 ANTIBOT_BLOCKED\n * 4. site 选 parseWeixin / parseGeneric\n * 5. body 太短报 empty_body\n * 6. htmlToMarkdown → 写文件(含 21b buildFrontmatter)+ 下载图 + 改写图链\n *\n * 与旧 fetcher.ts:493-606 byte 等价(21g-pre parity 验证 generic + weixin 各一例)。\n */\nexport async function fetchUrl(url: string, opts: FetchOptions): Promise<FetchResult> {\n const site = detectSite(url);\n const headers = buildHeaders(site);\n let sourceLayer = 'L1';\n let html = '';\n\n // --- L1 fetch ---\n try {\n html = await fetchHtmlL1(url, headers);\n if (detectAntibot(html, site)) {\n html = '';\n }\n } catch (e) {\n // SSRF guard 拒绝时直接冒泡为 FetchResult error,不退 L2 fallback\n // (L2 playwright 同样会被绕过 guard 命中私网,必须显式拒绝)\n if (e instanceof PrivateAddressError) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'PRIVATE_ADDRESS_BLOCKED',\n suggest:\n `target resolves to private address ${e.address}. ` +\n 'Set LOREKIT_FETCH_ALLOW_PRIVATE=1 to allow (local dev only).',\n };\n }\n // 其它 L1 失败(HTTP 非 2xx / abort / 网络错误)→ 退 L2 fallback\n html = '';\n }\n\n // --- L2 fallback ---\n if (!html) {\n sourceLayer = 'L2';\n const l2html = await fetchHtmlL2(url);\n if (!l2html) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'ANTIBOT_BLOCKED',\n suggest: 'Install playwright-core + chromium, or paste content manually',\n };\n }\n html = l2html;\n if (detectAntibot(html, site)) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'ANTIBOT_BLOCKED',\n suggest: 'Site requires login or manual intervention',\n };\n }\n }\n\n // --- Parse ---\n const doc = site === 'weixin' ? parseWeixin(html, url) : parseGeneric(html, url);\n\n if (!doc.bodyHtml || doc.bodyHtml.replace(/<[^>]*>/g, '').trim().length < 50) {\n return {\n status: 'error',\n route: 'rich',\n url,\n reason: 'empty_body',\n };\n }\n\n // --- Convert to markdown ---\n let md = htmlToMarkdown(doc.bodyHtml);\n\n // --- Output paths (Obsidian-compatible flat layout) ---\n // <outRoot>/<slug>.md\n // <outRoot>/<slug>.assets/img_01.jpg\n const slug = slugify(doc.title || 'untitled');\n const assetsDir = join(opts.outRoot, `${slug}.assets`);\n await mkdir(opts.outRoot, { recursive: true });\n\n // --- Download images ---\n let imagesOk = 0;\n let imagesFailed = 0;\n if (!opts.noImages && doc.imgSrcs.length > 0) {\n const imgResults = await downloadImages(doc.imgSrcs, assetsDir, headers, `./${slug}.assets/`);\n md = rewriteMarkdownImages(md, imgResults);\n for (const r of imgResults) {\n if (r.status === 'ok') imagesOk++;\n else imagesFailed++;\n }\n }\n\n // --- Build frontmatter + write article.md ---\n // Follows templates/default-corpus/系统/frontmatter-spec.md\n // 21b buildFrontmatter(routeKind 二元)替换原 inline fmLines 拼装;title/\n // author 在 generic/weixin 路由都是条件输出(21b 已用 generic-full /\n // weixin-no-author-no-date / generic-no-title-no-author 三 case 验证 byte 等价)\n const sourceKind: 'article' | 'clipping' = site === 'weixin' ? 'clipping' : 'article';\n const today = todayYMD();\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: sourceKind,\n title: doc.title,\n today,\n url,\n author: doc.author,\n publishDate: doc.publishDate,\n }),\n );\n fmLines.push('');\n if (doc.title) fmLines.push(`# ${doc.title}`, '');\n fmLines.push(md, '');\n\n const articlePath = join(opts.outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'rich',\n url,\n title: doc.title || undefined,\n author: doc.author || undefined,\n publishDate: doc.publishDate,\n sourceKind,\n sourceLayer,\n slug,\n markdown: articlePath,\n assetsDir,\n imagesOk,\n imagesFailed,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API re-exports\n// ---------------------------------------------------------------------------\n\nexport { fetchGist } from './routes/gist.js';\nexport { fetchGithubDoc } from './routes/github.js';\nexport type { FetchOptions, FetchResult } from './types.js';\n","/**\n * fetcher/frontmatter.ts — 4 路由共用的 frontmatter 拼装\n *\n * 批次 21b strangler fig 第二步:从 src/lib/fetcher.ts 抽出 frontmatter 生成逻辑。\n * 原 fetcher.ts 仍保留 3 处内嵌拼装代码(generic/weixin 共一处 + gist + github),\n * commands/*.ts 暂未切换,本文件目前未被使用,21f 才切换并删旧文件。\n *\n * ## 4 路由字段差异矩阵\n *\n * | 字段 | generic/weixin | gist | github |\n * | ----------------------------- | ----------------- | ------------- | ----------- |\n * | `type: source` | 总有 | 总有 | 总有 |\n * | `title: \"...\"` | **条件** if title | 总有 | 总有 |\n * | `created: <today>` | 总有 | 总有 | 总有 |\n * | `updated: <today>` | 总有 | 总有 | 总有 |\n * | `source_url: <url>` | 总有 | 总有 | 总有 |\n * | `source_author: \"...\"` | **条件** if 有 | 总有 | 总有 |\n * | `source_date: <YMD>` | 条件 if 有 | 条件 if 有 | **从不输出**|\n * | `source_kind: <kind>` | article/clipping | 固定 gist | 固定 github |\n *\n * 字段顺序、引号风格、缺字段不输出的语义都按上表,与原 4 路由内嵌实现 byte-level 等价。\n *\n * ## byte-level 一致性验证(手动)\n *\n * 用 4 mock 输入分别比对 buildFrontmatter() 和原 fetcher.ts 的 fmLines 数组,\n * `JSON.stringify(actual) === JSON.stringify(expected)` 全部 pass,\n * 详见 `tmp/frontmatter-parity-check.mjs`(一次性脚本,跑完 21b 即可丢弃)。\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * 4 路由的 sourceKind 取值。注意 weixin 写出来是 `clipping`(剪藏),\n * generic 是 `article`,gist / github 同名。\n */\nexport type RouteKind = 'article' | 'clipping' | 'gist' | 'github';\n\nexport interface BuildFrontmatterOpts {\n /** sourceKind,决定 `source_kind:` 字段值,也决定 title/author 必输出还是条件输出 */\n routeKind: RouteKind;\n /** 文章标题。generic/weixin 路由可空(不输出 title 行);gist/github 路由必填 */\n title?: string;\n /** 创建/更新日期 `YYYY-MM-DD`,调用方传入(一般是 todayYMD()) */\n today: string;\n /** 抓取来源 URL */\n url: string;\n /** 作者。generic/weixin 路由可空(不输出 source_author 行);gist/github 路由必填 */\n author?: string;\n /** 发布日期 `YYYY-MM-DD`。github 路由忽略此字段(永远不输出) */\n publishDate?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * YAML 双引号字符串内嵌双引号需 `\\\"` 转义。原 4 路由都用同一句 `replace(/\"/g, '\\\\\"')`。\n */\nfunction escapeDoubleQuote(s: string): string {\n return s.replace(/\"/g, '\\\\\"');\n}\n\n// ---------------------------------------------------------------------------\n// Public\n// ---------------------------------------------------------------------------\n\n/**\n * 生成 frontmatter YAML 块(含外层 `---` 起止符),返回行数组。\n *\n * 调用方拿到后用 `lines.push(...buildFrontmatter(opts))` 拼到文章 fmLines 里。\n * 拼接后的下一行通常是空行,再跟正文 `# title` 等。\n *\n * 不负责 slug / 写文件 / 正文 —— 仅 frontmatter 一段。\n *\n * 字段输出语义见文件头注释的差异矩阵。\n */\nexport function buildFrontmatter(opts: BuildFrontmatterOpts): string[] {\n const { routeKind, title, today, url, author, publishDate } = opts;\n const omitPublishDate = routeKind === 'github';\n\n const lines: string[] = ['---'];\n lines.push('type: source');\n\n // title / author 在 generic/weixin 是条件输出;在 gist/github 调用方\n // 必传非空字符串,分支同样命中。统一用 truthy 判定,与原 4 路由 `if (xxx)` 等价。\n if (title) {\n lines.push(`title: \"${escapeDoubleQuote(title)}\"`);\n }\n // slug 留空:fetcher 不知道最终归档位置(_工作台 vs 原料/剪藏 vs 原料/文章),\n // wiki-ingest 在 mv 时再补。语义同原代码注释。\n\n lines.push(`created: ${today}`);\n lines.push(`updated: ${today}`);\n lines.push(`source_url: ${url}`);\n\n if (author) {\n lines.push(`source_author: \"${escapeDoubleQuote(author)}\"`);\n }\n\n if (!omitPublishDate && publishDate) {\n lines.push(`source_date: ${publishDate}`);\n }\n\n lines.push(`source_kind: ${routeKind}`);\n lines.push('---');\n\n return lines;\n}\n","/**\n * fetcher/helpers.ts — 通用工具函数(slug / url / markdown / 日期)\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * self-contained:不 import fetcher 子目录下其他模块。\n */\nimport TurndownService from 'turndown';\n\n// ---------------------------------------------------------------------------\n// 字符串 / URL helper\n// ---------------------------------------------------------------------------\n\n/**\n * 把任意字符串裁成可作文件名的 slug:\n * 保留 word 字符(含中文),其他符号收敛成 `-`,限长 50;空串回退 `untitled`。\n */\nexport function slugify(s: string): string {\n let slug = s.replace(/[^\\w\\u4e00-\\u9fff-]+/g, '-').replace(/^-+|-+$/g, '');\n return slug.slice(0, 50) || 'untitled';\n}\n\n/**\n * 相对 URL 解析为绝对 URL;解析失败则原样返回,避免异常向上抛。\n */\nexport function resolveUrl(src: string, base: string): string {\n try {\n return new URL(src, base).href;\n } catch {\n // URL 构造失败说明输入已经是非法 URI(典型如 `data:` / 空字符串),\n // 维持原值让上游 caller 自己决定怎么处理。\n return src;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTML -> Markdown\n// ---------------------------------------------------------------------------\n\n/**\n * Turndown 包装:固定 ATX heading + fenced code 风格,trim 末尾空白。\n */\nexport function htmlToMarkdown(html: string): string {\n const td = new TurndownService({\n headingStyle: 'atx',\n codeBlockStyle: 'fenced',\n });\n return td.turndown(html).trim();\n}\n\n// ---------------------------------------------------------------------------\n// 日期 helper(Asia/Shanghai 视角)\n// ---------------------------------------------------------------------------\n\nconst SHANGHAI_TZ_OFFSET_MS = 8 * 60 * 60 * 1000;\n\n/**\n * 把 unix seconds 时间戳格式化为 `YYYY-MM-DD`(Asia/Shanghai)。\n */\nexport function tsToYMD(seconds: number): string {\n const d = new Date(seconds * 1000 + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * 当前日期 `YYYY-MM-DD`(Asia/Shanghai)。\n *\n * 注意:批次 9 已建立 `src/lib/date.ts`,迁移到统一 helper 的工作留给批次 21\n * 后续子批一起做(见 LEGACY 批次 9 备注)。本批次仅 copy 保持行为一致。\n */\nexport function todayYMD(): string {\n const d = new Date(Date.now() + SHANGHAI_TZ_OFFSET_MS);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * 把人类常见格式的日期文本归一化为 `YYYY-MM-DD`。\n * 支持 ISO(`2026-04-15` / `2026/04/15` / 带 `T...`)和中文(`2026年4月15日`)。\n * 无法识别返回 undefined。\n */\nexport function normalizeDateText(raw: string): string | undefined {\n const s = raw.trim();\n if (!s) return undefined;\n // ISO-ish: 2026-04-15, 2026/04/15, 2026-04-15T10:00:00+08:00\n const iso = s.match(/(\\d{4})[-/.](\\d{1,2})[-/.](\\d{1,2})/);\n if (iso) {\n const [, y, m, d] = iso;\n return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;\n }\n // Chinese: 2026年4月15日\n const zh = s.match(/(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*日/);\n if (zh) {\n const [, y, m, d] = zh;\n return `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;\n }\n return undefined;\n}\n","/**\n * fetcher/http.ts — 站点检测、请求头、antibot 探测、L1/L2 抓页\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * 依赖关系:本文件不 import fetcher/helpers.ts 或 fetcher/images.ts,self-contained。\n * `HTTP_TIMEOUT_MS` 同时被 fetcher/images.ts 引用(images→http 单向依赖)。\n *\n * SSRF 防御(PR #5):\n * `fetchHtmlL1` 默认拒绝抓取解析到 RFC 1918 / RFC 6890 私网范围的目标,\n * 并手动跟随 redirect(最多 5 跳)逐跳重检,避免 redirect-to-private-ip 绕过。\n * 用户可通过环境变量 `LOREKIT_FETCH_ALLOW_PRIVATE=1` opt-out(本地开发 / 自建\n * localhost wiki 场景)。注意:该检查仅作用于 fetcher,**不影响** `lib/ollama.ts`\n * 的本地 localhost:11434 调用(那是另一个 import 路径,未经 http.ts)。\n */\n\nimport { lookup } from 'node:dns/promises';\nimport { BlockList, isIP } from 'node:net';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst UA_IPHONE =\n 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ' +\n 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 ' +\n 'Mobile/15E148 Safari/604.1';\n\nconst UA_DESKTOP =\n 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +\n 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';\n\n/**\n * L1 / L2 抓页超时(ms)。images.ts 下载图片时也复用此常量。\n */\nexport const HTTP_TIMEOUT_MS = 20_000;\n\n/**\n * 手动跟随 redirect 的最大跳数。超过即拒绝(防 redirect 循环 / SSRF 跳板)。\n */\nconst MAX_REDIRECTS = 5;\n\nconst ANTIBOT_TRIGGERS = [\n '环境异常',\n '请在微信客户端打开',\n '完成验证后即可继续',\n 'Just a moment',\n 'cf-browser-verification',\n];\n\n// ---------------------------------------------------------------------------\n// SSRF guard\n// ---------------------------------------------------------------------------\n\n/**\n * 私网 / 链路本地 / loopback 地址段。RFC 1918 / RFC 6890 / RFC 4193。\n * 与 `node:net.BlockList` 配合使用做 IP 归属判断。\n */\nconst PRIVATE_BLOCKS = (() => {\n const bl = new BlockList();\n // IPv4 RFC 1918 + loopback + link-local\n bl.addSubnet('127.0.0.0', 8, 'ipv4');\n bl.addSubnet('10.0.0.0', 8, 'ipv4');\n bl.addSubnet('172.16.0.0', 12, 'ipv4');\n bl.addSubnet('192.168.0.0', 16, 'ipv4');\n bl.addSubnet('169.254.0.0', 16, 'ipv4');\n bl.addSubnet('0.0.0.0', 8, 'ipv4'); // 0.0.0.0/8 — \"this network\"\n // IPv6 loopback + ULA + link-local\n bl.addAddress('::1', 'ipv6');\n bl.addSubnet('fc00::', 7, 'ipv6');\n bl.addSubnet('fe80::', 10, 'ipv6');\n return bl;\n})();\n\n/**\n * 由 SSRF guard 抛出的 sentinel error。调用方可用 `instanceof` 识别后短路 L2\n * fallback,把私网拒绝原因冒泡给用户(而不是被通用 catch 误判为 antibot)。\n */\nexport class PrivateAddressError extends Error {\n readonly code = 'SSRF_PRIVATE_ADDRESS';\n constructor(\n message: string,\n public readonly host: string,\n public readonly address: string,\n ) {\n super(message);\n this.name = 'PrivateAddressError';\n }\n}\n\nfunction isPrivateOptOut(): boolean {\n const v = process.env.LOREKIT_FETCH_ALLOW_PRIVATE;\n return v === '1' || v === 'true';\n}\n\n/**\n * 检查给定 host 的 DNS 解析结果是否落在私网范围。\n * - host 已经是 IP 字面量:直接判定\n * - 否则 `dns.lookup()` 拿 A/AAAA 记录后判定\n *\n * 命中即抛 `PrivateAddressError`;未命中静默返回。\n * 若环境变量 `LOREKIT_FETCH_ALLOW_PRIVATE=1` 已设置,本函数直接跳过检查。\n */\nexport async function assertPublicAddress(urlStr: string): Promise<void> {\n if (isPrivateOptOut()) return;\n\n let parsed: URL;\n try {\n parsed = new URL(urlStr);\n } catch {\n // URL 无法解析交给上游 fetch 报错,不在 SSRF 层判\n return;\n }\n\n // 仅检查 http(s);其它 scheme(data:/file:)由上游决定是否拒绝\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;\n\n const rawHost = parsed.hostname;\n // URL.hostname 对 IPv6 会带方括号 [::1],去掉以匹配 isIP / dns.lookup\n const host = rawHost.startsWith('[') && rawHost.endsWith(']') ? rawHost.slice(1, -1) : rawHost;\n if (!host) return;\n\n const ipVersion = isIP(host);\n if (ipVersion === 4 || ipVersion === 6) {\n const family = ipVersion === 4 ? 'ipv4' : 'ipv6';\n if (PRIVATE_BLOCKS.check(host, family)) {\n throw new PrivateAddressError(\n `refusing to fetch private address ${host} (set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`,\n host,\n host,\n );\n }\n return;\n }\n\n // 普通域名 → DNS 解析\n let addr: { address: string; family: number };\n try {\n addr = await lookup(host);\n } catch {\n // DNS 失败交给上游 fetch 报错(用户能看到更明确的 ENOTFOUND)\n return;\n }\n const family = addr.family === 6 ? 'ipv6' : 'ipv4';\n if (PRIVATE_BLOCKS.check(addr.address, family)) {\n throw new PrivateAddressError(\n `refusing to fetch ${host} which resolves to private address ${addr.address} ` +\n `(set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`,\n host,\n addr.address,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Site detection / headers\n// ---------------------------------------------------------------------------\n\n/**\n * 简单 host 匹配:识别微信公众号文章,其他一律 'generic'。\n */\nexport function detectSite(url: string): 'weixin' | 'generic' {\n try {\n const host = new URL(url).hostname.toLowerCase();\n if (host.includes('mp.weixin.qq.com')) return 'weixin';\n } catch {\n /* ignore */\n }\n return 'generic';\n}\n\n/**\n * 按站点构造 fetch headers:微信公众号必须 iPhone UA + Referer 才返回正文。\n */\nexport function buildHeaders(site: string): Record<string, string> {\n if (site === 'weixin') {\n return {\n 'User-Agent': UA_IPHONE,\n Referer: 'https://mp.weixin.qq.com/',\n 'Accept-Language': 'zh-CN,zh;q=0.9',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n };\n }\n return {\n 'User-Agent': UA_DESKTOP,\n 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n };\n}\n\n/**\n * 粗略判断返回 HTML 是否被反爬拦截。微信特别加了\"无 js_content 节点\"启发式。\n */\nexport function detectAntibot(html: string, site: string): boolean {\n if (ANTIBOT_TRIGGERS.some((t) => html.includes(t))) return true;\n if (site === 'weixin' && !html.includes('js_content')) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// L1 fetch — native Node fetch\n// ---------------------------------------------------------------------------\n\n/**\n * 用 Node 内置 fetch 拉 HTML,超时由 HTTP_TIMEOUT_MS 控制。\n *\n * 行为说明:\n * - 默认拒绝私网(RFC 1918 / RFC 6890)目标;可由 `LOREKIT_FETCH_ALLOW_PRIVATE=1`\n * 绕过(本地开发场景)。失败抛 `PrivateAddressError`\n * - `redirect: 'manual'` 手动跟随,最多 `MAX_REDIRECTS=5` 跳,每跳前对目标 URL\n * 重新做 SSRF 检查(避免公网 URL 通过 302 跳板进内网)\n * - HTTP 非 2xx 或 abort 抛 `Error`,由调用方决定是否走 L2 fallback\n */\nexport async function fetchHtmlL1(url: string, headers: Record<string, string>): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);\n try {\n let currentUrl = url;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n // 每跳前 SSRF 检查(首跳也查)\n await assertPublicAddress(currentUrl);\n\n const res = await fetch(currentUrl, {\n headers,\n redirect: 'manual',\n signal: controller.signal,\n });\n\n // 3xx with Location → 手动跟随\n if (res.status >= 300 && res.status < 400) {\n const loc = res.headers.get('location');\n if (!loc) {\n // 没有 Location header 的 3xx 当作错误返回\n throw new Error(`HTTP ${res.status} without Location header`);\n }\n if (hop === MAX_REDIRECTS) {\n throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${url}`);\n }\n // 相对 URL 用 currentUrl 作 base 解析\n currentUrl = new URL(loc, currentUrl).toString();\n // 释放 body,避免连接泄漏\n try {\n await res.arrayBuffer();\n } catch {\n // 流式响应可能已 abort,吞掉\n }\n continue;\n }\n\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return await res.text();\n }\n // 理论不可达:循环出口都在 return / throw\n throw new Error(`unreachable: redirect loop exit ${url}`);\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// L2 fetch — optional playwright-core\n// ---------------------------------------------------------------------------\n\n/**\n * playwright-core fallback。可选依赖,缺了或 chromium 没装就返回 null,\n * 由调用方汇报 ANTIBOT_BLOCKED。\n */\nexport async function fetchHtmlL2(url: string): Promise<string | null> {\n try {\n // Dynamic import — playwright-core is optional\n // @ts-expect-error — playwright-core 是可选依赖,类型可能未安装\n const pw = await import('playwright-core');\n const browser = await pw.chromium.launch({ headless: true });\n try {\n const page = await browser.newPage();\n await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });\n return await page.content();\n } finally {\n await browser.close();\n }\n } catch {\n // playwright-core not installed or chromium not available\n return null;\n }\n}\n","/**\n * fetcher/images.ts — 图片下载、magic byte 嗅探、markdown 链接重写\n *\n * 批次 21a strangler fig 第一步:从 src/lib/fetcher.ts copy 出来作为旁路新模块。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换,本文件目前未被使用。\n *\n * 依赖关系:仅 import fetcher/http.ts 的 HTTP_TIMEOUT_MS,不依赖 helpers.ts。\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { HTTP_TIMEOUT_MS } from './http.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_IMG_BYTES = 5 * 1024 * 1024;\nconst IMG_CONCURRENCY = 5;\n\n// 常见图片格式的 magic bytes — 优先靠字节签名判断扩展名,\n// 其次才落回 Content-Type(远端 MIME 经常乱报)。\nconst MAGIC: Array<[Uint8Array | number[], string]> = [\n [[0xff, 0xd8, 0xff], '.jpg'],\n [[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], '.png'], // \\x89PNG\\r\\n\\x1a\\n\n [[0x47, 0x49, 0x46, 0x38, 0x37, 0x61], '.gif'], // GIF87a\n [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], '.gif'], // GIF89a\n];\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ImgDownloadResult {\n originalUrl: string;\n localRel: string | null; // relative path like ./images/img_01.jpg\n status: 'ok' | 'failed' | 'too_large';\n}\n\n// ---------------------------------------------------------------------------\n// Magic byte sniffing\n// ---------------------------------------------------------------------------\n\n/**\n * 优先按文件头的 magic bytes 判扩展,不命中再用 Content-Type 作 fallback。\n * 都不命中返回 null(调用方应当丢弃此图)。\n */\nexport function sniffExt(head: Uint8Array, contentType: string): string | null {\n for (const [sig, ext] of MAGIC) {\n if (sig.every((b, i) => head[i] === b)) return ext;\n }\n // RIFF....WEBP\n if (\n head[0] === 0x52 &&\n head[1] === 0x49 &&\n head[2] === 0x46 &&\n head[3] === 0x46 &&\n head[8] === 0x57 &&\n head[9] === 0x45 &&\n head[10] === 0x42 &&\n head[11] === 0x50\n ) {\n return '.webp';\n }\n const ct = contentType.toLowerCase();\n if (ct.includes('image/jpeg') || ct.includes('image/jpg')) return '.jpg';\n if (ct.includes('image/png')) return '.png';\n if (ct.includes('image/gif')) return '.gif';\n if (ct.includes('image/webp')) return '.webp';\n if (ct.includes('image/svg')) return '.svg';\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Image downloading\n// ---------------------------------------------------------------------------\n\n/**\n * 单张图下载:最多 2 次尝试;超过 MAX_IMG_BYTES 标 too_large;\n * 文件名格式 `img_{idx:02d}.{ext}`,相对链接形如 `./<slug>.assets/img_01.jpg`。\n */\nexport async function downloadOneImage(\n url: string,\n idx: number,\n imagesDir: string,\n headers: Record<string, string>,\n assetsRelPath: string,\n): Promise<ImgDownloadResult> {\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);\n const res = await fetch(url, {\n headers,\n redirect: 'follow',\n signal: controller.signal,\n });\n clearTimeout(timer);\n\n if (!res.ok) continue;\n\n const cl = Number(res.headers.get('content-length') || 0);\n if (cl && cl > MAX_IMG_BYTES) {\n return { originalUrl: url, localRel: null, status: 'too_large' };\n }\n\n const buf = await res.arrayBuffer();\n if (buf.byteLength > MAX_IMG_BYTES) {\n return { originalUrl: url, localRel: null, status: 'too_large' };\n }\n\n const data = new Uint8Array(buf);\n const ext = sniffExt(data.slice(0, 16), res.headers.get('content-type') || '');\n if (!ext) continue;\n\n const fname = `img_${String(idx).padStart(2, '0')}${ext}`;\n await writeFile(join(imagesDir, fname), data);\n // localRel 相对于 .md 文件位置;assetsRelPath 例如 \"./<slug>.assets/\"\n return { originalUrl: url, localRel: `${assetsRelPath}${fname}`, status: 'ok' };\n } catch {\n // retry\n }\n }\n return { originalUrl: url, localRel: null, status: 'failed' };\n}\n\n/**\n * 批量下载:按 IMG_CONCURRENCY 分批 Promise.all,避免一次打开太多 socket。\n * 返回结果数组顺序对应 imgSrcs。\n */\nexport async function downloadImages(\n imgSrcs: string[],\n imagesDir: string,\n headers: Record<string, string>,\n assetsRelPath: string,\n): Promise<ImgDownloadResult[]> {\n if (imgSrcs.length === 0) return [];\n await mkdir(imagesDir, { recursive: true });\n\n const results: ImgDownloadResult[] = [];\n // Process in batches of IMG_CONCURRENCY\n for (let i = 0; i < imgSrcs.length; i += IMG_CONCURRENCY) {\n const batch = imgSrcs.slice(i, i + IMG_CONCURRENCY);\n const batchResults = await Promise.all(\n batch.map((src, j) => downloadOneImage(src, i + j + 1, imagesDir, headers, assetsRelPath)),\n );\n results.push(...batchResults);\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Markdown link rewriting\n// ---------------------------------------------------------------------------\n\n/**\n * 用下载结果把 markdown 中的 `![alt](远端 URL)` 重写为 `![alt](本地相对路径)`。\n * 下载失败的图保留原 URL。\n */\nexport function rewriteMarkdownImages(md: string, imgResults: ImgDownloadResult[]): string {\n const urlToLocal = new Map<string, string>();\n for (const r of imgResults) {\n if (r.status === 'ok' && r.localRel) {\n urlToLocal.set(r.originalUrl, r.localRel);\n }\n }\n // Replace ![alt](url) with ![alt](localRel)\n return md.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (match, alt, url) => {\n const local = urlToLocal.get(url);\n return local ? `![${alt}](${local})` : match;\n });\n}\n","/**\n * fetcher/routes/web.ts — generic 网页解析(OpenGraph + meta + article/main/body)\n *\n * 批次 21c strangler fig 第三步:从 src/lib/fetcher.ts copy 出 parseGeneric。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路 parser。21g 才创建 routes/index.ts dispatcher 取代\n * fetchUrl 内的 `site === 'weixin' ? parseWeixin : parseGeneric` 三元逻辑。\n *\n * 不含微信特定逻辑(见 21d 抽 routes/weixin.ts)。\n * 不含 frontmatter 拼装(见 21b 的 frontmatter.ts,本文件仅做 HTML → ParsedDoc)。\n */\nimport * as cheerio from 'cheerio';\n\nimport { normalizeDateText, resolveUrl } from '../helpers.js';\nimport type { ParsedDoc } from '../types.js';\n\n// ParsedDoc 21g-pre 上提到 fetcher/types.ts,本文件 21c 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// parseGeneric\n// ---------------------------------------------------------------------------\n\n/**\n * generic 网页 HTML → ParsedDoc。\n *\n * 策略:\n * - title: og:title > <title>\n * - author: meta[name=author]\n * - publishDate: 一组常见 meta / `<time datetime>` / `<time>` 文本,按优先级第一个能解析的胜出\n * - body: <article> > <main> > <body>(找不到则返回空 bodyHtml)\n * - 图片:data-src > data-original > src,相对路径转绝对,丢弃 data:URL\n */\nexport function parseGeneric(html: string, baseUrl: string): ParsedDoc {\n const $ = cheerio.load(html);\n\n // Title\n const ogTitle = $('meta[property=\"og:title\"]').attr('content')?.trim();\n const titleTag = $('title').text().trim();\n const title = ogTitle || titleTag || '';\n\n // Author\n const author = $('meta[name=\"author\"]').attr('content')?.trim() || '';\n\n // Publish date — common places: OpenGraph article, meta, <time datetime>, JSON-LD\n let publishDate: string | undefined;\n const dateCandidates: Array<string | undefined> = [\n $('meta[property=\"article:published_time\"]').attr('content'),\n $('meta[property=\"og:article:published_time\"]').attr('content'),\n $('meta[name=\"article:published_time\"]').attr('content'),\n $('meta[itemprop=\"datePublished\"]').attr('content'),\n $('meta[name=\"date\"]').attr('content'),\n $('meta[name=\"pubdate\"]').attr('content'),\n $('meta[name=\"publishdate\"]').attr('content'),\n $('time[datetime]').first().attr('datetime'),\n $('time').first().text(),\n ];\n for (const cand of dateCandidates) {\n if (!cand) continue;\n const norm = normalizeDateText(cand);\n if (norm) {\n publishDate = norm;\n break;\n }\n }\n\n // Body: article > main > body\n let body = $('article');\n if (!body.length) body = $('main');\n if (!body.length) body = $('body');\n if (!body.length) {\n return { title, author, publishDate, bodyHtml: '', imgSrcs: [] };\n }\n\n // Clean junk\n body.find('script, style, nav, footer, header, aside').remove();\n\n // Normalize images\n const imgSrcs: string[] = [];\n body.find('img').each((_i, el) => {\n const $el = $(el);\n const real = (\n $el.attr('data-src') ||\n $el.attr('data-original') ||\n $el.attr('src') ||\n ''\n ).trim();\n if (!real || real.startsWith('data:')) {\n $el.remove();\n return;\n }\n const abs = resolveUrl(real, baseUrl);\n $el.attr('src', abs);\n imgSrcs.push(abs);\n });\n\n return { title, author, publishDate, bodyHtml: body.html() || '', imgSrcs };\n}\n","/**\n * fetcher/routes/weixin.ts — 微信公众号文章解析\n *\n * 批次 21d strangler fig 第四步:从 src/lib/fetcher.ts copy 出 parseWeixin。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路 parser。21g 才创建 routes/index.ts dispatcher 取代\n * fetchUrl 内的 `site === 'weixin' ? parseWeixin : parseGeneric` 三元逻辑。\n *\n * ## 与原 parseWeixin 的差异(LEGACY P4-4 顺手修,规划方批准)\n *\n * 旧版只识别 `<img>` 标签的 lazy attrs(data-src / data-original / data-url)。\n * 部分微信文章用 `<picture><source srcset=\"...\"><img ...></picture>` 或仅 `<picture>\n * <source srcset=\"...\"></picture>`(无 img)的写法,旧版会丢图。\n *\n * 新版在跑 img 流程前先扫一遍 `<picture>` 节点:\n * - 取内部第一个 `<source>` 的 `srcset`,parse 第一个 URL(srcset 语法:`url [w|x], url2 [w|x], ...`,用 `,` 分隔候选 + 空白分隔 url 与 descriptor)\n * - 若 picture 内有 `<img>` 且其 `src/data-src/data-original/data-url` 都为空,把 srcset url 写入 `data-src`(让后续 img 流程统一处理)\n * - 若 picture 内无 `<img>` 且 srcset 有效,append 一个 `<img data-src=\"...\">`\n * - 用 picture 的 first `<img>` 节点替换 picture 整体(unwrap),删掉 picture 与所有 `<source>` 子节点,避免 turndown 输出残留\n * - 兜底:扫整个 body 内残留的 `<source>` 节点(picture 之外野生的,极少见)一并 remove\n *\n * 这样后续 `body.find('img').each(...)` 流程不变,imgSrcs 自然累加。\n */\nimport * as cheerio from 'cheerio';\n\nimport { normalizeDateText, resolveUrl, tsToYMD } from '../helpers.js';\nimport type { ParsedDoc } from '../types.js';\n\n// ParsedDoc 21g-pre 上提到 fetcher/types.ts,本文件 21d 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// P4-4 helper: srcset → 第一个 URL\n// ---------------------------------------------------------------------------\n\n/**\n * srcset 形如:`a.jpg 320w, b.jpg 640w` 或 `a.jpg, b.jpg 2x`。\n * 取第一个候选(按 `,` 分),再取候选里第一个 whitespace token(去掉 `Nw` / `Nx` descriptor)。\n * 空 / 非法返回空串。\"最高质量 URL\"判断需要 parse w/x 比较,本批次只取第一个保持最简实现。\n */\nfunction firstSrcsetUrl(srcset: string): string {\n const s = srcset.trim();\n if (!s) return '';\n const firstCandidate = s.split(',')[0].trim();\n if (!firstCandidate) return '';\n const url = firstCandidate.split(/\\s+/)[0].trim();\n return url;\n}\n\n// ---------------------------------------------------------------------------\n// parseWeixin\n// ---------------------------------------------------------------------------\n\nexport function parseWeixin(html: string, baseUrl: string): ParsedDoc {\n const $ = cheerio.load(html);\n\n // Title\n let title =\n $('h1#activity-name').text().trim() ||\n $('h1.rich_media_title').text().trim() ||\n $('meta[property=\"og:title\"]').attr('content')?.trim() ||\n '';\n\n // Author\n const author = $('a#js_name').text().trim() || $('#js_author_name').text().trim() || '';\n\n // Publish date — prefer `var ct = \"<unix seconds>\"` (most reliable),\n // fallback to <em id=\"publish_time\"> text node.\n let publishDate: string | undefined;\n const ctMatch = html.match(/var\\s+ct\\s*=\\s*\"(\\d+)\"/);\n if (ctMatch) {\n const ts = Number(ctMatch[1]);\n if (Number.isFinite(ts) && ts > 0) publishDate = tsToYMD(ts);\n }\n if (!publishDate) {\n const ptText = $('em#publish_time').text().trim();\n if (ptText) publishDate = normalizeDateText(ptText);\n }\n\n // Body\n const body = $('#js_content');\n if (!body.length) {\n return { title, author, publishDate, bodyHtml: '', imgSrcs: [] };\n }\n\n // Clean\n body.find('script, style').remove();\n\n // ---------------------------------------------------------------------------\n // P4-4: 把 <picture><source srcset> 展开成 <img>,再走原有 img 流程\n // ---------------------------------------------------------------------------\n body.find('picture').each((_i, el) => {\n const $picture = $(el);\n // 取第一个 source 的 srcset\n const $firstSource = $picture.find('source[srcset]').first();\n const srcsetRaw = $firstSource.attr('srcset') || '';\n const pickedUrl = firstSrcsetUrl(srcsetRaw);\n\n let $img = $picture.find('img').first();\n\n if ($img.length) {\n // 有 img:若 src / data-* 都空才用 srcset 兜底,避免覆盖原有更明确的来源\n const existing = (\n $img.attr('data-src') ||\n $img.attr('data-original') ||\n $img.attr('data-url') ||\n $img.attr('src') ||\n ''\n ).trim();\n if (!existing && pickedUrl) {\n $img.attr('data-src', pickedUrl);\n }\n } else if (pickedUrl) {\n // 无 img:用 srcset url 新建一个,后续 img 流程统一处理\n $picture.append(`<img data-src=\"${pickedUrl}\">`);\n $img = $picture.find('img').first();\n }\n\n // unwrap:用 img 替换 picture 整体;若没拿到 img(srcset 也空),picture 整块移除\n if ($img.length) {\n $picture.replaceWith($img);\n } else {\n $picture.remove();\n }\n });\n\n // 兜底:清掉野生 <source> 节点(picture 之外极少见,但为 turndown 干净起见统一删)\n body.find('source').remove();\n\n // Normalize images: data-src / data-original → src\n const imgSrcs: string[] = [];\n body.find('img').each((_i, el) => {\n const $el = $(el);\n const real = (\n $el.attr('data-src') ||\n $el.attr('data-original') ||\n $el.attr('data-url') ||\n $el.attr('src') ||\n ''\n ).trim();\n if (!real || real.startsWith('data:')) {\n $el.remove();\n return;\n }\n const abs = resolveUrl(real, baseUrl);\n $el.attr('src', abs);\n // Remove noisy attrs\n for (const a of [\n 'data-src',\n 'data-original',\n 'data-url',\n 'data-w',\n 'data-ratio',\n 'data-type',\n 'data-s',\n 'srcset',\n ]) {\n $el.removeAttr(a);\n }\n imgSrcs.push(abs);\n });\n\n return { title, author, publishDate, bodyHtml: body.html() || '', imgSrcs };\n}\n","/**\n * fetcher/routes/gist.ts — GitHub Gist 抓取\n *\n * 批次 21e strangler fig 第五步:从 src/lib/fetcher.ts copy 出 parseGistUrl + fetchGist。\n * 原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;本文件目前未被任何\n * 调用方 import,仅作旁路。21g 才切换 dispatcher 并删旧。\n *\n * **本批首次集成 21b 的 buildFrontmatter()**:旧 fetchGist 内嵌的 fmLines 拼装段\n * 替换为 `lines.push(...buildFrontmatter({routeKind: 'gist', ...}))`,验证 21b 抽出的\n * helper 设计可用。21b 已用 6 mock case 证明 byte-level 等价;本文件因此 frontmatter\n * 段无需重复验证,整份文件 buffer 等价由 21e 自有 mock 兜底。\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport * as cheerio from 'cheerio';\n\nimport { buildFrontmatter } from '../frontmatter.js';\nimport { normalizeDateText, slugify, todayYMD } from '../helpers.js';\nimport { buildHeaders, fetchHtmlL1 } from '../http.js';\nimport type { FetchResult } from '../types.js';\n\n// FetchResult 21g-pre 上提到 fetcher/types.ts,本文件 21e 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\n// ---------------------------------------------------------------------------\n// parseGistUrl\n// ---------------------------------------------------------------------------\n\n/**\n * 校验并解析 gist URL。仅接受 `gist.github.com` / `gist.githubusercontent.com`,\n * 路径需含 `/<user>/<id>` 至少两段。其余返回 null。\n */\nexport function parseGistUrl(url: string): { user: string; id: string } | null {\n try {\n const u = new URL(url);\n if (\n !u.hostname.endsWith('gist.github.com') &&\n !u.hostname.endsWith('gist.githubusercontent.com')\n ) {\n return null;\n }\n const parts = u.pathname.split('/').filter(Boolean);\n if (parts.length < 2) return null;\n return { user: parts[0], id: parts[1] };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// fetchGist 主流程\n// ---------------------------------------------------------------------------\n\n/**\n * gist 抓取流程:\n * 1. 校验 URL,解析 user / id\n * 2. fetch gist 主页 HTML(L1 only,gist 页面不需要 playwright)\n * 3. 用 cheerio 抽 title / author / publishDate\n * 4. 扫所有 `<a href>`,匹配 `/<user>/<id>/raw/<sha>/<filename>` 模式抽 raw 链接\n * 5. 优先 `.md` / `.markdown`,否则取第一个;二次 fetch 拿正文\n * 6. 拼 frontmatter(用 21b buildFrontmatter)+ 可选 H1 + 正文,写到 outRoot/<slug>.md\n *\n * 失败路径返回 `{status:'error', reason:...}`,由调用方决定是否兜底。\n */\nexport async function fetchGist(url: string, outRoot: string): Promise<FetchResult> {\n const parsed = parseGistUrl(url);\n if (!parsed) {\n return { status: 'error', route: 'gist', url, reason: 'invalid_gist_url' };\n }\n\n const headers = buildHeaders('generic');\n let html: string;\n try {\n html = await fetchHtmlL1(url, headers);\n } catch (e) {\n return {\n status: 'error',\n route: 'gist',\n url,\n reason: `fetch_failed: ${(e as Error).message}`,\n };\n }\n\n const $ = cheerio.load(html);\n\n // gist 页面把描述放在 .gist-header [itemprop=\"about\"],OpenGraph title 通常是第一个文件名\n const description = $('[itemprop=\"about\"]').first().text().trim();\n const ogTitle = $('meta[property=\"og:title\"]').attr('content')?.trim();\n const title = description || ogTitle || parsed.id;\n\n const author = parsed.user;\n\n // 日期:<relative-time datetime=\"ISO\"> 是 GitHub 标准元素\n let publishDate: string | undefined;\n const dateRaw =\n $('relative-time').first().attr('datetime') ||\n $('time-ago').first().attr('datetime') ||\n $('meta[property=\"article:published_time\"]').attr('content') ||\n '';\n if (dateRaw) publishDate = normalizeDateText(dateRaw);\n\n // 抽 raw 链接。gist 页面的 raw 链接形如:\n // /karpathy/442a6b.../raw/ac46de.../llm-wiki.md\n const rawRe = /^\\/([^/]+)\\/([a-f0-9]{20,})\\/raw\\/([a-f0-9]{20,})\\/(.+)$/i;\n const rawLinks: Array<{ name: string; rawUrl: string }> = [];\n $('a').each((_i, el) => {\n const href = $(el).attr('href') || '';\n const m = href.match(rawRe);\n if (m) {\n rawLinks.push({\n name: m[4],\n rawUrl: 'https://gist.githubusercontent.com' + href,\n });\n }\n });\n\n if (rawLinks.length === 0) {\n return { status: 'error', route: 'gist', url, reason: 'no_raw_files_found' };\n }\n\n // 优先 markdown,其次第一个\n const mdLink = rawLinks.find((l) => /\\.(md|markdown)$/i.test(l.name)) || rawLinks[0];\n\n let content: string;\n try {\n const res = await fetch(mdLink.rawUrl, { headers, redirect: 'follow' });\n if (!res.ok) throw new Error(`HTTP ${res.status} on ${mdLink.rawUrl}`);\n content = await res.text();\n } catch (e) {\n const err = e as Error & { cause?: Error };\n const cause = err.cause?.message ? ` (${err.cause.message})` : '';\n return {\n status: 'error',\n route: 'gist',\n url,\n reason: `raw_fetch_failed: ${err.message}${cause} [raw_url=${mdLink.rawUrl}]`,\n };\n }\n\n const slug = slugify(title);\n await mkdir(outRoot, { recursive: true });\n\n const today = todayYMD();\n const hasH1 = /^#\\s+/m.test(content);\n // 21b buildFrontmatter 返回的行数组与原 fetcher.ts:709-718 内嵌 fmLines 块\n // byte-level 等价(21b 已 6 mock case 证明),spread 进 fmLines 后续操作不变\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: 'gist',\n title,\n today,\n url,\n author,\n publishDate,\n }),\n );\n fmLines.push('');\n if (!hasH1) fmLines.push(`# ${title}`, '');\n fmLines.push(content.trim(), '');\n\n const articlePath = join(outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'gist',\n url,\n title,\n author,\n publishDate,\n sourceKind: 'gist',\n sourceLayer: 'L1',\n slug,\n markdown: articlePath,\n imagesOk: 0,\n imagesFailed: 0,\n };\n}\n","/**\n * fetcher/routes/github.ts — GitHub repo / blob 抓取(README.md 或具体文件)\n *\n * 批次 21f strangler fig 第六步:从 src/lib/fetcher.ts copy 出 parseGithubRepoUrl\n * + fetchGithubDoc。原 fetcher.ts 同名函数仍保留,commands/*.ts 暂未切换;\n * 本文件目前未被任何调用方 import,仅作旁路。21g 才切换 dispatcher 并删旧。\n *\n * 集成 21b 的 buildFrontmatter,routeKind='github':\n * - 21b 已验证 github routeKind 强制忽略 publishDate(即使传入也不输出)\n * - 原 fetchGithubDoc 本来就不抽 publishDate,调用时不传该字段,行为等价\n */\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { buildFrontmatter } from '../frontmatter.js';\nimport { slugify, todayYMD } from '../helpers.js';\nimport { buildHeaders } from '../http.js';\nimport type { FetchResult } from '../types.js';\n\n// FetchResult 21g-pre 上提到 fetcher/types.ts,本文件 21f 内的 inline 定义已删除。\n// 字段、可选性、注释完全一致,纯类型替换。\n\ninterface GithubRepoRef {\n owner: string;\n repo: string;\n ref: string; // HEAD / branch / commit\n subpath?: string; // 具体文件路径,如 \"docs/foo.md\"\n}\n\n// ---------------------------------------------------------------------------\n// parseGithubRepoUrl\n// ---------------------------------------------------------------------------\n\n/**\n * 校验并解析 github URL。仅接受 `github.com` / `www.github.com`,\n * 路径需含 `/<owner>/<repo>` 至少两段。支持 `/blob/<ref>/<subpath>` 与 `/tree/<ref>`。\n * 其余返回 null。\n */\nexport function parseGithubRepoUrl(url: string): GithubRepoRef | null {\n try {\n const u = new URL(url);\n if (u.hostname !== 'github.com' && u.hostname !== 'www.github.com') return null;\n const parts = u.pathname.split('/').filter(Boolean);\n if (parts.length < 2) return null;\n const [owner, rawRepo, ...rest] = parts;\n const repo = rawRepo.replace(/\\.git$/, '');\n\n if (rest.length === 0) {\n return { owner, repo, ref: 'HEAD' };\n }\n if (rest[0] === 'blob' && rest.length >= 3) {\n return { owner, repo, ref: rest[1], subpath: rest.slice(2).join('/') };\n }\n if (rest[0] === 'tree' && rest.length >= 2) {\n return { owner, repo, ref: rest[1] };\n }\n return { owner, repo, ref: 'HEAD' };\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// fetchGithubDoc 主流程\n// ---------------------------------------------------------------------------\n\n/**\n * github 抓取流程:\n * 1. 校验 URL\n * 2. 子路径模式:直接拼 raw URL;仓库根模式:循环试 5 个常见 README 文件名\n * 3. 第一个 HTTP 200 + 正文 > 20 字符 的胜出\n * 4. 拼 frontmatter(用 21b buildFrontmatter)+ 可选 H1 + `> Fetched from:` 注解 + 正文\n * 5. 写到 outRoot/<slug>.md\n *\n * 失败路径返回 `{status:'error', reason:...}`,由调用方决定是否兜底。\n */\nexport async function fetchGithubDoc(url: string, outRoot: string): Promise<FetchResult> {\n const parsed = parseGithubRepoUrl(url);\n if (!parsed) {\n return { status: 'error', route: 'github', url, reason: 'invalid_github_url' };\n }\n\n const { owner, repo, ref, subpath } = parsed;\n const headers = buildHeaders('generic');\n\n // 确定候选 raw URL 列表\n const candidates: string[] = [];\n if (subpath) {\n candidates.push(`https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${subpath}`);\n } else {\n // 仓库根:尝试常见 README 文件名\n for (const name of ['README.md', 'README.MD', 'Readme.md', 'readme.md', 'README']) {\n candidates.push(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${name}`);\n }\n }\n\n let content = '';\n let chosenUrl = '';\n for (const candUrl of candidates) {\n try {\n const res = await fetch(candUrl, { headers });\n if (!res.ok) continue;\n const text = await res.text();\n if (text && text.trim().length > 20) {\n content = text;\n chosenUrl = candUrl;\n break;\n }\n } catch {\n // try next\n }\n }\n\n if (!content) {\n return { status: 'error', route: 'github', url, reason: 'no_readable_content_found' };\n }\n\n const fileName = subpath ? subpath.split('/').pop()! : 'README.md';\n const title = subpath ? fileName.replace(/\\.(md|markdown)$/i, '') : `${owner}/${repo}`;\n\n const slug = slugify(subpath ? `${owner}-${repo}-${fileName}` : `${owner}-${repo}`);\n await mkdir(outRoot, { recursive: true });\n\n const today = todayYMD();\n const hasH1 = /^#\\s+/m.test(content);\n // 21b buildFrontmatter routeKind='github' 强制忽略 publishDate(21b parity case 6 已验证),\n // 不传该字段即可。返回行数组与原 fetcher.ts:826-834 内嵌 fmLines 块 byte-level 等价。\n const fmLines: string[] = [];\n fmLines.push(\n ...buildFrontmatter({\n routeKind: 'github',\n title,\n today,\n url,\n author: owner,\n }),\n );\n fmLines.push('');\n if (!hasH1) fmLines.push(`# ${title}`, '');\n fmLines.push(`> Fetched from: ${chosenUrl}`, '');\n fmLines.push(content.trim(), '');\n\n const articlePath = join(outRoot, `${slug}.md`);\n await writeFile(articlePath, fmLines.join('\\n'), 'utf-8');\n\n return {\n status: 'ok',\n route: 'github',\n url,\n title,\n author: owner,\n sourceKind: 'github',\n sourceLayer: 'L1',\n slug,\n markdown: articlePath,\n imagesOk: 0,\n imagesFailed: 0,\n };\n}\n","/**\n * ingest-state.ts — Persistent ingest pipeline state.\n *\n * Lives at `<corpus>/.wiki/ingest-state.json`. The authoritative record of\n * \"what URL was ingested, at what step, and did we finish it.\" Used by:\n * - `lorekit fetch` : dedupe + resume detection\n * - `lorekit ingest *` : step tracking from the skill/agent\n * - `lorekit ingest-check` : surface in-flight / failed ingests\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, dirname } from 'node:path';\n\n/**\n * Three coarse states the user actually cares about:\n * started — URL has entered the pipeline, not finished yet.\n * (which sub-step it's at is recorded in `stepsDone` below.)\n * completed — archived + wiki + lint all done.\n * failed — explicit abort with a reason.\n *\n * Finer sub-steps live in `stepsDone[]` so resuming an interrupted ingest\n * can skip already-done work, but the top-level symbol stays one of three.\n */\nexport type IngestStatus = 'started' | 'completed' | 'failed';\n\nexport type IngestStep = 'fetch' | 'archive' | 'wiki' | 'backlink' | 'lint';\n\nexport interface IngestRecord {\n url: string;\n title?: string;\n sourceDate?: string; // YYYY-MM-DD\n startedAt: string; // ISO timestamp\n updatedAt: string; // ISO timestamp\n status: IngestStatus;\n stepsDone: IngestStep[];\n workbenchMd?: string; // absolute path to <slug>.md while status=fetched\n // 老字段,兼容 0.3.x 之前的 state.json(产物是 <slug>/article.md 嵌套结构)\n workbenchDir?: string;\n archivedTo?: string; // relative-to-corpus path (e.g. 原料/剪藏/xxx)\n wikiPages?: string[]; // relative-to-corpus paths\n error?: string;\n}\n\nexport interface IngestStateFile {\n version: 1;\n ingests: Record<string, IngestRecord>;\n}\n\nfunction stateFilePath(corpus: string): string {\n return join(corpus, '.wiki', 'ingest-state.json');\n}\n\nexport function loadIngestState(corpus: string): IngestStateFile {\n const p = stateFilePath(corpus);\n if (!existsSync(p)) {\n return { version: 1, ingests: {} };\n }\n try {\n const raw = readFileSync(p, 'utf-8');\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') {\n return { version: 1, ingests: {} };\n }\n if (!parsed.ingests || typeof parsed.ingests !== 'object') {\n parsed.ingests = {};\n }\n parsed.version = 1;\n return parsed as IngestStateFile;\n } catch {\n return { version: 1, ingests: {} };\n }\n}\n\nexport function saveIngestState(corpus: string, state: IngestStateFile): void {\n const p = stateFilePath(corpus);\n mkdirSync(dirname(p), { recursive: true });\n const serialized = JSON.stringify(state, null, 2);\n writeFileSync(p, serialized + '\\n', 'utf-8');\n}\n\nexport function getIngestRecord(corpus: string, url: string): IngestRecord | undefined {\n return loadIngestState(corpus).ingests[url];\n}\n\nexport function upsertIngestRecord(\n corpus: string,\n url: string,\n patch: Partial<IngestRecord>,\n): IngestRecord {\n const state = loadIngestState(corpus);\n const now = new Date().toISOString();\n const existing = state.ingests[url];\n const merged: IngestRecord = existing\n ? { ...existing, ...patch, url, updatedAt: now }\n : {\n url,\n startedAt: now,\n updatedAt: now,\n status: (patch.status as IngestStatus) ?? 'started',\n stepsDone: patch.stepsDone ?? [],\n ...patch,\n };\n // Dedup stepsDone\n if (merged.stepsDone) {\n merged.stepsDone = Array.from(new Set(merged.stepsDone));\n }\n state.ingests[url] = merged;\n saveIngestState(corpus, state);\n return merged;\n}\n\nexport function deleteIngestRecord(corpus: string, url: string): boolean {\n const state = loadIngestState(corpus);\n if (!(url in state.ingests)) return false;\n delete state.ingests[url];\n saveIngestState(corpus, state);\n return true;\n}\n\nexport function listPendingIngests(corpus: string): IngestRecord[] {\n const state = loadIngestState(corpus);\n return Object.values(state.ingests).filter((r) => r.status !== 'completed');\n}\n\n/**\n * Suggest the next step for a resumed ingest.\n * Derived from stepsDone so the caller doesn't have to know the step order.\n */\nexport function nextStepHint(record: IngestRecord): string {\n if (record.status === 'completed') return 'nothing to do';\n if (record.status === 'failed') {\n return `failed: ${record.error ?? 'unknown error'} — inspect and re-run with --force if you want to retry`;\n }\n const done = new Set(record.stepsDone);\n if (!done.has('fetch')) {\n return 'fetch: nothing recorded yet — run `lorekit fetch <url>`';\n }\n if (!done.has('archive')) {\n return 'archive: mv the workbench dir into 原料/(剪藏|文章|书籍|...)';\n }\n if (!done.has('wiki')) {\n return 'wiki: compile wiki pages in 知识库/(概念|实体|摘要|专题)';\n }\n if (!done.has('lint')) {\n return 'lint: run `lorekit ingest-check`, fix any issues, then `lorekit ingest record <url> --complete`';\n }\n return 'all steps done but status not yet completed — run `lorekit ingest record <url> --complete`';\n}\n","/**\n * `lorekit ingest` — subcommands for marking ingest pipeline progress.\n *\n * The agent running wiki-ingest calls these as it advances through the\n * Decision tree; each call mutates .wiki/ingest-state.json. This makes\n * interrupted ingests resumable and provides the source of truth for\n * `lorekit ingest-check`.\n */\nimport type { Command } from 'commander';\nimport { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, relative } from 'node:path';\nimport { requireCorpus, collectMdFiles, extractFrontmatter } from '../lib/corpus.js';\nimport {\n loadIngestState,\n saveIngestState,\n upsertIngestRecord,\n deleteIngestRecord,\n listPendingIngests,\n nextStepHint,\n type IngestRecord,\n type IngestStep,\n type IngestStatus,\n} from '../lib/ingest-state.js';\nimport { dateToYMDLocal } from '../lib/date.js';\nimport { out, print } from '../utils/logger.js';\n\nconst VALID_STEPS: IngestStep[] = ['fetch', 'archive', 'wiki', 'backlink', 'lint'];\n\n// Today as YYYY-MM-DD in local time (Asia/Shanghai assumed by user setup).\nfunction today(): string {\n return dateToYMDLocal(new Date());\n}\n\n/**\n * Append a structured ingest entry to corpus/log.md.\n *\n * The log.md format (see existing entries) uses `## [YYYY-MM-DD] ingest | 标题`\n * as a section header followed by structured bullet lines. We prepend the new\n * entry just below the file's intro block (after the first `>` blockquote line)\n * so the most recent ingest sits on top — matching the convention of existing\n * entries.\n *\n * `body` is the LLM-supplied one-paragraph summary describing what was done.\n * The CLI auto-fills url / archive / wiki pages from the state record so the\n * skill doesn't have to repeat them.\n */\nfunction appendLogEntry(corpus: string, record: IngestRecord, body: string): void {\n const logPath = join(corpus, 'log.md');\n const title = record.title ?? '(untitled)';\n const wikiList = (record.wikiPages ?? []).map((p) => ` - ${p}`).join('\\n');\n const archived = record.archivedTo ?? '(unrecorded)';\n\n const entry = [\n `## [${today()}] ingest | ${title}`,\n '',\n body.trim(),\n '',\n `- **URL**:${record.url}`,\n `- **归档**:${archived}`,\n record.wikiPages && record.wikiPages.length > 0\n ? `- **新建/更新页**:\\n${wikiList}`\n : '- **新建/更新页**:(无)',\n '',\n '',\n ].join('\\n');\n\n let existing = '';\n if (existsSync(logPath)) existing = readFileSync(logPath, 'utf-8');\n\n if (!existing) {\n // Bootstrap a minimal log.md with header + first entry.\n const header =\n '# Log\\n\\n> 操作时间线,append-only。每条格式:`## [YYYY-MM-DD] 操作类型 | 标题`\\n> 可用 `grep \"^## \\\\[\" log.md | tail -10` 快速查最近操作。\\n\\n';\n writeFileSync(logPath, header + entry, 'utf-8');\n return;\n }\n\n // Insert new entry between intro block and first existing `## [` section.\n const firstSection = existing.search(/^## \\[/m);\n if (firstSection === -1) {\n // No existing sections — append at end.\n const sep = existing.endsWith('\\n') ? '' : '\\n';\n writeFileSync(logPath, existing + sep + entry, 'utf-8');\n } else {\n const before = existing.slice(0, firstSection);\n const after = existing.slice(firstSection);\n writeFileSync(logPath, before + entry + after, 'utf-8');\n }\n}\n\nexport function ingestCommand(program: Command): void {\n const group = program\n .command('ingest')\n .description('Track ingest pipeline state (record step progress, list pending, reconcile)');\n\n // ---------- list ----------\n group\n .command('list')\n .description('List every ingest record (completed + in-progress)')\n .action(() => {\n const corpus = requireCorpus();\n const state = loadIngestState(corpus);\n const rows = Object.values(state.ingests);\n if (rows.length === 0) {\n print('[lorekit ingest list] no records');\n out(JSON.stringify({ ingests: [] }));\n return;\n }\n const summary = rows.map((r) => {\n const done = r.stepsDone.join(',') || '(none)';\n const dest = r.archivedTo ?? r.workbenchMd ?? r.workbenchDir ?? '-';\n return ` [${r.status.padEnd(12)}] ${r.url}\\n steps: ${done} → ${dest}`;\n });\n print(`[lorekit ingest list] ${rows.length} record(s)\\n${summary.join('\\n')}`);\n out(JSON.stringify(state));\n });\n\n // ---------- pending ----------\n group\n .command('pending')\n .description('List only in-progress (non-completed) ingests — what you need to resume')\n .action(() => {\n const corpus = requireCorpus();\n const pending = listPendingIngests(corpus);\n if (pending.length === 0) {\n print('[lorekit ingest pending] all ingests are completed — nothing to resume');\n out(JSON.stringify({ pending: [] }));\n return;\n }\n const summary = pending.map((r) => {\n return ` [${r.status.padEnd(12)}] ${r.url}\\n next step → ${nextStepHint(r)}`;\n });\n print(\n `[lorekit ingest pending] ${pending.length} ingest(s) need attention\\n${summary.join('\\n')}`,\n );\n out(JSON.stringify({ pending }));\n process.exitCode = 1;\n });\n\n // ---------- record ----------\n group\n .command('record <url>')\n .description('Record step progress for an ingest (call from wiki-ingest skill)')\n .option(\n '--step <steps>',\n `mark step(s) as done. single: archive | multi: archive,wiki,backlink,lint. valid: ${VALID_STEPS.join(', ')}`,\n )\n .option('--archived-to <path>', 'relative path where the source was moved (e.g. 原料/剪藏/xxx)')\n .option('--wiki-page <path...>', 'relative path of a wiki page created (can be repeated)')\n .option(\n '--log <body>',\n 'append a one-paragraph summary to corpus/log.md (CLI auto-fills url/archive/pages)',\n )\n .option('--status <status>', 'explicit status (started|completed|failed)')\n .option('--complete', 'shortcut: mark status=completed')\n .option('--fail <reason>', 'shortcut: mark status=failed with reason')\n .action(\n (\n url: string,\n opts: {\n step?: string;\n archivedTo?: string;\n wikiPage?: string[];\n log?: string;\n status?: string;\n complete?: boolean;\n fail?: string;\n },\n ) => {\n const corpus = requireCorpus();\n const patch: Parameters<typeof upsertIngestRecord>[2] = {};\n\n // --step accepts either a single step (\"archive\") or a comma-separated\n // chain (\"archive,wiki,backlink,lint\") so the skill can close the books\n // in one call instead of four CLI invocations.\n let parsedSteps: IngestStep[] = [];\n if (opts.step) {\n parsedSteps = opts.step\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean) as IngestStep[];\n\n for (const s of parsedSteps) {\n if (!VALID_STEPS.includes(s)) {\n print(\n `[lorekit ingest record] invalid step: ${s}. valid: ${VALID_STEPS.join(', ')}`,\n );\n process.exitCode = 2;\n return;\n }\n }\n\n const existing = loadIngestState(corpus).ingests[url];\n const prev = existing?.stepsDone ?? [];\n // 去重:保留首次出现顺序。多次 record 链式调用追加同一 step 时不应重复,\n // 否则 ingest-state.json 的 stepsDone 会出现 `[archive, archive, wiki, wiki, backlink]`\n // 这种噪声,nextStepHint / pending 推断也会受影响。\n // 见批次 20b / P4-1 同模式(与 wikiPages 同步修)。\n patch.stepsDone = [...new Set([...prev, ...parsedSteps])];\n\n // status precedence: explicit flags win; otherwise, if the chain\n // includes 'lint' it implies completion.\n if (!opts.status && !opts.complete && !opts.fail) {\n if (parsedSteps.includes('lint')) patch.status = 'completed';\n else patch.status = 'started';\n }\n }\n if (opts.archivedTo) patch.archivedTo = opts.archivedTo;\n if (opts.wikiPage && opts.wikiPage.length > 0) {\n const existing = loadIngestState(corpus).ingests[url];\n const prev = existing?.wikiPages ?? [];\n // 去重:保留首次出现顺序。多次 record 调用追加同一 wiki 页面时不应重复,\n // 否则 log.md 的 `- **新建/更新页**` 块会出现重复条目。\n // 见 LEGACY P4-1 / 批次 20 的复现 smoke。\n patch.wikiPages = [...new Set([...prev, ...opts.wikiPage])];\n }\n if (opts.status) {\n const validStatuses: readonly IngestStatus[] = ['started', 'completed', 'failed'];\n if (!validStatuses.includes(opts.status as IngestStatus)) {\n print(\n `[lorekit ingest record] invalid --status: ${opts.status}. valid: ${validStatuses.join(', ')}`,\n );\n process.exitCode = 2;\n return;\n }\n patch.status = opts.status as IngestStatus;\n }\n if (opts.complete) patch.status = 'completed';\n if (opts.fail) {\n patch.status = 'failed';\n patch.error = opts.fail;\n }\n\n const updated = upsertIngestRecord(corpus, url, patch);\n\n // --log: append entry to corpus/log.md AFTER state is updated so the\n // entry can include the freshly-recorded archive path / wiki pages.\n let logAppended = false;\n if (opts.log) {\n try {\n appendLogEntry(corpus, updated, opts.log);\n logAppended = true;\n } catch (e) {\n print(`[lorekit ingest record] log append failed: ${(e as Error).message}`);\n }\n }\n\n print(\n `[lorekit ingest record] ${url}\\n` +\n ` status: ${updated.status} steps: ${updated.stepsDone.join(',') || '(none)'}` +\n (logAppended ? ' +log' : ''),\n );\n out(JSON.stringify({ ...updated, logAppended }));\n },\n );\n\n // ---------- check ----------\n // Pre-flight broken-link check for one or more wiki pages. Used by the\n // wiki-ingest skill RIGHT AFTER writing new pages, before recording the\n // backlink step. Catches `[[xxx]]` whose target page doesn't exist so the\n // skill can either create the target stub or downgrade `[[xxx]]` to plain\n // text — instead of leaving the corpus in a state lint will flag later.\n group\n .command('check <files...>')\n .description('Scan given wiki pages for broken [[wikilinks]] (pre-commit check)')\n .action((files: string[]) => {\n const corpus = requireCorpus();\n\n // Build the same lookup sets lint.ts uses (stems + bare names + folder\n // packages like 原料/剪藏/xxx/article.md → 原料/剪藏/xxx).\n const allMd = collectMdFiles(corpus);\n const stemSet = new Set<string>();\n const baseNameSet = new Set<string>();\n for (const file of allMd) {\n const rel = relative(corpus, file);\n const stem = rel.replace(/\\.md$/, '');\n stemSet.add(stem);\n baseNameSet.add(stem.split('/').pop()!);\n if (stem.endsWith('/article')) {\n const folder = stem.replace(/\\/article$/, '');\n stemSet.add(folder);\n baseNameSet.add(folder.split('/').pop()!);\n }\n }\n\n const stripCode = (s: string) => s.replace(/```[\\s\\S]*?```/g, '').replace(/`[^`\\n]+`/g, '');\n\n const broken: { file: string; link: string }[] = [];\n const okLinks: { file: string; link: string }[] = [];\n const checked: string[] = [];\n\n for (const f of files) {\n const abs = f.startsWith('/') ? f : join(process.cwd(), f);\n if (!existsSync(abs)) {\n print(`[lorekit ingest check] file not found: ${f}`);\n process.exitCode = 2;\n continue;\n }\n const rel = relative(corpus, abs);\n checked.push(rel);\n\n let content: string;\n try {\n content = stripCode(readFileSync(abs, 'utf-8'));\n } catch {\n continue;\n }\n\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n const seen = new Set<string>();\n while ((m = linkRe.exec(content)) !== null) {\n const target = m[1].trim();\n if (seen.has(target)) continue;\n seen.add(target);\n if (stemSet.has(target) || baseNameSet.has(target)) {\n okLinks.push({ file: rel, link: target });\n } else {\n broken.push({ file: rel, link: target });\n }\n }\n }\n\n const result = { checked, ok: okLinks, broken };\n\n if (broken.length === 0) {\n print(\n `[lorekit ingest check] ${checked.length} file(s), ${okLinks.length} link(s) ok, no broken links`,\n );\n } else {\n print(`[lorekit ingest check] ${broken.length} broken link(s) found:`);\n for (const b of broken) {\n print(` ✗ ${b.file}: [[${b.link}]]`);\n }\n process.exitCode = 1;\n }\n out(JSON.stringify(result));\n });\n\n // ---------- forget ----------\n group\n .command('forget <url>')\n .description('Remove a record from the state (e.g. after manual cleanup)')\n .action((url: string) => {\n const corpus = requireCorpus();\n const removed = deleteIngestRecord(corpus, url);\n print(\n removed\n ? `[lorekit ingest forget] removed ${url}`\n : `[lorekit ingest forget] no record for ${url}`,\n );\n out(JSON.stringify({ removed, url }));\n });\n\n // ---------- reconcile ----------\n group\n .command('reconcile')\n .description('Back-fill state for pre-existing 原料/ pages missing a state record')\n .option('--dry-run', 'list what would be added without writing')\n .action((opts: { dryRun?: boolean }) => {\n const corpus = requireCorpus();\n const sourcesRoot = join(corpus, '原料');\n if (!existsSync(sourcesRoot)) {\n print('[lorekit ingest reconcile] no 原料/ directory');\n return;\n }\n const state = loadIngestState(corpus);\n const added: string[] = [];\n for (const mdPath of collectMdFiles(sourcesRoot)) {\n const fm = extractFrontmatter(mdPath);\n const url =\n (typeof fm.source_url === 'string' && fm.source_url) ||\n (typeof fm.url === 'string' && fm.url) ||\n '';\n if (!url) continue;\n if (state.ingests[url]) continue;\n\n const rel = relative(corpus, mdPath);\n const archivedTo = rel.replace(/\\/article\\.md$/, '');\n const sdRaw = fm.source_date;\n const sourceDate =\n typeof sdRaw === 'string'\n ? sdRaw\n : sdRaw instanceof Date\n ? sdRaw.toISOString().slice(0, 10)\n : undefined;\n const now = new Date().toISOString();\n state.ingests[url] = {\n url,\n title: typeof fm.title === 'string' ? fm.title : undefined,\n sourceDate,\n startedAt: now,\n updatedAt: now,\n status: 'completed',\n stepsDone: ['fetch', 'archive', 'wiki', 'lint'],\n archivedTo,\n };\n added.push(url);\n }\n if (!opts.dryRun && added.length > 0) saveIngestState(corpus, state);\n print(\n `[lorekit ingest reconcile] ${opts.dryRun ? 'would add' : 'added'} ${added.length} record(s)`,\n );\n for (const u of added) print(` + ${u}`);\n out(JSON.stringify({ dryRun: !!opts.dryRun, added }));\n });\n}\n","import type { Command } from 'commander';\nimport chalk from 'chalk';\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { requireCorpus } from '../lib/corpus.js';\nimport { ok, warn, err, print, out } from '../utils/logger.js';\nimport { runIndex } from './dir-index.js';\nimport { runVectorSync } from './vector.js';\nimport { runDoctor } from './doctor.js';\nimport { refreshRootIndex } from '../lib/root-index.js';\n\nexport interface SyncOptions {\n force?: boolean;\n model?: string;\n skipDoctor?: boolean;\n skipVector?: boolean;\n skipRootIndex?: boolean;\n json?: boolean;\n report?: boolean;\n}\n\ntype SyncStepStatus = 'ok' | 'skipped' | 'error';\n\ninterface SyncStepReport {\n status: SyncStepStatus;\n detail?: string;\n [key: string]: unknown;\n}\n\nexport interface SyncRunReport {\n status: 'ok' | 'error';\n startedAt: string;\n finishedAt: string;\n corpus: string;\n steps: {\n index: SyncStepReport;\n rootIndex: SyncStepReport;\n vector: SyncStepReport;\n doctor: SyncStepReport;\n };\n reportPath: string | null;\n errors: string[];\n}\n\nfunction createReport(corpus: string): SyncRunReport {\n return {\n status: 'ok',\n startedAt: new Date().toISOString(),\n finishedAt: '',\n corpus,\n steps: {\n index: { status: 'skipped' },\n rootIndex: { status: 'skipped' },\n vector: { status: 'skipped' },\n doctor: { status: 'skipped' },\n },\n reportPath: null,\n errors: [],\n };\n}\n\nfunction writeSyncReport(corpus: string, report: SyncRunReport): string {\n const dir = join(corpus, '.wiki', 'reports', 'sync');\n mkdirSync(dir, { recursive: true });\n const stamp = report.startedAt.replace(/[:.]/g, '-');\n const path = join(dir, `${stamp}.json`);\n report.reportPath = path;\n writeFileSync(path, JSON.stringify(report, null, 2) + '\\n', 'utf-8');\n return path;\n}\n\n/**\n * lorekit sync — 一条命令把「文本档案 + 向量库」对齐。\n *\n * 执行顺序(必须是这个顺序):\n * 1a. runIndex:扫目录生成/刷新所有 _INDEX.md\n * → 向量 L1 的输入源必须先存在,才能被下一步读\n * 1b. refreshRootIndex:合并刷新 corpus/index.md 的四个受控区\n * → L0 向量的输入源;保留人类手写摘要,只追加新页 / 删失踪页\n * 2. runVectorSync(layered=true):增量嵌入 chunk + 刷 L0/L1 向量\n * → L0 读 corpus/index.md 的 ## 分区\n * → L1 读每个 {dir}/_INDEX.md 的条目行\n * 3. runDoctor:sanity check,只报告不阻塞\n */\nexport async function runSync(corpus: string, opts: SyncOptions = {}): Promise<SyncRunReport> {\n const force = opts.force ?? false;\n const model = opts.model ?? 'bge-m3';\n const report = createReport(corpus);\n\n // Step 1a: 各子目录的 _INDEX.md\n print(chalk.cyan('── [1/3] index: refresh _INDEX.md ──'));\n try {\n const generated = runIndex(corpus);\n report.steps.index = { status: 'ok', generated };\n if (generated === 0) {\n warn('no indexable directories found');\n } else {\n ok(`refreshed ${generated} _INDEX.md file(s)`);\n }\n } catch (e) {\n report.status = 'error';\n report.steps.index = { status: 'error', error: (e as Error).message };\n report.errors.push(`index failed: ${(e as Error).message}`);\n err(`index failed: ${(e as Error).message}`);\n throw e;\n }\n\n // Step 1b: corpus 根的 index.md(受控区合并刷新)\n if (!opts.skipRootIndex) {\n try {\n const r = refreshRootIndex(corpus);\n const totals = r.perSection.reduce(\n (acc, s) => ({\n added: acc.added + s.added.length,\n removed: acc.removed + s.removed.length,\n kept: acc.kept + s.kept,\n }),\n { added: 0, removed: 0, kept: 0 },\n );\n report.steps.rootIndex = {\n status: 'ok',\n changed: r.changed,\n added: totals.added,\n removed: totals.removed,\n kept: totals.kept,\n };\n if (!r.changed) {\n ok(`index.md unchanged (${totals.kept} entries across managed sections)`);\n } else {\n ok(\n `index.md merged: +${totals.added} added, -${totals.removed} removed, ${totals.kept} kept`,\n );\n for (const s of r.perSection) {\n if (s.added.length === 0 && s.removed.length === 0) continue;\n for (const slug of s.added) print(` + ${slug}`);\n for (const slug of s.removed) print(` - ${slug} (file gone)`);\n }\n }\n } catch (e) {\n report.status = 'error';\n report.steps.rootIndex = { status: 'error', error: (e as Error).message };\n report.errors.push(`root index sync failed: ${(e as Error).message}`);\n err(`root index sync failed: ${(e as Error).message}`);\n throw e;\n }\n } else {\n report.steps.rootIndex = { status: 'skipped', reason: 'skip-root-index' };\n }\n print();\n\n // Step 2: 向量库(除非显式 --skip-vector)\n if (!opts.skipVector) {\n print(chalk.cyan('── [2/3] vector: sync chunks + L0/L1 ──'));\n try {\n const r = await runVectorSync(corpus, { force, model, layered: true });\n report.steps.vector = { status: 'ok', ...r, model };\n ok(`synced ${r.synced} files (${r.totalChunks} chunks), skipped ${r.skipped} unchanged`);\n } catch (e) {\n report.status = 'error';\n report.steps.vector = { status: 'error', error: (e as Error).message, model };\n report.errors.push(`vector sync failed: ${(e as Error).message}`);\n err(`vector sync failed: ${(e as Error).message}`);\n throw e;\n }\n print();\n } else {\n report.steps.vector = { status: 'skipped', reason: 'skip-vector' };\n }\n\n // Step 3: 健康体检(只报告不阻塞)\n if (!opts.skipDoctor) {\n print(chalk.cyan('── [3/3] doctor: sanity check ──'));\n const issues = await runDoctor(corpus);\n report.steps.doctor = { status: 'ok', issues };\n } else {\n report.steps.doctor = { status: 'skipped', reason: 'skip-doctor' };\n }\n\n report.finishedAt = new Date().toISOString();\n return report;\n}\n\nexport function syncCommand(program: Command): void {\n program\n .command('sync')\n .description('one-shot: refresh _INDEX.md → vector sync (layered) → doctor')\n .option('--force', 'full rebuild of vector index', false)\n .option('--model <name>', 'ollama model name', 'bge-m3')\n .option('--skip-doctor', 'skip the final doctor sanity check', false)\n .option('--skip-vector', 'only refresh _INDEX.md, skip vector sync', false)\n .option('--skip-root-index', 'skip merging corpus/index.md against disk', false)\n .option('--json', 'output machine-readable sync report', false)\n .option('--report', 'write .wiki/reports/sync/<timestamp>.json', false)\n .action(async (opts: SyncOptions) => {\n const corpus = requireCorpus();\n try {\n const report = await runSync(corpus, opts);\n if (opts.report) writeSyncReport(corpus, report);\n if (opts.json) out(JSON.stringify(report, null, 2));\n } catch {\n process.exit(1);\n }\n });\n}\n","/**\n * root-index.ts — sync `corpus/index.md` against the actual filesystem.\n *\n * Unlike `_INDEX.md` (auto-overwritten by `lorekit index`), the root index.md\n * carries human-curated one-line summaries that we deliberately preserve.\n * The sync logic is therefore a *merge*, not an overwrite:\n *\n * - File on disk + already in index → keep the human-written line as-is\n * - File on disk + missing from index → append a new line, summary auto-\n * extracted from `## Compiled Truth` first sentence\n * - In index + file deleted on disk → drop the line\n *\n * Only the four wiki sections are managed: 概念 / 实体 / 摘要 / 专题.\n * Any other heading (e.g. \"写作\", \"待研究问题\", \"空缺\") stays untouched.\n */\nimport { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { debug } from '../utils/logger.js';\n\nconst MANAGED_SECTIONS: { heading: string; subdir: string }[] = [\n { heading: '## 概念', subdir: '知识库/概念' },\n { heading: '## 实体', subdir: '知识库/实体' },\n { heading: '## 摘要', subdir: '知识库/摘要' },\n { heading: '## 专题', subdir: '知识库/专题' },\n];\n\ninterface DiskEntry {\n slug: string;\n summary: string;\n}\n\nfunction listEntriesInDir(corpus: string, subdir: string): DiskEntry[] {\n const dirPath = join(corpus, subdir);\n if (!existsSync(dirPath)) return [];\n const out: DiskEntry[] = [];\n for (const name of readdirSync(dirPath)) {\n if (name.startsWith('.')) continue;\n if (name === '_INDEX.md') continue;\n if (!name.endsWith('.md')) continue;\n const file = join(dirPath, name);\n const slug = `${subdir}/${name.replace(/\\.md$/, '')}`;\n out.push({ slug, summary: extractCompiledTruthSnippet(file) });\n }\n return out.sort((a, b) => a.slug.localeCompare(b.slug));\n}\n\n/**\n * Extract a short summary from `## Compiled Truth` — first non-blank paragraph,\n * capped at the first sentence terminator or 80 chars. Falls back to \"—\".\n *\n * Strips a leading `**bold**` lead-in (common pattern: \"**EntityName** is …\")\n * so the summary reads like a definition rather than a label.\n */\nfunction extractCompiledTruthSnippet(filePath: string): string {\n let content: string;\n try {\n content = readFileSync(filePath, 'utf-8');\n } catch (e) {\n // refreshRootIndex 会扫整个 知识库 子目录,单个文件读失败不阻塞批量;走 debug\n debug(`extractCompiledTruthSnippet(${filePath}) failed: ${(e as Error).message}`);\n return '—';\n }\n\n // Skip frontmatter\n const body = content.replace(/^---\\n[\\s\\S]*?\\n---\\n/, '');\n\n // Find ## Compiled Truth section\n const sectionMatch = body.match(/##\\s*Compiled Truth\\s*\\n+([\\s\\S]*?)(?=\\n---|\\n##\\s|$)/);\n if (!sectionMatch) return '—';\n\n // First non-blank line/paragraph\n const para = sectionMatch[1]\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!para) return '—';\n\n // Strip leading **bold** label\n const cleaned = para.replace(/^\\*\\*([^*]+)\\*\\*\\s*/, '$1 ');\n\n // First sentence (Chinese 。 or English .) within 80 chars\n const sentenceMatch = cleaned.match(/^(.{1,80}?[。.!?!?])/);\n if (sentenceMatch) return sentenceMatch[1];\n\n return cleaned.slice(0, 80) + (cleaned.length > 80 ? '…' : '');\n}\n\ninterface MergeResult {\n added: string[];\n removed: string[];\n kept: number;\n}\n\n/**\n * Merge one section in-place. Returns the new content + bookkeeping.\n */\nfunction mergeSection(\n content: string,\n heading: string,\n onDisk: DiskEntry[],\n): { newContent: string; result: MergeResult } {\n const lines = content.split('\\n');\n const startIdx = lines.findIndex((l) => l.trim() === heading);\n if (startIdx === -1) {\n // Section header missing — leave content untouched\n return { newContent: content, result: { added: [], removed: [], kept: 0 } };\n }\n\n // Find end of section: next \"## \" heading or EOF\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n if (lines[i].startsWith('## ')) {\n endIdx = i;\n break;\n }\n }\n\n const sectionBody = lines.slice(startIdx + 1, endIdx);\n const linkRe = /^-\\s+\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/;\n\n const onDiskSlugs = new Set(onDisk.map((e) => e.slug));\n const seenInIndex = new Set<string>();\n const removed: string[] = [];\n const kept: string[] = [];\n\n for (const line of sectionBody) {\n const trimmed = line.trim();\n // Strip blank lines and the placeholder; we re-add canonical padding below.\n if (trimmed === '' || trimmed === '(暂无条目)') continue;\n\n const m = line.match(linkRe);\n if (m) {\n const slug = m[1].trim();\n if (onDiskSlugs.has(slug)) {\n seenInIndex.add(slug);\n kept.push(line);\n } else {\n removed.push(slug);\n }\n } else {\n // preserve any non-link manual annotation (e.g. \"> note about this section\")\n kept.push(line);\n }\n }\n\n // Append entries on disk that aren't in the index yet\n const added: string[] = [];\n for (const e of onDisk) {\n if (!seenInIndex.has(e.slug)) {\n kept.push(`- [[${e.slug}]] — ${e.summary}`);\n added.push(e.slug);\n }\n }\n\n const sectionContentLines = kept.length === 0 ? ['', '(暂无条目)', ''] : ['', ...kept, ''];\n\n const newLines = [\n ...lines.slice(0, startIdx + 1),\n ...sectionContentLines,\n ...lines.slice(endIdx),\n ];\n\n return {\n newContent: newLines.join('\\n'),\n result: { added, removed, kept: seenInIndex.size },\n };\n}\n\nexport interface RootIndexSyncResult {\n filePath: string;\n changed: boolean;\n perSection: { heading: string; added: string[]; removed: string[]; kept: number }[];\n}\n\nexport function refreshRootIndex(corpus: string): RootIndexSyncResult {\n const indexPath = join(corpus, 'index.md');\n if (!existsSync(indexPath)) {\n return { filePath: indexPath, changed: false, perSection: [] };\n }\n\n const before = readFileSync(indexPath, 'utf-8');\n let content = before;\n const perSection: RootIndexSyncResult['perSection'] = [];\n\n for (const sec of MANAGED_SECTIONS) {\n const onDisk = listEntriesInDir(corpus, sec.subdir);\n const { newContent, result } = mergeSection(content, sec.heading, onDisk);\n content = newContent;\n perSection.push({ heading: sec.heading, ...result });\n }\n\n const changed = content !== before;\n if (changed) writeFileSync(indexPath, content, 'utf-8');\n\n return { filePath: indexPath, changed, perSection };\n}\n","/**\n * obsidian-tune.ts — 老用户升级触达 + 维护 .obsidian/graph.json filter(批次 26)\n *\n * 三种模式:\n * - 默认:检查 filter 完整性,diff 输出到 stderr,exit 0/1 便于脚本判断\n * - --write:备份原文件后应用推荐 filter(先 cp .bak.<ts> 再覆盖)\n * - --print:把推荐 filter JSON 打到 stdout,便于 `lorekit obsidian-tune --print > .obsidian/graph.json`\n *\n * 推荐 filter SSOT 在 `templates/default-corpus/.obsidian/graph.json`,\n * 由 `lib/obsidian.ts` 统一读取(避免和模板漂移)。\n */\nimport type { Command } from 'commander';\nimport { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { ok, info, warn, err, print, out } from '../utils/logger.js';\nimport { findCorpus } from '../lib/corpus.js';\nimport {\n getRecommendedFilter,\n getRecommendedGraphConfig,\n readCorpusFilter,\n isFilterComplete,\n missingTokens,\n} from '../lib/obsidian.js';\nimport { tsCompact } from '../lib/date.js';\n\ninterface TuneOpts {\n write?: boolean;\n print?: boolean;\n}\n\nfunction runPrint(): void {\n // 推荐 graph.json 完整体走 stdout(管道友好)\n const cfg = getRecommendedGraphConfig();\n out(JSON.stringify(cfg, null, 2));\n}\n\nfunction runCheck(corpus: string): number {\n const recommended = getRecommendedFilter();\n const cur = readCorpusFilter(corpus);\n\n if (!cur.exists) {\n warn('.obsidian/graph.json 缺失');\n print('');\n print('推荐 filter(含 _归档 / 反馈 + 完整根元数据):');\n print(` ${recommended}`);\n print('');\n print('应用:lorekit obsidian-tune --write');\n return 1;\n }\n\n if (isFilterComplete(cur.search, recommended)) {\n ok('.obsidian/graph.json filter 完整');\n return 0;\n }\n\n warn('.obsidian/graph.json filter 不完整');\n print('');\n print('当前 filter(如有):');\n print(` ${cur.search ?? '(空)'}`);\n print('');\n print('推荐 filter(含 _归档 / 反馈 + 完整根元数据):');\n print(` ${recommended}`);\n print('');\n print('缺少的 token:');\n for (const t of missingTokens(cur.search, recommended)) {\n print(` - ${t}`);\n }\n print('');\n print('应用:lorekit obsidian-tune --write');\n return 1;\n}\n\nfunction runWrite(corpus: string): number {\n const dest = join(corpus, '.obsidian', 'graph.json');\n const destDir = join(corpus, '.obsidian');\n if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });\n\n if (existsSync(dest)) {\n // 先备份再写 —— 红线:绝不许覆盖用户文件无备份\n const backup = `${dest}.bak.${tsCompact()}`;\n cpSync(dest, backup);\n ok(`备份 .obsidian/graph.json → ${backup}`);\n }\n\n // 直接落盘推荐配置(完整对象,不只是 search 字段)\n const cfg = getRecommendedGraphConfig();\n writeFileSync(dest, JSON.stringify(cfg, null, 2) + '\\n', 'utf-8');\n ok('写入推荐 filter');\n info('请关掉 Obsidian「关系图谱」标签页再重开生效');\n return 0;\n}\n\nexport function obsidianTuneCommand(program: Command) {\n program\n .command('obsidian-tune')\n .description('check / apply recommended Obsidian graph filter for the corpus')\n .option('--write', 'apply recommended filter (backs up existing graph.json first)')\n .option('--print', 'print recommended graph.json to stdout (pipe-friendly)')\n .action((opts: TuneOpts) => {\n // --print 不依赖 corpus,纯打印模板\n if (opts.print) {\n runPrint();\n process.exitCode = 0;\n return;\n }\n\n const corpus = findCorpus();\n if (!corpus) {\n err('not inside a corpus (no .wiki/ or CLAUDE.md found)');\n process.exitCode = 2;\n return;\n }\n\n if (opts.write) {\n process.exitCode = runWrite(corpus);\n } else {\n process.exitCode = runCheck(corpus);\n }\n });\n}\n","import type { Command } from 'commander';\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport matter from 'gray-matter';\nimport trash from 'trash';\nimport { collectMdFiles, extractFrontmatter, findSourceByUrl, requireCorpus } from '../lib/corpus.js';\nimport { loadIngestState, saveIngestState } from '../lib/ingest-state.js';\nimport { todayYMDShanghai, tsCompact } from '../lib/date.js';\nimport { createSnapshot } from './snapshot.js';\nimport { runSync } from './sync.js';\nimport { printLintReport, runLint } from './lint.js';\nimport { pruneVectorDbMissingFiles } from '../lib/vectordb/prune.js';\nimport { bad, err, ok, out, print } from '../utils/logger.js';\n\ninterface TrashTarget {\n rel: string;\n abs: string;\n reason: 'target' | 'source' | 'summary';\n}\n\ninterface PageChange {\n file: string;\n removedLines: string[];\n removedSources: string[];\n sourceCountBefore?: number;\n sourceCountAfter?: number;\n}\n\ninterface ReviewItem {\n file: string;\n section: 'Compiled Truth';\n text: string;\n}\n\ninterface RemovalPlan {\n input: string;\n apply: boolean;\n trashTargets: TrashTarget[];\n pageChanges: PageChange[];\n reviewItems: ReviewItem[];\n ingestRecords: string[];\n aliases: string[];\n snapshot?: string;\n syncSkippedVector?: boolean;\n vectorPruned?: number;\n lintIssues?: number;\n}\n\nfunction isUrl(input: string): boolean {\n return /^https?:\\/\\//i.test(input);\n}\n\nfunction toSlash(p: string): string {\n return p.split(sep).join('/');\n}\n\nfunction stripMd(rel: string): string {\n return rel.replace(/\\.md$/, '');\n}\n\nfunction normalizeRel(rel: string): string {\n return toSlash(rel).replace(/^\\.\\//, '').replace(/\\/+/g, '/');\n}\n\nfunction withinCorpus(corpus: string, abs: string): boolean {\n const rel = relative(corpus, abs);\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n}\n\nfunction resolveInputPath(corpus: string, input: string): string | null {\n const candidates = [];\n const rawAbs = isAbsolute(input) ? input : join(corpus, input);\n candidates.push(rawAbs);\n if (!input.endsWith('.md')) candidates.push(`${rawAbs}.md`);\n for (const candidate of candidates) {\n const abs = resolve(candidate);\n if (withinCorpus(corpus, abs) && existsSync(abs)) return abs;\n }\n return null;\n}\n\nfunction relFromAbs(corpus: string, abs: string): string {\n return normalizeRel(relative(corpus, abs));\n}\n\nfunction aliasesForRel(rel: string): string[] {\n const aliases = new Set<string>();\n const normalized = normalizeRel(rel);\n aliases.add(stripMd(normalized));\n if (normalized.endsWith('/article.md')) {\n aliases.add(stripMd(normalized).replace(/\\/article$/, ''));\n }\n return [...aliases];\n}\n\nfunction readText(abs: string): string {\n return readFileSync(abs, 'utf-8');\n}\n\nfunction extractWikilinks(content: string): string[] {\n const links: string[] = [];\n const linkRe = /\\[\\[([^\\]|#]+)[^\\]]*\\]\\]/g;\n let m: RegExpExecArray | null;\n while ((m = linkRe.exec(content)) !== null) links.push(m[1].trim());\n return links;\n}\n\nfunction addExistingTarget(\n corpus: string,\n targets: Map<string, TrashTarget>,\n relOrAbs: string,\n reason: TrashTarget['reason'],\n): void {\n const abs = isAbsolute(relOrAbs) ? relOrAbs : join(corpus, relOrAbs);\n if (!existsSync(abs)) return;\n const rel = relFromAbs(corpus, abs);\n targets.set(rel, { rel, abs, reason });\n}\n\nfunction addSourceTarget(\n corpus: string,\n targets: Map<string, TrashTarget>,\n relOrAbs: string,\n): void {\n const abs = isAbsolute(relOrAbs) ? relOrAbs : join(corpus, relOrAbs);\n if (!existsSync(abs)) return;\n\n const rel = relFromAbs(corpus, abs);\n if (rel.endsWith('/article.md')) {\n addExistingTarget(corpus, targets, dirname(abs), 'source');\n return;\n }\n\n addExistingTarget(corpus, targets, abs, 'source');\n\n if (rel.endsWith('.md')) {\n const assetsDir = abs.replace(/\\.md$/, '.assets');\n addExistingTarget(corpus, targets, assetsDir, 'source');\n }\n}\n\nfunction sourceCandidatesForSlug(corpus: string, slug: string): string[] {\n return [\n join(corpus, slug),\n join(corpus, `${slug}.md`),\n join(corpus, slug, 'article.md'),\n ];\n}\n\nfunction collectSourceUrls(corpus: string, targets: Map<string, TrashTarget>): string[] {\n const urls = new Set<string>();\n for (const target of targets.values()) {\n const files = existsSync(target.abs) && target.rel.endsWith('.md')\n ? [target.abs]\n : collectMdFiles(target.abs);\n for (const file of files) {\n const fm = extractFrontmatter(file);\n if (typeof fm.source_url === 'string') urls.add(fm.source_url);\n if (typeof fm.url === 'string') urls.add(fm.url);\n }\n }\n return [...urls];\n}\n\nfunction addSourcesFromSummary(\n corpus: string,\n targets: Map<string, TrashTarget>,\n summaryAbs: string,\n): void {\n const parsed = matter(readText(summaryAbs));\n const sources = Array.isArray(parsed.data.sources) ? parsed.data.sources : [];\n for (const source of sources) {\n if (typeof source !== 'string') continue;\n for (const candidate of sourceCandidatesForSlug(corpus, source)) {\n if (existsSync(candidate)) addSourceTarget(corpus, targets, candidate);\n }\n }\n\n for (const link of extractWikilinks(parsed.content)) {\n if (!link.startsWith('原料/')) continue;\n for (const candidate of sourceCandidatesForSlug(corpus, link)) {\n if (existsSync(candidate)) addSourceTarget(corpus, targets, candidate);\n }\n }\n}\n\nfunction addSummariesReferencingSources(\n corpus: string,\n targets: Map<string, TrashTarget>,\n aliases: Set<string>,\n): void {\n for (const file of collectMdFiles(join(corpus, '知识库', '摘要'))) {\n const rel = relFromAbs(corpus, file);\n if (targets.has(rel)) continue;\n const content = readText(file);\n if ([...aliases].some((alias) => content.includes(`[[${alias}`))) {\n addExistingTarget(corpus, targets, file, 'summary');\n addSourcesFromSummary(corpus, targets, file);\n }\n }\n}\n\nfunction compiledTruthSnippets(content: string, aliases: Set<string>, input: string): string[] {\n const body = content.replace(/^---\\n[\\s\\S]*?\\n---\\n/, '');\n const match = body.match(/##\\s*Compiled Truth\\s*\\n+([\\s\\S]*?)(?=\\n##\\s|$)/);\n if (!match) return [];\n return match[1]\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter((p) => {\n if (!p) return false;\n if (isUrl(input) && p.includes(input)) return true;\n return [...aliases].some((alias) => p.includes(`[[${alias}`));\n });\n}\n\nfunction rewritePageForRemoval(\n corpus: string,\n file: string,\n aliases: Set<string>,\n): { change: PageChange | null; nextContent: string } {\n const rel = relFromAbs(corpus, file);\n const parsed = matter(readText(file));\n const removedSources: string[] = [];\n let sourceCountBefore: number | undefined;\n let sourceCountAfter: number | undefined;\n\n if (Array.isArray(parsed.data.sources)) {\n const nextSources = parsed.data.sources.filter((source: unknown) => {\n if (typeof source !== 'string') return true;\n const remove = aliases.has(stripMd(normalizeRel(source)));\n if (remove) removedSources.push(source);\n return !remove;\n });\n if (removedSources.length > 0) {\n parsed.data.sources = nextSources;\n const rawCount = parsed.data.source_count;\n const numeric =\n typeof rawCount === 'number'\n ? rawCount\n : typeof rawCount === 'string'\n ? Number.parseInt(rawCount, 10)\n : Number.NaN;\n if (Number.isFinite(numeric)) {\n sourceCountBefore = numeric;\n sourceCountAfter = Math.max(0, numeric - new Set(removedSources).size);\n parsed.data.source_count = sourceCountAfter;\n }\n parsed.data.updated = todayYMDShanghai();\n }\n }\n\n const removedLines: string[] = [];\n const nextLines = parsed.content.split('\\n').filter((line) => {\n const trimmed = line.trim();\n const hasTargetLink = [...aliases].some((alias) => line.includes(`[[${alias}`));\n const removable = hasTargetLink && /^[-*]\\s+/.test(trimmed);\n if (removable) {\n removedLines.push(line);\n return false;\n }\n return true;\n });\n if (removedLines.length > 0) parsed.data.updated = todayYMDShanghai();\n\n const changed = removedLines.length > 0 || removedSources.length > 0;\n const nextContent = changed ? matter.stringify(nextLines.join('\\n'), parsed.data) : readText(file);\n\n return {\n nextContent,\n change: changed\n ? {\n file: rel,\n removedLines,\n removedSources,\n sourceCountBefore,\n sourceCountAfter,\n }\n : null,\n };\n}\n\nfunction buildRemovalPlan(corpus: string, input: string, apply: boolean): RemovalPlan {\n const targets = new Map<string, TrashTarget>();\n const ingestRecords = new Set<string>();\n\n if (isUrl(input)) {\n const state = loadIngestState(corpus);\n const record = state.ingests[input];\n ingestRecords.add(input);\n if (record?.archivedTo) addSourceTarget(corpus, targets, record.archivedTo);\n for (const page of record?.wikiPages ?? []) {\n if (normalizeRel(page).startsWith('知识库/摘要/')) {\n const pageAbs = join(corpus, page);\n addExistingTarget(corpus, targets, pageAbs, 'summary');\n if (existsSync(pageAbs)) addSourcesFromSummary(corpus, targets, pageAbs);\n }\n }\n const source = findSourceByUrl(corpus, input);\n if (source) addSourceTarget(corpus, targets, source);\n } else {\n const abs = resolveInputPath(corpus, input);\n if (!abs) throw new Error(`target not found inside corpus: ${input}`);\n const rel = relFromAbs(corpus, abs);\n if (rel.startsWith('原料/')) {\n addSourceTarget(corpus, targets, abs);\n } else if (rel.startsWith('知识库/摘要/')) {\n addExistingTarget(corpus, targets, abs, 'summary');\n addSourcesFromSummary(corpus, targets, abs);\n } else {\n addExistingTarget(corpus, targets, abs, 'target');\n }\n }\n\n let aliases = new Set([...targets.keys()].flatMap((rel) => aliasesForRel(rel)));\n addSummariesReferencingSources(corpus, targets, aliases);\n aliases = new Set([...targets.keys()].flatMap((rel) => aliasesForRel(rel)));\n\n for (const url of collectSourceUrls(corpus, targets)) ingestRecords.add(url);\n\n const trashedRels = new Set(targets.keys());\n const pageChanges: PageChange[] = [];\n const reviewItems: ReviewItem[] = [];\n for (const file of collectMdFiles(corpus)) {\n const rel = relFromAbs(corpus, file);\n if (trashedRels.has(rel)) continue;\n if ([...trashedRels].some((targetRel) => rel.startsWith(`${targetRel}/`))) continue;\n\n const { change } = rewritePageForRemoval(corpus, file, aliases);\n if (change) pageChanges.push(change);\n for (const text of compiledTruthSnippets(readText(file), aliases, input)) {\n reviewItems.push({ file: rel, section: 'Compiled Truth', text });\n }\n }\n\n return {\n input,\n apply,\n trashTargets: [...targets.values()].sort((a, b) => a.rel.localeCompare(b.rel)),\n pageChanges,\n reviewItems,\n ingestRecords: [...ingestRecords],\n aliases: [...aliases].sort(),\n };\n}\n\nasync function moveToTrash(paths: string[]): Promise<void> {\n const testTrashDir = process.env.LOREKIT_TEST_TRASH_DIR;\n if (testTrashDir) {\n mkdirSync(testTrashDir, { recursive: true });\n for (const p of paths) {\n if (!existsSync(p)) continue;\n const dest = join(testTrashDir, `${tsCompact()}-${basename(p)}`);\n renameSync(p, dest);\n }\n return;\n }\n await trash(paths, { glob: false });\n}\n\nfunction applyPageChanges(corpus: string, plan: RemovalPlan): void {\n const aliases = new Set(plan.aliases);\n for (const change of plan.pageChanges) {\n const file = join(corpus, change.file);\n const { nextContent } = rewritePageForRemoval(corpus, file, aliases);\n writeFileSync(file, nextContent, 'utf-8');\n }\n}\n\nfunction forgetIngestRecords(corpus: string, urls: string[]): void {\n if (urls.length === 0) return;\n const state = loadIngestState(corpus);\n let changed = false;\n for (const url of urls) {\n if (state.ingests[url]) {\n delete state.ingests[url];\n changed = true;\n }\n }\n if (changed) saveIngestState(corpus, state);\n}\n\nfunction printPlan(plan: RemovalPlan): void {\n print(`lorekit remove — ${plan.apply ? 'apply' : 'dry-run'}\\n`);\n\n print(`将移动到系统回收站 (${plan.trashTargets.length})`);\n for (const target of plan.trashTargets) {\n print(` - ${target.rel} (${target.reason})`);\n }\n if (plan.trashTargets.length === 0) print(' - (无)');\n print();\n\n print(`将修改页面 (${plan.pageChanges.length})`);\n for (const change of plan.pageChanges) {\n print(` - ${change.file}`);\n if (change.removedSources.length > 0) {\n print(` sources: -${change.removedSources.length}`);\n }\n if (change.sourceCountBefore !== undefined && change.sourceCountAfter !== undefined) {\n print(` source_count: ${change.sourceCountBefore} -> ${change.sourceCountAfter}`);\n }\n if (change.removedLines.length > 0) {\n print(` lines: -${change.removedLines.length}`);\n }\n }\n if (plan.pageChanges.length === 0) print(' - (无)');\n print();\n\n if (plan.reviewItems.length > 0) {\n print(`需人工复核 Compiled Truth (${plan.reviewItems.length})`);\n for (const item of plan.reviewItems) {\n print(` - ${item.file}: ${item.text.slice(0, 120)}`);\n }\n print();\n }\n\n if (!plan.apply) {\n print('dry-run only. Run again with --apply to move files to OS Trash.');\n }\n}\n\nexport function removeCommand(program: Command): void {\n program\n .command('remove')\n .argument('<target>', 'URL or corpus-relative path to remove')\n .option('--apply', 'execute the removal; default is dry-run', false)\n .option('--json', 'emit a machine-readable JSON report', false)\n .description('safely remove a source/wiki page and provenance-linked references')\n .action(async (target: string, opts: { apply?: boolean; json?: boolean }) => {\n const corpus = requireCorpus();\n let plan: RemovalPlan;\n try {\n plan = buildRemovalPlan(corpus, target, !!opts.apply);\n } catch (e) {\n err((e as Error).message);\n process.exitCode = 2;\n return;\n }\n\n if (!opts.json) printPlan(plan);\n if (opts.json && !opts.apply) out(JSON.stringify(plan));\n if (!opts.apply) return;\n\n if (plan.trashTargets.length === 0 && plan.pageChanges.length === 0) {\n bad('nothing to remove');\n process.exitCode = 1;\n if (opts.json) out(JSON.stringify(plan));\n return;\n }\n\n try {\n const snapshot = await createSnapshot(corpus, { tag: 'remove' });\n plan.snapshot = snapshot;\n ok(`snapshot saved: ${snapshot}`);\n\n applyPageChanges(corpus, plan);\n forgetIngestRecords(corpus, plan.ingestRecords);\n await moveToTrash(plan.trashTargets.map((t) => t.abs));\n ok(`moved ${plan.trashTargets.length} item(s) to OS Trash`);\n\n const hasVectorDb = existsSync(join(corpus, '.wiki', 'vector.sqlite'));\n if (hasVectorDb) {\n plan.vectorPruned = await pruneVectorDbMissingFiles(corpus);\n if (plan.vectorPruned > 0) ok(`vector pruned ${plan.vectorPruned} missing file(s)`);\n }\n\n const skipVector = !hasVectorDb || process.env.LOREKIT_TEST_SKIP_VECTOR_SYNC === '1';\n plan.syncSkippedVector = skipVector;\n await runSync(corpus, { skipVector });\n\n const issues = runLint(corpus);\n plan.lintIssues = issues.length;\n printLintReport(corpus, issues);\n } catch (e) {\n err((e as Error).message);\n process.exitCode = 1;\n }\n\n if (opts.json) out(JSON.stringify(plan));\n });\n}\n","import type { Command } from 'commander';\nimport { requireCorpus } from '../lib/corpus.js';\nimport {\n doctorGbrain,\n exportForGbrain,\n getGbrainStatus,\n queryGbrain,\n syncGbrain,\n} from '../lib/integrations/gbrain.js';\nimport { bad, info, ok, out, print, warn } from '../utils/logger.js';\n\nfunction printJson(result: unknown): void {\n out(JSON.stringify(result, null, 2));\n}\n\nexport function gbrainCommand(program: Command): void {\n const cmd = program\n .command('gbrain')\n .description('optional GBrain read-only integration');\n\n cmd\n .command('status')\n .description('check whether GBrain is installed')\n .option('--json', 'output json', false)\n .action(async (opts: { json?: boolean }) => {\n const result = await getGbrainStatus();\n if (opts.json) {\n printJson(result);\n return;\n }\n if (result.installed) {\n ok(`GBrain installed: ${result.version ?? result.binary}`);\n } else {\n warn('GBrain is not installed');\n print(result.installHint);\n }\n });\n\n cmd\n .command('export')\n .description('export lorekit 知识库/ pages into a GBrain-safe staging directory')\n .option('--out <dir>', 'export directory relative to corpus')\n .option('--dry-run', 'preview only; do not write files', false)\n .option('--json', 'output json', false)\n .action((opts: { out?: string; dryRun?: boolean; json?: boolean }) => {\n const corpus = requireCorpus();\n const result = exportForGbrain(corpus, { out: opts.out, dryRun: opts.dryRun });\n if (opts.json) {\n printJson(result);\n return;\n }\n if (result.dryRun) {\n info(`would export ${result.pagesExported} page(s) to ${result.exportDir}`);\n } else {\n ok(`exported ${result.pagesExported} page(s) to ${result.exportDir}`);\n }\n if (result.pagesSkipped > 0) warn(`skipped ${result.pagesSkipped} index file(s)`);\n for (const w of result.warnings) warn(w);\n });\n\n cmd\n .command('sync')\n .description('export lorekit pages and run gbrain import on the staging directory')\n .option('--dry-run', 'preview only; do not write export files or call gbrain import', false)\n .option('--json', 'output json', false)\n .option(\n '--export-even-if-missing',\n 'refresh staging export even when the gbrain binary is missing',\n false,\n )\n .action(async (opts: { dryRun?: boolean; json?: boolean; exportEvenIfMissing?: boolean }) => {\n const corpus = requireCorpus();\n const result = await syncGbrain(corpus, opts);\n if (opts.json) {\n printJson(result);\n } else if (result.status === 'ok') {\n if (result.dryRun) {\n info(`would export ${result.export?.pagesExported ?? 0} page(s); gbrain import skipped`);\n } else {\n ok(`gbrain sync complete: ${result.export?.pagesExported ?? 0} page(s) exported`);\n }\n } else {\n bad(`gbrain sync failed: ${result.errors.join('; ')}`);\n }\n process.exitCode = result.status === 'ok' ? 0 : 1;\n });\n\n cmd\n .command('doctor')\n .description('check GBrain integration health')\n .option('--json', 'output json', false)\n .action(async (opts: { json?: boolean }) => {\n const corpus = requireCorpus();\n const result = await doctorGbrain(corpus);\n if (opts.json) {\n printJson(result);\n } else {\n if (result.status === 'ok') ok('GBrain integration healthy');\n for (const issue of result.issues) {\n const line = `${issue.message}. ${issue.recommendation}`;\n if (issue.severity === 'error') bad(line);\n else warn(line);\n }\n }\n process.exitCode = result.status === 'error' ? 1 : 0;\n });\n\n cmd\n .command('query')\n .argument('<text>', 'query text')\n .description('run gbrain query without writing back to lorekit')\n .option('--json', 'output json', false)\n .option('--no-stale-check', 'skip corpus export/sync freshness guard')\n .action(async (text: string, opts: { json?: boolean; staleCheck?: boolean }) => {\n const corpus = requireCorpus();\n const result = await queryGbrain(corpus, text, { staleCheck: opts.staleCheck !== false });\n if (opts.json) {\n printJson(result);\n } else {\n info(result.message);\n for (const w of result.warnings) warn(w);\n if (result.gbrain?.stdout) print(result.gbrain.stdout.trim());\n if (result.gbrain?.stderr) warn(result.gbrain.stderr.trim());\n if (result.status === 'error') bad(result.errors.join('; '));\n }\n process.exitCode = result.status === 'ok' ? 0 : 1;\n });\n}\n"],"mappings":"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","names":["createHash","readFileSync","readdirSync","basename","join","relative","matter","sha256","existsSync","mkdirSync","join","Database","readFileSync","basename","matter","relative","chunkFile","sha256","existsSync","readFileSync","readdirSync","join","relative","matter","createHash","existsSync","join","rec","existsSync","chalk","readFileSync","statSync","join","dirname","join","dirname","readFileSync","existsSync","readdirSync","join","chalk","resolve","existsSync","readdirSync","join","version","program","chalk","existsSync","lstatSync","readFileSync","readdirSync","join","relative","chalk","existsSync","readFileSync","join","join","readFileSync","existsSync","existsSync","mkdirSync","readFileSync","join","existsSync","join","resolve","existsSync","join","version","createHash","existsSync","mkdirSync","readFileSync","readdirSync","statSync","writeFileSync","dirname","join","relative","resolve","matter","existsSync","readFileSync","writeFileSync","createHash","out","join","resolve","existsSync","readdirSync","relative","mkdirSync","matter","readFileSync","statSync","dirname","writeFileSync","join","mkdirSync","exportResult","result","existsSync","readFileSync","join","existsSync","chalk","readFileSync","readdirSync","relative","lstatSync","program","issues","readFileSync","statSync","relative","program","relative","statSync","readFileSync","readFileSync","relative","basename","chalk","basename","relative","readFileSync","chalk","program","existsSync","mkdirSync","readFileSync","writeFileSync","join","basename","readFileSync","join","existsSync","basename","mkdirSync","writeFileSync","program","existsSync","readdirSync","readFileSync","statSync","writeFileSync","lstatSync","join","basename","relative","resolve","readFileSync","basename","statSync","relative","join","readdirSync","lstatSync","writeFileSync","existsSync","resolve","program","existsSync","mkdirSync","readdirSync","lstatSync","join","lstatSync","program","join","existsSync","readdirSync","mkdirSync","existsSync","mkdirSync","writeFileSync","unlinkSync","readdirSync","statSync","join","relative","readdirSync","join","relative","mkdirSync","statSync","writeFileSync","existsSync","unlinkSync","program","existsSync","mkdirSync","readFileSync","join","dirname","createInterface","tar","chalk","ask","createInterface","resolve","program","existsSync","join","mkdirSync","readFileSync","chalk","dirname","readFileSync","join","relative","join","relative","readFileSync","program","createHash","existsSync","readFileSync","join","relative","relative","embed","embedSingle","openDb","syncFile","buildLayeredIndex","collectFiles","relative","createHash","readFileSync","program","queryFlat","queryLayered","queryBM25Layered","queryHybrid","join","existsSync","getStatus","info","existsSync","mkdirSync","join","relative","mkdir","writeFile","join","today","SHANGHAI_TZ_OFFSET_MS","family","join","join","cheerio","mkdir","writeFile","join","cheerio","err","mkdir","today","join","writeFile","mkdir","writeFile","join","mkdir","today","join","writeFile","join","mkdir","today","writeFile","existsSync","mkdirSync","readFileSync","writeFileSync","join","dirname","program","join","existsSync","mkdirSync","relative","existsSync","readFileSync","writeFileSync","join","relative","join","existsSync","readFileSync","writeFileSync","sep","program","relative","chalk","mkdirSync","writeFileSync","join","existsSync","readFileSync","readdirSync","writeFileSync","join","out","writeSyncReport","join","mkdirSync","writeFileSync","chalk","program","cpSync","existsSync","mkdirSync","writeFileSync","join","join","existsSync","mkdirSync","cpSync","writeFileSync","program","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","basename","dirname","join","relative","resolve","matter","relative","join","resolve","existsSync","readFileSync","dirname","matter","mkdirSync","basename","renameSync","writeFileSync","program","program","existsSync","chalk"]} \ No newline at end of file diff --git a/src/commands/search.ts b/src/commands/search.ts index e42f137..82369dd 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -30,10 +30,17 @@ function searchWithRipgrep( const result = spawnSync('rg', args, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, + // 30s 上限:规避恶意 / 退化 regex 在巨型 corpus 上拖垮 CLI(PR #5 review M3)。 + // ripgrep 正常扫几 GB markdown 也只要几秒,30s 是宽松值。 + timeout: 30_000, }); if (result.error) { - // rg not found + // rg 未安装 → 返回空让上层 fallback; + // 也可能是 timeout(result.signal === 'SIGTERM'),同样返回空 + 上层 fallback + if ((result as { signal?: string }).signal === 'SIGTERM') { + warn(`rg timed out after 30s, falling back to built-in scan`); + } return []; } diff --git a/src/lib/fetcher/http.ts b/src/lib/fetcher/http.ts index a9744b1..352e48f 100644 --- a/src/lib/fetcher/http.ts +++ b/src/lib/fetcher/http.ts @@ -6,8 +6,18 @@ * * 依赖关系:本文件不 import fetcher/helpers.ts 或 fetcher/images.ts,self-contained。 * `HTTP_TIMEOUT_MS` 同时被 fetcher/images.ts 引用(images→http 单向依赖)。 + * + * SSRF 防御(PR #5): + * `fetchHtmlL1` 默认拒绝抓取解析到 RFC 1918 / RFC 6890 私网范围的目标, + * 并手动跟随 redirect(最多 5 跳)逐跳重检,避免 redirect-to-private-ip 绕过。 + * 用户可通过环境变量 `LOREKIT_FETCH_ALLOW_PRIVATE=1` opt-out(本地开发 / 自建 + * localhost wiki 场景)。注意:该检查仅作用于 fetcher,**不影响** `lib/ollama.ts` + * 的本地 localhost:11434 调用(那是另一个 import 路径,未经 http.ts)。 */ +import { lookup } from 'node:dns/promises'; +import { BlockList, isIP } from 'node:net'; + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- @@ -26,6 +36,11 @@ const UA_DESKTOP = */ export const HTTP_TIMEOUT_MS = 20_000; +/** + * 手动跟随 redirect 的最大跳数。超过即拒绝(防 redirect 循环 / SSRF 跳板)。 + */ +const MAX_REDIRECTS = 5; + const ANTIBOT_TRIGGERS = [ '环境异常', '请在微信客户端打开', @@ -34,6 +49,110 @@ const ANTIBOT_TRIGGERS = [ 'cf-browser-verification', ]; +// --------------------------------------------------------------------------- +// SSRF guard +// --------------------------------------------------------------------------- + +/** + * 私网 / 链路本地 / loopback 地址段。RFC 1918 / RFC 6890 / RFC 4193。 + * 与 `node:net.BlockList` 配合使用做 IP 归属判断。 + */ +const PRIVATE_BLOCKS = (() => { + const bl = new BlockList(); + // IPv4 RFC 1918 + loopback + link-local + bl.addSubnet('127.0.0.0', 8, 'ipv4'); + bl.addSubnet('10.0.0.0', 8, 'ipv4'); + bl.addSubnet('172.16.0.0', 12, 'ipv4'); + bl.addSubnet('192.168.0.0', 16, 'ipv4'); + bl.addSubnet('169.254.0.0', 16, 'ipv4'); + bl.addSubnet('0.0.0.0', 8, 'ipv4'); // 0.0.0.0/8 — "this network" + // IPv6 loopback + ULA + link-local + bl.addAddress('::1', 'ipv6'); + bl.addSubnet('fc00::', 7, 'ipv6'); + bl.addSubnet('fe80::', 10, 'ipv6'); + return bl; +})(); + +/** + * 由 SSRF guard 抛出的 sentinel error。调用方可用 `instanceof` 识别后短路 L2 + * fallback,把私网拒绝原因冒泡给用户(而不是被通用 catch 误判为 antibot)。 + */ +export class PrivateAddressError extends Error { + readonly code = 'SSRF_PRIVATE_ADDRESS'; + constructor( + message: string, + public readonly host: string, + public readonly address: string, + ) { + super(message); + this.name = 'PrivateAddressError'; + } +} + +function isPrivateOptOut(): boolean { + const v = process.env.LOREKIT_FETCH_ALLOW_PRIVATE; + return v === '1' || v === 'true'; +} + +/** + * 检查给定 host 的 DNS 解析结果是否落在私网范围。 + * - host 已经是 IP 字面量:直接判定 + * - 否则 `dns.lookup()` 拿 A/AAAA 记录后判定 + * + * 命中即抛 `PrivateAddressError`;未命中静默返回。 + * 若环境变量 `LOREKIT_FETCH_ALLOW_PRIVATE=1` 已设置,本函数直接跳过检查。 + */ +export async function assertPublicAddress(urlStr: string): Promise<void> { + if (isPrivateOptOut()) return; + + let parsed: URL; + try { + parsed = new URL(urlStr); + } catch { + // URL 无法解析交给上游 fetch 报错,不在 SSRF 层判 + return; + } + + // 仅检查 http(s);其它 scheme(data:/file:)由上游决定是否拒绝 + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return; + + const rawHost = parsed.hostname; + // URL.hostname 对 IPv6 会带方括号 [::1],去掉以匹配 isIP / dns.lookup + const host = rawHost.startsWith('[') && rawHost.endsWith(']') ? rawHost.slice(1, -1) : rawHost; + if (!host) return; + + const ipVersion = isIP(host); + if (ipVersion === 4 || ipVersion === 6) { + const family = ipVersion === 4 ? 'ipv4' : 'ipv6'; + if (PRIVATE_BLOCKS.check(host, family)) { + throw new PrivateAddressError( + `refusing to fetch private address ${host} (set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`, + host, + host, + ); + } + return; + } + + // 普通域名 → DNS 解析 + let addr: { address: string; family: number }; + try { + addr = await lookup(host); + } catch { + // DNS 失败交给上游 fetch 报错(用户能看到更明确的 ENOTFOUND) + return; + } + const family = addr.family === 6 ? 'ipv6' : 'ipv4'; + if (PRIVATE_BLOCKS.check(addr.address, family)) { + throw new PrivateAddressError( + `refusing to fetch ${host} which resolves to private address ${addr.address} ` + + `(set LOREKIT_FETCH_ALLOW_PRIVATE=1 to override)`, + host, + addr.address, + ); + } +} + // --------------------------------------------------------------------------- // Site detection / headers // --------------------------------------------------------------------------- @@ -85,19 +204,55 @@ export function detectAntibot(html: string, site: string): boolean { /** * 用 Node 内置 fetch 拉 HTML,超时由 HTTP_TIMEOUT_MS 控制。 - * 失败抛 Error(HTTP 非 2xx 或 abort),由调用方决定是否走 L2 fallback。 + * + * 行为说明: + * - 默认拒绝私网(RFC 1918 / RFC 6890)目标;可由 `LOREKIT_FETCH_ALLOW_PRIVATE=1` + * 绕过(本地开发场景)。失败抛 `PrivateAddressError` + * - `redirect: 'manual'` 手动跟随,最多 `MAX_REDIRECTS=5` 跳,每跳前对目标 URL + * 重新做 SSRF 检查(避免公网 URL 通过 302 跳板进内网) + * - HTTP 非 2xx 或 abort 抛 `Error`,由调用方决定是否走 L2 fallback */ export async function fetchHtmlL1(url: string, headers: Record<string, string>): Promise<string> { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS); try { - const res = await fetch(url, { - headers, - redirect: 'follow', - signal: controller.signal, - }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - return await res.text(); + let currentUrl = url; + for (let hop = 0; hop <= MAX_REDIRECTS; hop++) { + // 每跳前 SSRF 检查(首跳也查) + await assertPublicAddress(currentUrl); + + const res = await fetch(currentUrl, { + headers, + redirect: 'manual', + signal: controller.signal, + }); + + // 3xx with Location → 手动跟随 + if (res.status >= 300 && res.status < 400) { + const loc = res.headers.get('location'); + if (!loc) { + // 没有 Location header 的 3xx 当作错误返回 + throw new Error(`HTTP ${res.status} without Location header`); + } + if (hop === MAX_REDIRECTS) { + throw new Error(`too many redirects (>${MAX_REDIRECTS}) starting from ${url}`); + } + // 相对 URL 用 currentUrl 作 base 解析 + currentUrl = new URL(loc, currentUrl).toString(); + // 释放 body,避免连接泄漏 + try { + await res.arrayBuffer(); + } catch { + // 流式响应可能已 abort,吞掉 + } + continue; + } + + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return await res.text(); + } + // 理论不可达:循环出口都在 return / throw + throw new Error(`unreachable: redirect loop exit ${url}`); } finally { clearTimeout(timer); } @@ -114,7 +269,7 @@ export async function fetchHtmlL1(url: string, headers: Record<string, string>): export async function fetchHtmlL2(url: string): Promise<string | null> { try { // Dynamic import — playwright-core is optional - // @ts-ignore — playwright-core may not be installed + // @ts-expect-error — playwright-core 是可选依赖,类型可能未安装 const pw = await import('playwright-core'); const browser = await pw.chromium.launch({ headless: true }); try { diff --git a/src/lib/fetcher/index.ts b/src/lib/fetcher/index.ts index 109d5c4..38b6298 100644 --- a/src/lib/fetcher/index.ts +++ b/src/lib/fetcher/index.ts @@ -30,6 +30,7 @@ import { detectSite, fetchHtmlL1, fetchHtmlL2, + PrivateAddressError, } from './http.js'; import { downloadImages, rewriteMarkdownImages } from './images.js'; import { parseGeneric } from './routes/web.js'; @@ -65,8 +66,21 @@ export async function fetchUrl(url: string, opts: FetchOptions): Promise<FetchRe if (detectAntibot(html, site)) { html = ''; } - } catch { - // L1 失败(HTTP 非 2xx / abort / 网络错误)→ 退 L2 fallback + } catch (e) { + // SSRF guard 拒绝时直接冒泡为 FetchResult error,不退 L2 fallback + // (L2 playwright 同样会被绕过 guard 命中私网,必须显式拒绝) + if (e instanceof PrivateAddressError) { + return { + status: 'error', + route: 'rich', + url, + reason: 'PRIVATE_ADDRESS_BLOCKED', + suggest: + `target resolves to private address ${e.address}. ` + + 'Set LOREKIT_FETCH_ALLOW_PRIVATE=1 to allow (local dev only).', + }; + } + // 其它 L1 失败(HTTP 非 2xx / abort / 网络错误)→ 退 L2 fallback html = ''; } diff --git a/tests/smoke/fetch-ssrf.test.mjs b/tests/smoke/fetch-ssrf.test.mjs new file mode 100644 index 0000000..09efe27 --- /dev/null +++ b/tests/smoke/fetch-ssrf.test.mjs @@ -0,0 +1,58 @@ +// fetch SSRF guard smoke(PR #5)。 +// +// 验证两条核心路径: +// 1. 默认配置下,`lorekit fetch http://127.0.0.1:<port>/x` 应被 SSRF guard 拒绝, +// 退出码非 0,输出(stdout 或 stderr)含 "private" / "PRIVATE_ADDRESS" 关键字 +// 2. 设置 `LOREKIT_FETCH_ALLOW_PRIVATE=1` opt-out 后,应能跨过 guard 进入实际网络请求, +// 然后因端口关闭(或 antibot 报错)失败 —— 失败原因**不应**是 PRIVATE_ADDRESS +// +// 注意:这里用 127.0.0.1 + 一个大概率没人监听的随机高位端口;fetch.ts 在 corpus 外 +// 跑也支持(会写到 /tmp/lorekit-fetch),方便从 REPO_ROOT 直接调用。 + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { runLorekit, mkTmpDir, cleanupTmpDir, fmtRun } from './_util.mjs'; + +// 用一个大概率空闲的高位端口(动态 / 临时端口范围),避免误中真实服务 +const UNUSED_PORT = 59387; +const PRIVATE_URL = `http://127.0.0.1:${UNUSED_PORT}/lorekit-ssrf-smoke`; + +test('SSRF guard 默认拒绝抓取 127.0.0.1(私网)目标', () => { + const tmp = mkTmpDir('lorekit-smoke-ssrf-deny-'); + try { + const args = ['fetch', PRIVATE_URL]; + const r = runLorekit(args, { cwd: tmp }); + assert.notEqual(r.status, 0, fmtRun(r, args, 'exit != 0 (private 被拒)')); + // stdout 走 JSON(含 reason: PRIVATE_ADDRESS_BLOCKED),stderr 可能有补充提示 + const combined = `${r.stdout}\n${r.stderr}`; + assert.match( + combined, + /private|PRIVATE_ADDRESS/i, + fmtRun(r, args, 'output 含 "private" / "PRIVATE_ADDRESS"'), + ); + } finally { + cleanupTmpDir(tmp); + } +}); + +test('SSRF guard 在 LOREKIT_FETCH_ALLOW_PRIVATE=1 时放行(仍会因端口不通而失败,但理由不再是 private)', () => { + const tmp = mkTmpDir('lorekit-smoke-ssrf-allow-'); + try { + const args = ['fetch', PRIVATE_URL]; + const r = runLorekit(args, { + cwd: tmp, + env: { LOREKIT_FETCH_ALLOW_PRIVATE: '1' }, + }); + // 端口空闲 → ECONNREFUSED → fetcher 走 L2(playwright 缺也返回 null)→ ANTIBOT_BLOCKED + // 总之 exit 非 0,但失败 reason 不应是 PRIVATE_ADDRESS_BLOCKED + assert.notEqual(r.status, 0, fmtRun(r, args, 'exit != 0 (端口拒绝)')); + const combined = `${r.stdout}\n${r.stderr}`; + assert.doesNotMatch( + combined, + /PRIVATE_ADDRESS_BLOCKED/, + fmtRun(r, args, 'output 不含 "PRIVATE_ADDRESS_BLOCKED"(opt-out 应已放行)'), + ); + } finally { + cleanupTmpDir(tmp); + } +});