@thincoder/core 0.10.9 → 0.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/advisor/messages.mjs +4 -5
  2. package/advisor/project-context.mjs +69 -44
  3. package/agent/family-tools.mjs +9 -7
  4. package/agent/helpers.mjs +6 -0
  5. package/agent/run-start.mjs +1 -0
  6. package/agent/turn-loop.mjs +17 -13
  7. package/agent-tools/batch-gate.mjs +4 -5
  8. package/agent-tools/checkpoint.mjs +11 -6
  9. package/agent-tools/doc-check.mjs +18 -13
  10. package/agent-tools/manifest.mjs +44 -12
  11. package/agent-tools/subagent-spawn.mjs +34 -19
  12. package/agent-tools/subagent.mjs +1 -1
  13. package/agent-tools.mjs +1 -1
  14. package/declaration.mjs +5 -12
  15. package/doc-check/anchors.mjs +51 -59
  16. package/doc-check/index.mjs +96 -134
  17. package/doc-check/width.mjs +9 -54
  18. package/i18n.mjs +5 -0
  19. package/manifest-schema.mjs +7 -35
  20. package/manifest.mjs +4 -4
  21. package/memory/code-search.mjs +5 -14
  22. package/memory/docs.mjs +5 -14
  23. package/memory/read-gate.mjs +26 -0
  24. package/package.json +1 -1
  25. package/prompts/advisor-design.md +89 -38
  26. package/prompts/advisor-round1.md +72 -24
  27. package/prompts/advisor-round2.md +62 -25
  28. package/prompts/advisor-round3.md +68 -24
  29. package/prompts/common.md +212 -57
  30. package/prompts/consult-base.md +10 -3
  31. package/prompts/discipline-engineering.md +180 -53
  32. package/prompts/discipline-normal.md +91 -40
  33. package/prompts/persona-coder.md +3 -1
  34. package/prompts/persona-eng-coder.md +29 -9
  35. package/prompts/persona-eng-designer.md +87 -33
  36. package/prompts/persona-engineering.md +322 -89
  37. package/prompts/persona-explore.md +1 -1
  38. package/prompts/persona-normal.md +16 -8
  39. package/tool-docs/doc_check.md +27 -15
  40. package/tool-docs/manifest-read.md +8 -0
  41. package/tool-docs/manifest.md +9 -8
  42. package/tool-docs/subagent.md +2 -2
@@ -1,109 +1,91 @@
1
1
  /**
2
- * doc-check/index.mjs — 文档纪律机检 · 引擎入口(域扫描 + 声明面读取 + 报面行)。
2
+ * doc-check/index.mjs — 文档纪律机检 · 引擎入口(两条判据 + 报面行)。
3
3
  *
4
4
  * 判据两条(起步 · 闭集——口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S):
5
5
  * ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;② 行宽——口径 = §3.18 条目 Q,实现 = `./width.mjs`。
6
- * 判据参数由**被开发项目**的 `PROJECT-MANIFEST.json` `checkConfig` 声明(不硬编码任何本仓路径)。
6
+ * 判据参数 = **工具内置常量** `DOC_CHECK_BUILTINS`:不读任何项目的声明面,因而没有「缺参数 ⇒ 拒跑」这一层,
7
+ * 也没有常量校验面——改判据 = 改代码、走评审。
8
+ * 判定范围 = **调用方传入的文件清单**(判定范围 = 解析范围 = 传入的那几档;无目录遍历、无全域兜底):
9
+ * 不给清单 ⇒ 明确报错要求显式清单——判了本轮没碰过的档,别轮的存量问题会记到本轮头上。
7
10
  * 两种问法(§3.20「工具形」——同一条判据):
8
- * `report`(缺省)= 全域报告态——**零门禁 / 零退出码 / 零阻断**,输出不得被读作拒执行的理由(用户令 > 机检);
9
- * `gate` = 交付时点的**范围化裁定**——只判本轮改过的档(整档入判),首行给机器可判 verdict;
10
- * 强制面住交付协议 + 批次档 §6 前置,工具本体仍不阻断任何动作。
11
+ * `report`(缺省)= 报告态——**零门禁 / 零退出码 / 零阻断**,输出不得被读作拒执行的理由(用户令 > 机检);
12
+ * 清单里不可判档(不在盘 ∥ 非 .md)⇒ 报错点名、整次零判定;
13
+ * `gate` = 交付时点的裁定——首行给机器可判 verdict;不可判档给跳过行(可见,不静默丢掉)。
14
+ * 强制面住交付协议 + 批次档 §6 前置,工具本体不阻断任何动作。
11
15
  */
12
16
  import { statSync } from "node:fs"
13
17
  import { relative, resolve } from "node:path"
14
- import { MANIFEST_REL, readManifest } from "../manifest.mjs"
15
- import { anchorRootError } from "../manifest-schema.mjs"
16
- import { scanDomain, checkDocWidths } from "./width.mjs"
18
+ import { checkDocWidths } from "./width.mjs"
17
19
  import { checkAnchors } from "./anchors.mjs"
18
20
 
21
+ /** 判据参数(内置常量 · 冻结):阈值 300 字符;两项空列表 = 区带类 / 豁免类默认值且恒空
22
+ * ——豁免谓词(表行 / 围栏 / 可执行行 / 引文)住代码,没有「登记后放过」的写入口(空列表 ≠ 可登记豁免)。 */
23
+ export const DOC_CHECK_BUILTINS = Object.freeze({
24
+ lineWidth: 300,
25
+ widthExemptZones: Object.freeze([]),
26
+ exemptions: Object.freeze([]),
27
+ })
28
+
19
29
  /** 相对路径显示形(正斜杠——跨平台同形)。 */
20
30
  const rel = (root, p) => relative(root, p).replace(/\\/g, "/")
21
31
 
22
- /** 平台路径键(Windows 大小写不敏感——只用于域集比对,非判据面)。 */
32
+ /** 平台路径键(Windows 大小写不敏感——只用于清单去重,非判据面)。 */
23
33
  const pathKey = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
24
34
 
