@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.
- package/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/family-tools.mjs +9 -7
- package/agent/helpers.mjs +6 -0
- package/agent/run-start.mjs +1 -0
- package/agent/turn-loop.mjs +17 -13
- package/agent-tools/batch-gate.mjs +4 -5
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/doc-check.mjs +18 -13
- package/agent-tools/manifest.mjs +44 -12
- package/agent-tools/subagent-spawn.mjs +34 -19
- package/agent-tools/subagent.mjs +1 -1
- package/agent-tools.mjs +1 -1
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +51 -59
- package/doc-check/index.mjs +96 -134
- package/doc-check/width.mjs +9 -54
- package/i18n.mjs +5 -0
- package/manifest-schema.mjs +7 -35
- package/manifest.mjs +4 -4
- package/memory/code-search.mjs +5 -14
- package/memory/docs.mjs +5 -14
- package/memory/read-gate.mjs +26 -0
- package/package.json +1 -1
- package/prompts/advisor-design.md +89 -38
- package/prompts/advisor-round1.md +72 -24
- package/prompts/advisor-round2.md +62 -25
- package/prompts/advisor-round3.md +68 -24
- package/prompts/common.md +212 -57
- package/prompts/consult-base.md +10 -3
- package/prompts/discipline-engineering.md +180 -53
- package/prompts/discipline-normal.md +91 -40
- package/prompts/persona-coder.md +3 -1
- package/prompts/persona-eng-coder.md +29 -9
- package/prompts/persona-eng-designer.md +87 -33
- package/prompts/persona-engineering.md +322 -89
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +16 -8
- package/tool-docs/doc_check.md +27 -15
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/subagent.md +2 -2
package/doc-check/index.mjs
CHANGED
|
@@ -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
|
-
*
|
|
6
|
+
* 判据参数 = **工具内置常量** `DOC_CHECK_BUILTINS`:不读任何项目的声明面,因而没有「缺参数 ⇒ 拒跑」这一层,
|
|
7
|
+
* 也没有常量校验面——改判据 = 改代码、走评审。
|
|
8
|
+
* 判定范围 = **调用方传入的文件清单**(判定范围 = 解析范围 = 传入的那几档;无目录遍历、无全域兜底):
|
|
9
|
+
* 不给清单 ⇒ 明确报错要求显式清单——判了本轮没碰过的档,别轮的存量问题会记到本轮头上。
|
|
7
10
|
* 两种问法(§3.20「工具形」——同一条判据):
|
|
8
|
-
* `report`(缺省)=
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
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
|
-
|
|
98
|
-
|
|
71
|
+
checkFiles.length
|
|
72
|
+
? `范围:${src} ${checkFiles.length} 档 · 阈值 ${lineWidth} 字符(工具内置常量)`
|
|
73
|
+
: `范围:${src} —— **0 档受检**(空清单:本次没有判任何档,不得读作「已检」)· 阈值 ${lineWidth} 字符(工具内置常量)`,
|
|
74
|
+
"判据:工具内置常量(锚 ∥ 行宽)——判定范围 = 解析范围 = 传入档",
|
|
99
75
|
]
|
|
100
|
-
if (skipped.
|
|
101
|
-
if (skipped.
|
|
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
|
-
?
|
|
106
|
-
|
|
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
|
|
105
|
+
* @param {string} root 项目根(解析序 ① 的基准)
|
|
124
106
|
* @param {{docs?:string[], mode?:'report'|'gate', docsSource?:'session'|'explicit'}} [o]
|
|
125
|
-
* `mode` 缺省 = `report
|
|
126
|
-
*
|
|
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,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
-
|
|
138
|
-
|
|
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 (!
|
|
148
|
-
|
|
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
|
-
//
|
|
152
|
-
//
|
|
153
|
-
if (gate && (
|
|
154
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
171
|
-
const
|
|
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,
|
|
176
|
-
|
|
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} · ${
|
|
189
|
-
"
|
|
190
|
-
|
|
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
|
}
|
package/doc-check/width.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
53
|
+
/** 行宽判据(阈值 = 调用方内置常量 `lineWidth`):`[...line].length` > 阈值 ∧ 非表格行 ∧ 非 fenced 块内
|
|
98
54
|
* ∧ 非可执行行 ∧ 非连续引文 ∧ 非区带内 ⇔ 命中(口径逐字 = §3.18 条目 Q)。
|
|
99
55
|
* 单遍扫描携标题栈(`^(#{1,6})\s` 行入栈——同级 ∥ 更高级出栈):行所属标题链任一为区带头 ⇒ 豁免。
|
|
100
56
|
* 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。
|
|
101
|
-
* `
|
|
102
|
-
export function checkDocWidths(
|
|
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
|
-
|
|
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: "已终止" },
|
package/manifest-schema.mjs
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
29
|
-
*
|
|
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):顶层 +
|
|
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——未知键可见化):顶层 +
|
|
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/
|
|
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"
|
|
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
|
-
* -
|
|
15
|
-
*
|
|
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
|
|
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
|
-
* 初始化 =
|
|
280
|
+
* 初始化 = 写默认五键档(模块设计 §2.2):只提供机制,不包交互问答(问答归壳面)。
|
|
281
281
|
* 落盘走同一写门(内部 writeManifest(cwd, DEFAULT_MANIFEST, { writer }))——与 AC-M1-5
|
|
282
282
|
* 同一闸,无第二条写路径(KD-M1-4 / 评审 #12)。
|
|
283
283
|
* @returns {object} 写入的默认 manifest
|
package/memory/code-search.mjs
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
40
|
-
|
|
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,
|
|
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
|
-
//
|
|
122
|
-
|
|
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(`
|