25
- /** 文件存在性(目录 / 读错 ⇒ false)。 */
26
- function isFile(p) { try { return statSync(p).isFile() } catch { return false } }
27
-
28
- /** gate 取参失败(manifest 不可读 ∥ checkConfig 不合法)——**不产 PASS**(消费面 fail-closed)。 */
29
- function gateParamError(base, read) {
30
- const reason = read.reason === "missing"
31
- ? `${MANIFEST_REL} 不在 ${base}`
32
- : (read.errors?.length ? read.errors.join(";") : String(read.reason ?? "未知原因"))
33
- return {
34
- report: [
35
- `DOC-CHECK GATE: ERROR(manifest 不可读 —— ${reason})`,
36
- "原因:取参失败——判据参数来自 `PROJECT-MANIFEST.json` `checkConfig`,无声明面即无判据",
37
- "处置:修声明面(`checkConfig`)后复跑",
38
- ].join("\n"),
39
- hits: { total: 0, anchors: 0, width: 0 }, anchorHits: [], widthHits: [], anchorReports: [],
40
- files: [], checkFiles: [], scanDirs: [], lineWidth: null, zones: [], exclude: [],
41
- skipped: { outside: [], gone: [] }, error: "manifest",
42
- }
43
- }
44
-
45
- /** 声明面形态判据(gate 专用——报告态行为逐字不变):取参不合法 ⇒ 判据失效却出 PASS = fail-open,故 gate 先判后跑。
46
- * 分工:**整键缺** = 上方 `read.missingKeys` 判据(`fillDefaults` 会补内置默认值);**子键形态** = 本函数
47
- * (`validateManifest` 只拒 `docRoot` 值与三族键形态,`checkConfig` 子键不拒 ⇒ 不在这里判就没人判)。 */
48
- function configError(cfg) {
49
- if (cfg === null || typeof cfg !== "object" || Array.isArray(cfg)) return "`checkConfig` 须为对象"
50
- if (!Array.isArray(cfg.scanDirs) || cfg.scanDirs.length === 0 || !cfg.scanDirs.every((d) => typeof d === "string" && d.trim() !== "")) {
51
- return "`checkConfig.scanDirs` 须为非空字符串数组(空域不得出 PASS——空域假绿比红更坏)"
52
- }
53
- if (typeof cfg.lineWidth !== "number" || !Number.isFinite(cfg.lineWidth) || cfg.lineWidth <= 0) return "`checkConfig.lineWidth` 须为正有限数值"
54
- if (cfg.widthExemptZones !== undefined && !Array.isArray(cfg.widthExemptZones)) return "`checkConfig.widthExemptZones` 须为数组"
55
- if (cfg.anchors !== undefined) {
56
- if (cfg.anchors === null || typeof cfg.anchors !== "object" || Array.isArray(cfg.anchors)) return "`checkConfig.anchors` 须为对象"
57
- if (cfg.anchors.exclude !== undefined && !Array.isArray(cfg.anchors.exclude)) return "`checkConfig.anchors.exclude` 须为数组"
58
- if (cfg.anchors.roots !== undefined) {
59
- if (!Array.isArray(cfg.anchors.roots)) return "`checkConfig.anchors.roots` 须为字符串数组(元素 = 相对项目根的目录)"
60
- for (const r of cfg.anchors.roots) {
61
- const why = anchorRootError(r)
62
- if (why) return `\`checkConfig.anchors.roots\` ${why}:${JSON.stringify(r)}`
63
- }
64
- }
65
- }
35
+ /** 不可判档的两因(闭集 · 判序 = 存在性 → 类型):不在盘 ∥ 非 markdown 档(目录归后因)。 */
36
+ function undecidableReason(abs) {
37
+ let st
38
+ try { st = statSync(abs) } catch { return "not on disk" }
39
+ if (!st.isFile() || !abs.toLowerCase().endsWith(".md")) return "not a markdown document"
66
40
  return null
67
41
  }
68
42
 
69
- /**
70
- * gate 范围求交:候选(`docs`)= 显式清单 ∥ 本会话触达记账 ⇒ **声明域内 ∧ 盘上存在的 `.md`**。
71
- * 域外 / 已删**不是命中**但必须可见(跳过行)——静默丢掉会把「没跑」装成「跑了且清」。
72
- * 解析基线不缩:域内全集仍交锚判据建索引(只收窄被判档)。
73
- */
74
- function resolveRange(base, files, docs) {
75
- const inDomain = new Map(files.map((f) => [pathKey(f), f]))
76
- const check = new Set()
77
- const outside = []
78
- const gone = []
79
- for (const d of Array.isArray(docs) ? docs : []) {
80
- if (typeof d !== "string" || d.trim() === "") continue
43
+ /** 清单 → 可判集(去重保序)+ 不可判档两类(`missing` ∥ `notMarkdown`)+ 非法条目下标(非字符串 / 空白)。 */
44
+ function resolveRange(base, docs) {
45
+ const seen = new Set()
46
+ const check = []
47
+ const undecidable = []
48
+ const missing = []
49
+ const notMarkdown = []
50
+ const invalid = []
51
+ docs.forEach((d, i) => {
52
+ if (typeof d !== "string" || d.trim() === "") { invalid.push(i); return }
81
53
  const abs = resolve(base, d)
82
- const hit = inDomain.get(pathKey(abs))
83
- if (hit) check.add(hit)
84
- else if (isFile(abs)) outside.push(abs)
85
- else gone.push(abs)
86
- }
87
- return { check: [...check].sort(), outside, gone }
54
+ const key = pathKey(abs)
55
+ const reason = undecidableReason(abs)
56
+ if (reason === null) { if (!seen.has(key)) { seen.add(key); check.push(abs) } return }
57
+ undecidable.push({ abs, reason })
58
+ if (reason === "not on disk") missing.push(abs)
59
+ else notMarkdown.push(abs)
60
+ })
61
+ return { check: check.sort(), undecidable, missing, notMarkdown, invalid }
88
62
  }
89
63
 
90
64
  /** gate 报面(固定句形——§3.20「报面(固定句形 · 交付门 gate)」):首行 verdict = 唯一机判串。 */
91
- function gateReport({ base, docsSource, checkFiles, domainCount, skipped, anchors, widths, lineWidth, exclude }) {
65
+ function gateReport({ base, docsSource, checkFiles, skipped, anchors, widths, lineWidth }) {
92
66
  const total = anchors.hits.length + widths.length
93
67
  const src = docsSource === "session" ? "本会话写过的文档" : "显式 docs"
94
68
  const out = [
95
69
  total ? `DOC-CHECK GATE: REFUSED(命中 ${total} 条)` : (checkFiles.length ? "DOC-CHECK GATE: PASS" : "DOC-CHECK GATE: PASS(0 档)"),
96
70
  "文档纪律机检(交付门 · gate)",
97
- `范围:${src} ${checkFiles.length} 档(声明域 ${domainCount} 档)· 阈值 ${lineWidth} 字符(运行时取 checkConfig.lineWidth)· 排除:${exclude.length ? exclude.join(" ∥ ") : "(无)"}`,
98
- "判据:与报告态同源(`PROJECT-MANIFEST.json` `checkConfig`)——范围只收窄被判档,不收窄解析",
71
+ checkFiles.length
72
+ ? `范围:${src} ${checkFiles.length} 档 · 阈值 ${lineWidth} 字符(工具内置常量)`
73
+ : `范围:${src} —— **0 档受检**(空清单:本次没有判任何档,不得读作「已检」)· 阈值 ${lineWidth} 字符(工具内置常量)`,
74
+ "判据:工具内置常量(锚 ∥ 行宽)——判定范围 = 解析范围 = 传入档",
99
75
  ]
100
- if (skipped.outside.length) out.push(`跳过 ${skipped.outside.length} 档(不在声明域内,未检):${skipped.outside.map((p) => rel(base, p)).join(" ∥ ")}`)
101
- if (skipped.gone.length) out.push(`跳过 ${skipped.gone.length} 档(已不在盘,未检):${skipped.gone.map((p) => rel(base, p)).join(" ∥ ")}`)
76
+ if (skipped.missing.length) out.push(`跳过 ${skipped.missing.length} 档(已不在盘,未检):${skipped.missing.map((p) => rel(base, p)).join(" ∥ ")}`)
77
+ if (skipped.notMarkdown.length) out.push(`跳过 ${skipped.notMarkdown.length} 档(非 markdown 档,未检):${skipped.notMarkdown.map((p) => rel(base, p)).join(" ∥ ")}`)
102
78
  if (checkFiles.length === 0) {
103
- // 空范围不静默当通过(K6)——照实说明 + 给出补充通道
79
+ // 空范围不静默当通过(K6)——照实说明 + 给出补充通道(两侧都不得读成「已检」)。
80
+ // 措辞按「有没有跳过行」分流:无跳过行时不得写「见上方跳过行」。
81
+ const hasSkips = skipped.missing.length > 0 || skipped.notMarkdown.length > 0
104
82
  out.push(docsSource === "session"
105
- ? "范围为空:本会话没有写过声明域内的文档——若确认改过 ⇒ 显式传 docs"
106
- : "范围为空:显式 docs 无一落在声明域内——见上方跳过行")
83
+ ? (hasSkips
84
+ ? "范围为空:本会话触达的档均不可判(见上方跳过行)——若确认改过 ⇒ 显式传 docs"
85
+ : "范围为空:本会话没有写过 .md 档——若确认改过 ⇒ 显式传 docs")
86
+ : (hasSkips
87
+ ? "范围为空:显式 docs 里没有可判档——见上方跳过行"
88
+ : "范围为空:显式 docs 是空清单——列出要判的档后复跑"))
107
89
  } else if (total === 0) {
108
90
  out.push(`结论:0 条命中(${checkFiles.length} 档全清)`)
109
91
  } else {
@@ -120,91 +102,71 @@ function gateReport({ base, docsSource, checkFiles, domainCount, skipped, anchor
120
102
 
121
103
  /**
122
104
  * 跑一次文档纪律机检。
123
- * @param {string} root 项目根(判据参数取该根的 `PROJECT-MANIFEST.json` `checkConfig`)
105
+ * @param {string} root 项目根(解析序 ① 的基准)
124
106
  * @param {{docs?:string[], mode?:'report'|'gate', docsSource?:'session'|'explicit'}} [o]
125
- * `mode` 缺省 = `report`(全域报告态);`docs` = **仅 gate** 的范围候选(相对 `root` 或绝对);
126
- * `docsSource` = 范围来源标签(`session` = 本会话触达记账 ∥ `explicit` = 显式 `docs`)——缺省按 `docs` 是否给定推定;只影响报面措辞。
107
+ * `mode` 缺省 = `report`;`docs` = 被判档清单(相对 `root` 或绝对)——**必填**(两种问法同):
108
+ * 缺省 ∥ 非数组 ⇒ 报错要求显式清单(零遍历、零全域兜底);`report` 面**空数组也报错**,`gate` 面空数组 ⇒
109
+ * `PASS(0 档)` + 空范围说明(「本轮一档没碰」是真实读数);`report` 面不可判档 ⇒ 报错、整次零判定;
110
+ * `gate` 面不可判档 ⇒ 跳过行;`docsSource` = 范围来源标签(`session` = 本会话触达记账 ∥ `explicit` = 显式
111
+ * `docs`)——缺省 = `explicit`;只影响报面措辞。
127
112
  * @returns {{report:string, hits:{total:number, anchors:number, width:number}, anchorHits:Array,
128
- * widthHits:Array, anchorReports:Array, anchorRootGaps:Array, files:string[], checkFiles:string[], scanDirs:string[],
129
- * lineWidth:number|null, zones:string[], exclude:string[], skipped:{outside:string[], gone:string[]},
130
- * error?:"manifest"}}
131
- * `error` 在场 ⇒ 取参失败形(首行 = `DOC-CHECK GATE: ERROR(…)`、`lineWidth` = null、`hits` 恒 0)——
132
- * 消费面判「清」须读 `report` 首行,不得只看 `hits.total`(0 与「未跑成」不同义)。
113
+ * widthHits:Array, files:string[], checkFiles:string[], lineWidth:number,
114
+ * skipped:{missing:string[], notMarkdown:string[]}}}
115
+ * 消费面判「清」须读 `report` 首行(gate)∥ `hits.total`(report),不得只看其中一个。
133
116
  */
134
117
  export function runDocCheck(root, { docs, mode = "report", docsSource } = {}) {
135
118
  const base = resolve(root)
136
119
  const gate = mode === "gate"
137
- // `docs` = gate 专属(report + docs ⇒ 拒,不静默忽略——与工具面同一判据)
138
- if (!gate && docs !== undefined) throw new Error("doc_check: `docs` is gate-only — report mode always checks the whole declared domain")
139
- const source = docsSource ?? (docs === undefined ? "session" : "explicit")
140
- let read
141
- try {
142
- read = readManifest(base)
143
- } catch (e) {
144
- if (!gate) throw e // 报告态既有行为逐字不变(读错 fail-closed 上抛)
145
- return gateParamError(base, { reason: "unreadable", errors: [e?.message ?? String(e)] })
120
+ if (docs !== undefined && !Array.isArray(docs)) {
121
+ throw new Error(`doc_check: \`docs\` must be an array of paths (got ${typeof docs}) — a bare string is refused rather than read as a whole-domain request; pass ["<path>"].`)
146
122
  }
147
- if (!read.ok) {
148
- if (gate) return gateParamError(base, read)
149
- throw new Error(`doc_check: cannot read ${MANIFEST_REL} at ${base} — the check parameters come from that project's checkConfig (${read.reason ?? read.errors.join(";")})`)
123
+ if (!Array.isArray(docs)) {
124
+ throw new Error('doc_check: `docs` is required — pass the explicit list of documents to judge. There is no default range: nothing is scanned unless you name it.')
150
125
  }
151
- // `checkConfig` 整键缺失:`readManifest` 会经 `fillDefaults` 灌入默认值(`missingKeys` 是唯一痕迹)——
152
- // gate 不得拿内置默认值当声明面(设计「取参失败 ⇒ ERROR」的第一支)。
153
- if (gate && (read.missingKeys ?? []).includes("checkConfig")) {
154
- return gateParamError(base, { reason: "invalid-config", errors: ["`checkConfig` 缺——声明面未声明判据参数"] })
126
+ // 报告态空清单 = 报错(「没跑」不得装成「跑了且清」);gate 面空清单照实出 `PASS(0 档)` + 空范围说明
127
+ // ——交付面「本轮一档没碰」是一种真实读数,不是省略。
128
+ if (!gate && (docs.length === 0 || docs.every((d) => typeof d !== "string" || d.trim() === ""))) {
129
+ throw new Error('doc_check: docs is empty — an empty list names no document, so nothing can be judged; list the documents you mean.')
155
130
  }
156
- const cfg = read.manifest.checkConfig ?? {}
157
- if (gate) {
158
- const bad = configError(cfg)
159
- if (bad) return gateParamError(base, { reason: "invalid-config", errors: [bad] })
131
+ const source = docsSource ?? "explicit"
132
+ const range = resolveRange(base, docs)
133
+ // 清单条目本身非法(非字符串 / 空白)——调用方错误,两种问法同拒:静默丢掉一条就是不静默地收窄范围。
134
+ if (range.invalid.length) {
135
+ throw new Error(`doc_check: \`docs\` entries ${range.invalid.map((i) => `[${i}]`).join(", ")} are not paths — every entry must be a non-empty string; no document was judged.`)
136
+ }
137
+ // 报告态 fail-closed:清单里有一个不可判档 ⇒ 报错点名、整次零判定——清单写错却报 0 命中就是「没跑」装成「跑了且清」。
138
+ if (!gate && range.undecidable.length) {
139
+ const named = range.undecidable.map((u) => `${rel(base, u.abs)}(${u.reason})`).join(" ∥ ")
140
+ throw new Error(`doc_check: docs names ${range.undecidable.length} document(s) that cannot be judged — no document was checked: ${named}. Fix the list and re-run; nothing is silently skipped and the check never widens beyond the list.`)
160
141
  }
161
- const scanDirs = (Array.isArray(cfg.scanDirs) ? cfg.scanDirs : []).filter((d) => typeof d === "string" && d !== "")
162
- const exclude = Array.isArray(cfg.anchors?.exclude) ? cfg.anchors.exclude : []
163
- const roots = cfg.anchors?.roots // 原值下传:值形 ∥ 根在盘的门归 anchors(报告态弃用、不炸)
164
- const lineWidth = cfg.lineWidth
165
- const zones = (Array.isArray(cfg.widthExemptZones) ? cfg.widthExemptZones : []).filter((z) => typeof z === "string" && z !== "")
166
-
167
- const files = scanDomain(base, { scanDirs, exclude })
168
- const range = gate ? resolveRange(base, files, docs) : { check: files, outside: [], gone: [] }
169
142
  const checkFiles = range.check
170
- const anchors = checkAnchors(base, { scanDirs, files, checkFiles, roots })
171
- const widths = checkDocWidths(base, { lineWidth, scanDirs, exclude, exemptZones: zones, files, checkFiles })
143
+ const { lineWidth, widthExemptZones } = DOC_CHECK_BUILTINS
144
+ const anchors = checkAnchors(base, { files: checkFiles })
145
+ const widths = checkDocWidths({ lineWidth, exemptZones: widthExemptZones, files: checkFiles })
172
146
 
173
147
  const hits = { total: anchors.hits.length + widths.length, anchors: anchors.hits.length, width: widths.length }
174
148
  const shared = {
175
- hits, anchorHits: anchors.hits, widthHits: widths, anchorReports: anchors.reports, anchorRootGaps: anchors.rootGaps,
176
- files, checkFiles, scanDirs, lineWidth, zones, exclude, skipped: { outside: range.outside, gone: range.gone },
149
+ hits, anchorHits: anchors.hits, widthHits: widths, files: checkFiles, checkFiles, lineWidth,
150
+ skipped: { missing: range.missing, notMarkdown: range.notMarkdown },
177
151
  }
178
152
  if (gate) {
179
- return {
180
- ...shared,
181
- report: gateReport({ base, docsSource: source, checkFiles, domainCount: files.length, skipped: shared.skipped, anchors, widths, lineWidth, exclude }),
182
- }
153
+ return { ...shared, report: gateReport({ base, docsSource: source, checkFiles, skipped: shared.skipped, anchors, widths, lineWidth }) }
183
154
  }
184
155
 
185
- const total = hits.total
186
156
  const out = [
187
157
  "文档纪律机检(报告态 · 非门禁)",
188
- `射程:${base} · ${scanDirs.join(" + ") || "(无)"}(${files.length} 档)· 阈值 ${lineWidth} 字符 · 区带豁免:${zones.length ? zones.join(" ∥ ") : "无"} · 排除:${exclude.length ? exclude.join(" ∥ ") : "(无)"}`,
189
- "声明:判据参数依据 `PROJECT-MANIFEST.json` `checkConfig`",
190
- `命中 ${total} 条(锚 ${anchors.hits.length} · 行宽 ${widths.length})`,
158
+ `射程:${base} · 传入清单 ${checkFiles.length} 档 · 阈值 ${lineWidth} 字符(工具内置常量)· 区带豁免:${widthExemptZones.length ? widthExemptZones.join(" ∥ ") : "无"}`,
159
+ "声明:判据参数 = 工具内置常量;范围 = 传入文件(判定范围 = 解析范围)",
160
+ `范围:传入清单 ${checkFiles.length} 档(整档入判 · 判定范围 = 解析范围)`,
161
+ `命中 ${hits.total} 条(锚 ${anchors.hits.length} · 行宽 ${widths.length})`,
191
162
  ]
192
- // 报告面(不作判):① 声明解析根弃用 ∥ 缺位(⑤ 的配置面);② 域内同名 ≥2 ⇒ 多义不判(残余登记——fail-open 方向,归评审核验面)
193
- for (const g of anchors.rootGaps) {
194
- out.push(g.kind === "missing"
195
- ? `报告 锚·声明解析根缺位 \`${g.root}\`(未核、不入判)`
196
- : `报告 锚·解析根弃用 ${JSON.stringify(g.root)}(值形非法:${g.reason} · 本元素不入判)`)
197
- }
198
- for (const r of anchors.reports) {
199
- out.push(`报告 锚·多义 ${rel(base, r.file)}:${r.line} \`${r.token}\`(同名 ${r.candidates.length}:${r.candidates.map((c) => rel(base, c)).join(" ∥ ")}——报告面,不作判)`)
200
- }
201
163
  for (const h of anchors.hits) {
202
164
  out.push(h.clazz === "档缺"
203
165
  ? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
204
166
  : `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
205
167
  }
206
168
  for (const w of widths) out.push(`✗ 行宽 ${rel(base, w.file)}:${w.line}(${w.len} 字符)`)
207
- out.push(total ? `结论:${total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 行宽 均清)")
169
+ out.push(hits.total ? `结论:${hits.total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 行宽 均清)")
208
170
 
209
171
  return { ...shared, report: out.join("\n") }
210
172
  }
@@ -1,60 +1,16 @@
1
1
  /**
2
- * doc-check/width.mjs — 文档纪律机检的产品档位(行宽谓词族 + 域扫描 + 行宽判据——实现单源)。
2
+ * doc-check/width.mjs — 文档纪律机检的产品档位(行宽谓词族 + 行宽判据——实现单源)。
3
3
  *
4
- * 口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.18 条目 Q(度量 / 域 / 豁免类 / 阈值);
4
+ * 口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.18 条目 Q(度量 / 豁免类);
5
5
  * 产品内建裁定 = 同档 §3.20 条目 S——谓词单源随迁:本仓 `scripts/doc-check-width.mjs` 改述为
6
6
  * **再出口缝**(薄档 · 零逻辑 · 逐名再出口),盘上不留第二份谓词实现。
7
- * 度量 = `[...line].length`(CJK 计 1——不引显示宽度 / 字体);阈值 / 域 / 排除全部读
8
- * `PROJECT-MANIFEST.json` 的 `checkConfig`——零硬编码本仓路径,换项目开箱可用。
7
+ * 度量 = `[...line].length`(CJK 计 1——不引显示宽度 / 字体);阈值 = 调用方的内置常量
8
+ * (`DOC_CHECK_BUILTINS.lineWidth`)——不读声明面,也不扫目录:**判定集 = 调用方传入的文件清单**。
9
9
  * 豁免五类 = 表格行 ∥ fenced 块 ∥ 可执行行 ∥ 连续引文 ∥ 区带:**只认结构谓词**,
10
10
  * 无行级白名单(按行号 / 注释声明的豁免不入判)。
11
11
  * 边界:本档零门禁 / 零退出码 / 零阻断——报告态由 `agent-tools/doc-check.mjs` 呈现。
12
12
  */
13
- import { readdirSync, readFileSync, statSync } from "node:fs"
14
- import { join, resolve } from "node:path"
15
- import { MANIFEST_REL } from "../manifest.mjs"
16
-
17
- /** 域发现跳过目录(不视作候选产品域;递归亦不进入)。 */
18
- const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".turbo", ".thincoder"])
19
-
20
- /** 域发现(声明面):cwd 根(持 manifest)+ 各持自身 manifest 的一级子目录——判据 = manifest 在场,无路径硬编码。 */
21
- export function discoverDomains(root) {
22
- const rootAbs = resolve(root)
23
- const hasManifest = (p) => { try { return statSync(join(p, MANIFEST_REL)).isFile() } catch { return false } }
24
- const out = []
25
- if (hasManifest(rootAbs)) out.push(rootAbs)
26
- let names = []
27
- try { names = readdirSync(rootAbs) } catch { /* 不可读:仅根域 */ }
28
- for (const n of names) {
29
- if (SKIP_DIRS.has(n) || n.startsWith("dist-")) continue
30
- const p = join(rootAbs, n)
31
- try { if (statSync(p).isDirectory() && hasManifest(p)) out.push(p) } catch { /* 跳过 */ }
32
- }
33
- return out
34
- }
35
-
36
- /** 递归列 .md(跳过 SKIP_DIRS ∧ 声明面 exclude 段——exclude 逐层生效)。 */
37
- function walkMd(dir, exclude, out) {
38
- let names = []
39
- try { names = readdirSync(dir) } catch { return out }
40
- for (const n of names) {
41
- if (SKIP_DIRS.has(n) || n.startsWith("dist-") || exclude.includes(n)) continue
42
- const p = join(dir, n)
43
- let st
44
- try { st = statSync(p) } catch { continue }
45
- if (st.isDirectory()) walkMd(p, exclude, out)
46
- else if (n.endsWith(".md")) out.push(p)
47
- }
48
- return out
49
- }
50
-
51
- /** 源域(单源):`scanDirs` × `exclude` 声明面下的全部 .md——锚检查与行宽检查同一文件集。 */
52
- export function scanDomain(base, { scanDirs = [], exclude = [] } = {}) {
53
- const ex = Array.isArray(exclude) ? exclude : []
54
- const out = []
55
- for (const d of Array.isArray(scanDirs) ? scanDirs : []) walkMd(resolve(base, d), ex, out)
56
- return [...new Set(out)].sort()
57
- }
13
+ import { readFileSync } from "node:fs"
58
14
 
59
15
  /** 表格行谓词(markdown 表格行结构性不可折行——行宽豁免,与锚检查同源)。 */
60
16
  export const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(String(l))
@@ -94,16 +50,15 @@ export function inCodeSpan(line, idx) {
94
50
  return (String(line).slice(0, idx).match(/`/g) ?? []).length % 2 === 1
95
51
  }
96
52
 
97
- /** 行宽判据(单判据 = `checkConfig.lineWidth`):`[...line].length` > 阈值 ∧ 非表格行 ∧ 非 fenced 块内
53
+ /** 行宽判据(阈值 = 调用方内置常量 `lineWidth`):`[...line].length` > 阈值 ∧ 非表格行 ∧ 非 fenced 块内
98
54
  * ∧ 非可执行行 ∧ 非连续引文 ∧ 非区带内 ⇔ 命中(口径逐字 = §3.18 条目 Q)。
99
55
  * 单遍扫描携标题栈(`^(#{1,6})\s` 行入栈——同级 ∥ 更高级出栈):行所属标题链任一为区带头 ⇒ 豁免。
100
56
  * 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。
101
- * `checkFiles`(可选)= **被判集**(gate 的范围收窄面);缺省 ⇒ `files`(全域,报告态遍历面零变)。 */
102
- export function checkDocWidths(base, { lineWidth, scanDirs, exclude, exemptZones = [], files, checkFiles } = {}) {
57
+ * `files`(必填)= **判定集**(调用方传入的档清单——本档不遍历任何目录;空集 ⇒ 零命中)。 */
58
+ export function checkDocWidths({ lineWidth, exemptZones = [], files = [] } = {}) {
103
59
  const zones = (Array.isArray(exemptZones) ? exemptZones : []).filter((z) => typeof z === "string" && z !== "")
104
60
  const hits = []
105
- // 被判集优先(范围只收窄被判档);`files` 给定时直接用(调用方已扫同一域 ⇒ 域只走一遍);缺省自扫。
106
- for (const f of checkFiles ?? files ?? scanDomain(base, { scanDirs, exclude })) {
61
+ for (const f of files) {
107
62
  let text = ""
108
63
  try { text = readFileSync(f, "utf8") } catch { continue }
109
64
  const stack = [] // 标题栈 —— { level, exempt };exempt = 自身 ∥ 祖先命中区带
package/i18n.mjs CHANGED
@@ -60,6 +60,11 @@ export const CORE_MESSAGES = Object.freeze({
60
60
  "sub.cancelled": { en: "cancelled", zh: "已取消" },
61
61
  "sub.awaitingDigest": { en: "done · awaiting digestion", zh: "已完成 · 等待消化" },
62
62
  "sub.queueSlot": { en: "queued · position ${n} (slot full)", zh: "排队中 · 位置 ${n}(槽满等位)" },
63
+ // #1507 补四键(核字典缺键——值形与宿主同形:desktop `renderer/i18n.mjs:197-200` ∥ VSC `locales/{en,zh}.json`)
64
+ "sub.async": { en: "async", zh: "异步" },
65
+ "sub.sync": { en: "sync", zh: "同步" },
66
+ "sub.waiting": { en: "waiting", zh: "等待中" },
67
+ "sub.awaitingApproval": { en: "Awaiting approval: ${tool}", zh: "等待审批: ${tool}" },
63
68
  // ── 会诊状态词 ──
64
69
  "consult.answered": { en: "answered", zh: "已回复" },
65
70
  "consult.terminated": { en: "terminated", zh: "已终止" },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * manifest-schema.mjs — **校验 ∕ 默认值族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
3
  * 全量迁出——`isValidDocRootValue`(docRoot 子键值形态判据)·
4
- * `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词一件 · `validateManifest` ·
4
+ * `DEFAULT_MANIFEST`(默认五键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词一件 · `validateManifest` ·
5
5
  * `fillDefaults`;**结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 =
6
6
  * 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
7
7
  *
@@ -25,21 +25,8 @@ export function isValidDocRootValue(value) {
25
25
  return value.every((p) => typeof p === "string" && p.trim() !== "")
26
26
  }
27
27
 
28
- /** `checkConfig.anchors.roots` **单元素值形判据**(判据单源——解析根 = 相对项目根的目录):
29
- * 合法 ⇒ `null`;非法 ⇒ 理由句(非空字符串 ∥ 相对路径 ∥ 无 `..` 越界段)。
30
- * 消费面两处:产品引擎 `doc-check/anchors.mjs`(报告态逐元素弃用)∥ `doc-check/index.mjs`
31
- * (gate 面 `configError` 拒,fail-closed)。
32
- * @param {unknown} value 解析根元素
33
- * @returns {string|null}
34
- */
35
- export function anchorRootError(value) {
36
- if (typeof value !== "string" || value.trim() === "") return "元素须为非空字符串"
37
- if (/^(?:[A-Za-z]:[\\/]|[\\/])/.test(value)) return "元素须为相对项目根的路径(不得为绝对路径)"
38
- if (value.split(/[\\/]/).includes("..")) return "元素不得含 `..` 越界段"
39
- return null
40
- }
41
-
42
- /** 默认八键(架构 §2.3 E1 五键 + 三族声明键——KD-M1-31)——整档初始化的写源与缺键 fallback 的补源。 */
28
+ /** 默认五键——整档初始化的写源与缺键 fallback 的补源;每一键描述的都是这个用户项目自身
29
+ * (阶段 / 文档层 / 代码段 / 索引),产品内部布局与工具参数不住这里。 */
43
30
  export const DEFAULT_MANIFEST = Object.freeze({
44
31
  version: 1,
45
32
  phase: "initial-dev",
@@ -50,18 +37,9 @@ export const DEFAULT_MANIFEST = Object.freeze({
50
37
  modules: "docs/design/modules",
51
38
  batches: "docs/batches",
52
39
  }),
53
- promptsLanding: "thincoder-core/prompts",
54
- checkConfig: Object.freeze({
55
- scanDirs: Object.freeze(["docs"]),
56
- lineWidth: 300,
57
- widthExemptZones: Object.freeze([]),
58
- anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]), roots: Object.freeze([]) }),
59
- exemptions: Object.freeze([]),
60
- }),
61
40
  // 项目声明三族(2026-09-27 声明载体唯一化批并入——键名 / 形态逐字承前;来源档已退役)。
62
41
  codePaths: Object.freeze(["src"]),
63
42
  index: Object.freeze({ codeExtensions: Object.freeze([]), docExtensions: Object.freeze([]), excludePaths: Object.freeze([]), publicRepos: Object.freeze([]) }),
64
- advisor: Object.freeze({ docMap: "", standardsDoc: "" }),
65
43
  })
66
44
 
67
45
  /** 校验判据(模块设计 §2.2)——枚举 / 键存在(fallback 用);判据单源(KD-M1-4)。 */
@@ -73,9 +51,7 @@ export const MANIFEST_SCHEMA = Object.freeze({
73
51
  keys: Object.freeze(Object.keys(DEFAULT_MANIFEST)),
74
52
  nestedKeys: Object.freeze({
75
53
  docRoot: Object.freeze(Object.keys(DEFAULT_MANIFEST.docRoot)),
76
- checkConfig: Object.freeze(Object.keys(DEFAULT_MANIFEST.checkConfig)),
77
54
  index: Object.freeze(Object.keys(DEFAULT_MANIFEST.index)),
78
- advisor: Object.freeze(Object.keys(DEFAULT_MANIFEST.advisor)),
79
55
  }),
80
56
  })
81
57
 
@@ -89,7 +65,7 @@ function isNonEmptyStringArray(v) {
89
65
  * 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
90
66
  * 指针分支已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
91
67
  * 缺键 = 便利 fallback(补默认值),不是错(与整档缺失两分——KD-M1-2)。
92
- * 未知键(#802 / KD-M1-36):顶层 + 四嵌套层产 `unknownKeys`(非拒——ok 定性零变;产线恰 =
68
+ * 未知键(#802 / KD-M1-36):顶层 + 两嵌套层产 `unknownKeys`(非拒——ok 定性零变;产线恰 =
93
69
  * fillDefaults 丢弃面;嵌套键用点形路径——同 missingKeys 口径)。
94
70
  * @param {object} obj 待校验 manifest 对象
95
71
  * @returns {{ok:boolean, errors:string[], missingKeys:string[], unknownKeys:string[]}}
@@ -100,7 +76,7 @@ export function validateManifest(obj) {
100
76
  if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
101
77
  return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys, unknownKeys: [] }
102
78
  }
103
- // #802(KD-M1-36——未知键可见化):顶层 + 四嵌套层逐键对照(非拒——ok 定性零变;嵌套键点形)。
79
+ // #802(KD-M1-36——未知键可见化):顶层 + 两嵌套层逐键对照(非拒——ok 定性零变;嵌套键点形)。
104
80
  const unknownKeys = []
105
81
  for (const key of Object.keys(obj)) if (!MANIFEST_SCHEMA.keys.includes(key)) unknownKeys.push(key)
106
82
  for (const [nested, subkeys] of Object.entries(MANIFEST_SCHEMA.nestedKeys)) {
@@ -129,7 +105,6 @@ export function validateManifest(obj) {
129
105
  // `index.excludePaths`(§6.14 面①):**数组层** fail-closed(非数组 ⇒ 拒);元素层宽容——空 ∕ 非串
130
106
  // 元素由归一剔除(L-①-1 夹具 `["./openclaw/","refs",""]` ⇒ `["openclaw","refs"]`);`index.publicRepos`(§6.15)同款。
131
107
  const strArrErr = (key, v) => { if (!isNonEmptyStringArray(v)) errors.push(`${key} 值形态非法:${JSON.stringify(v)}(须为非空字符串数组——KD-M1-32)`) }
132
- const strErr = (key, v) => { if (typeof v !== "string") errors.push(`${key} 值形态非法:${JSON.stringify(v)}(须为字符串——KD-M1-32)`) }
133
108
  const objErr = (key, v) => (v === null || typeof v !== "object" || Array.isArray(v)
134
109
  ? `${key} 值形态非法:${JSON.stringify(v)}(须为对象——KD-M1-32)` : null)
135
110
  if (obj.codePaths !== undefined) strArrErr("codePaths", obj.codePaths)
@@ -144,9 +119,6 @@ export function validateManifest(obj) {
144
119
  errors.push(`index.publicRepos 值形态非法:${JSON.stringify(obj.index.publicRepos)}(须为数组——非数组 fail-closed;空 ∕ 非串元素由归一剔除)`)
145
120
  }
146
121
  }
147
- const advBad = obj.advisor === undefined ? null : objErr("advisor", obj.advisor)
148
- if (advBad) errors.push(advBad)
149
- else if (obj.advisor !== undefined) for (const k of ["docMap", "standardsDoc"]) if (obj.advisor[k] !== undefined) strErr(`advisor.${k}`, obj.advisor[k])
150
122
  if (obj.phase !== undefined && !MANIFEST_SCHEMA.enum.phase.includes(obj.phase)) {
151
123
  errors.push(`phase 取值非法:"${obj.phase}"(允许:${MANIFEST_SCHEMA.enum.phase.join(" | ")})`)
152
124
  }
@@ -156,7 +128,7 @@ export function validateManifest(obj) {
156
128
  return { ok: errors.length === 0, errors, missingKeys, unknownKeys }
157
129
  }
158
130
 
159
- /** 缺键补默认值(module 设计 §2.2 管线):顶层缺键补默认、嵌套键(docRoot/checkConfig/index/advisor)
131
+ /** 缺键补默认值(module 设计 §2.2 管线):顶层缺键补默认、嵌套键(docRoot/index)
160
132
  * 子键补默认(与整键缺同语义);不覆写既有值。深拷贝默认源(structuredClone)——冻结常量永不外泄引用。
161
133
  * 三族键的非对象值**原样透传**(不洗白——再校验拒,KD-M1-32)。 */
162
134
  export function fillDefaults(obj) {
@@ -167,7 +139,7 @@ export function fillDefaults(obj) {
167
139
  if (def !== null && typeof def === "object" && !Array.isArray(def)) {
168
140
  const src = obj[key]
169
141
  if (src === null || typeof src !== "object" || Array.isArray(src)) {
170
- if (key === "index" || key === "advisor") out[key] = src // 形态错 → 透传(再校验拒);docRoot/checkConfig 保持默认
142
+ if (key === "index") out[key] = src // 形态错 → 透传(再校验拒);docRoot 保持默认
171
143
  continue
172
144
  }
173
145
  for (const sub of Object.keys(def)) {
package/manifest.mjs CHANGED
@@ -11,8 +11,8 @@
11
11
  * 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
12
12
  * - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
13
13
  * docRoot 子键值形态(串 | 数组 | null(本面无——KD-M1-37),F7)——**纯函数、零 fs**;不落盘。
14
- * - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
15
- * KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
14
+ * - 两族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}`——KD-M1-31 / M1-32):
15
+ * 缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
16
16
  * 读向 = `declaration.mjs`(2026-10-01 自 `conventions.mjs` 迁出——经其转口可达)经 `readManifest` /
17
17
  * `manifestFilePath` 投影(单向——KD-M1-33);`index.publicRepos` 绝对根集 = `declaredPublicRoots`(§6.15)。
18
18
  * - isValidDocRootValue(value) / docRootPaths(value, cwd)(F7 判据单源 KD-M1-8):值形态谓词
@@ -35,7 +35,7 @@
35
35
  * 落盘);仅 writer === 'main' 落盘(fail-closed 缺省拒)。
36
36
  *
37
37
  * N1 零依赖:仅 node:fs / node:path + 标准库 JSON.parse——无第三方解析器、无 node:sqlite。
38
- * N3 可迁移:本模块不硬编码任何本仓路径(docRoot / checkConfig 由被开发项目声明)。
38
+ * N3 可迁移:本模块不硬编码任何本仓路径(docRoot 由被开发项目声明;机检参数不在 manifest 面——住引擎内置常量)。
39
39
  */
40
40
  import { readFileSync, writeFileSync } from "node:fs"
41
41
  import { join, resolve } from "node:path"
@@ -277,7 +277,7 @@ export function writeManifest(cwd, manifest, { writer = "subagent" } = {}) {
277
277
  }
278
278
 
279
279
  /**
280
- * 初始化 = 写默认八键档(模块设计 §2.2):只提供机制,不包交互问答(问答归壳面)。
280
+ * 初始化 = 写默认五键档(模块设计 §2.2):只提供机制,不包交互问答(问答归壳面)。
281
281
  * 落盘走同一写门(内部 writeManifest(cwd, DEFAULT_MANIFEST, { writer }))——与 AC-M1-5
282
282
  * 同一闸,无第二条写路径(KD-M1-4 / 评审 #12)。
283
283
  * @returns {object} 写入的默认 manifest
@@ -8,7 +8,8 @@
8
8
  import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
9
9
  import { scanVectors, createTopK } from "./scan.mjs"
10
10
  import { normalizeOrigin } from "./origin.mjs"
11
- import { declaredPublicRoots, loadProjectDeclaration, isExcludedRelPath, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs"
11
+ import { declaredPublicRoots } from "../conventions.mjs"
12
+ import { createReadGate } from "./read-gate.mjs" // 面⑥∪面⑦ 读面准入单源(#1372 抽件——两档原各持一份逐字相同的 rowHidden)
12
13
  import { buildFtsQuery, ensureEmbeddings, EMBED_TEXT_MAX_LEN } from "./core.mjs"
13
14
  import { safeSliceUTF16 } from "../text-budget.mjs"
14
15
  import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/code_search.md
@@ -35,19 +36,9 @@ export async function codeSearch(memory, query, { limit = 5 } = {}) {
35
36
  const origins = searchOrigins(memory)
36
37
  const ftsOriginFilter = origins.length === 1 ? `AND c.origin = ?` : origins.length > 1 ? `AND c.origin IN (${origins.map(() => "?").join(", ")})` : ""
37
38
  const originParams = origins
38
- // §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前;两判据并列互不吞):排除谓词(面①∪面⑥,
39
- // 行 origin 为 `base`)∪ 声明范围门(面⑦——`isInDeclaredScope(path, declaredIndexScope(origin), origin)`,
40
- // 逐 origin 预载一次)⇒「批次档 ∥ 范围外路径从未入索引」等价检索;存量行不删(收尾保护位);窗口欠额接受。
41
- const declCache = new Map()
42
- const scopeCache = new Map()
43
- const rowHidden = (origin, path) => {
44
- const d = declCache.get(origin) ?? loadProjectDeclaration(origin)
45
- declCache.set(origin, d)
46
- if (isExcludedRelPath(path, d, origin)) return true
47
- let scope = scopeCache.get(origin)
48
- if (scope === undefined) { scope = declaredIndexScope(origin); scopeCache.set(origin, scope) }
49
- return !isInDeclaredScope(path, scope, origin)
50
- }
39
+ // §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前)——判据与逐 origin 预载单源 = `./read-gate.mjs`;
40
+ // 存量行不删(收尾保护位);窗口欠额接受。
41
+ const { rowHidden } = createReadGate()
51
42
  const ftsNeedOrigin = origins.length !== 1 // 单 origin 形 SQL 零改;多 origin ∥ 无过滤形补 `origin` 列(行 origin 随行可得)
52
43
 
53
44
  const ftsList = ftsQuery ? memory.db.prepare(`
package/memory/docs.mjs CHANGED
@@ -12,7 +12,8 @@ import { _upsertDocFile, yieldTick } from "./code-index.mjs"
12
12
  import { markIndexedCommit, listProjectFiles, indexExtensions, declaredRootEntries } from "./code-sync.mjs"
13
13
  import { searchOrigins } from "./code-search.mjs"
14
14
  import { createRowBudget, rowCountOfPath, sweepStaleRows } from "./sync-tail.mjs"
15
- import { loadProjectDeclaration, isExcludedRelPath, declaredIndexScope, isInDeclaredScope } from "../conventions.mjs" // 面① 谓词经 `decl` 传 `sweepStaleRows`;面⑥∪面⑦ 读面准入本档直调
15
+ import { loadProjectDeclaration, declaredIndexScope } from "../conventions.mjs" // 面① 谓词经 `decl` 传 `sweepStaleRows`
16
+ import { createReadGate } from "./read-gate.mjs" // 面⑥∪面⑦ 读面准入单源(#1372 抽件——两档原各持一份逐字相同的 rowHidden)
16
17
  import { logEvent } from "../log.mjs"
17
18
  import { safeSliceUTF16 } from "../text-budget.mjs"
18
19
  import { DESC } from "../tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/*.md(DESC 单解析面,缺档抛错语义不变)
@@ -117,19 +118,9 @@ export async function docSearch(memory, query, { limit = 5, origins: originsOver
117
118
  const origins = originsOverride ?? searchOrigins(memory)
118
119
  const ftsOriginFilter = origins.length === 1 ? `AND d.origin = ?` : origins.length > 1 ? `AND d.origin IN (${origins.map(() => "?").join(", ")})` : ""
119
120
  const originParams = origins
120
- // §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前;两判据并列互不吞):排除谓词(面①∪面⑥,
121
- // 行 origin 为 `base`)∪ 声明范围门(面⑦——`isInDeclaredScope(path, declaredIndexScope(origin), origin)`,
122
- // 逐 origin 预载一次)⇒「批次档 ∥ 范围外路径从未入索引」等价检索;存量行不删(收尾保护位);窗口欠额接受。
123
- const declCache = new Map()
124
- const scopeCache = new Map()
125
- const rowHidden = (origin, path) => {
126
- const d = declCache.get(origin) ?? loadProjectDeclaration(origin)
127
- declCache.set(origin, d)
128
- if (isExcludedRelPath(path, d, origin)) return true
129
- let scope = scopeCache.get(origin)
130
- if (scope === undefined) { scope = declaredIndexScope(origin); scopeCache.set(origin, scope) }
131
- return !isInDeclaredScope(path, scope, origin)
132
- }
121
+ // §6.14 读面准入过滤(FTS 列表建成后 ∥ 向量 top-K push 前)——判据与逐 origin 预载单源 = `./read-gate.mjs`;
122
+ // 存量行不删(收尾保护位);窗口欠额接受。
123
+ const { rowHidden } = createReadGate()
133
124
  const ftsNeedOrigin = origins.length !== 1 // 单 origin 形 SQL 零改;多 origin ∥ 无过滤形补 `origin` 列(行 origin 随行可得)
134
125
 
135
126
  const ftsList = ftsQuery ? memory.db.prepare(`