@thincoder/core 0.10.8 → 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/CHANGELOG.md +6 -0
- package/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/family-tools.mjs +11 -9
- package/agent/helpers.mjs +12 -6
- package/agent/post-turn.mjs +34 -0
- package/agent/run-stages.mjs +43 -9
- package/agent/run-start.mjs +1 -0
- package/agent/setup.mjs +10 -2
- package/agent/turn-loop.mjs +23 -13
- package/agent-tools/audit-block.mjs +4 -2
- package/agent-tools/batch-gate.mjs +197 -0
- package/agent-tools/batch-lifecycle.mjs +216 -36
- package/agent-tools/batch-skeleton.mjs +87 -0
- package/agent-tools/batch.mjs +11 -3
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/context.mjs +28 -5
- package/agent-tools/design-slots.mjs +7 -33
- package/agent-tools/doc-check.mjs +50 -7
- package/agent-tools/manifest.mjs +44 -12
- package/agent-tools/spawn-gates.mjs +100 -41
- package/agent-tools/ssh-session.mjs +108 -61
- package/agent-tools/ssh-transport.mjs +17 -4
- package/agent-tools/ssh-vault.mjs +291 -0
- package/agent-tools/subagent-spawn.mjs +52 -30
- package/agent-tools/subagent.mjs +14 -8
- package/agent-tools/timer.mjs +2 -2
- package/agent-tools.mjs +1 -1
- package/config-defaults.mjs +92 -0
- package/config-normalize.mjs +109 -0
- package/config.mjs +88 -197
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +68 -34
- package/doc-check/index.mjs +143 -32
- package/doc-check/version.mjs +84 -0
- package/doc-check/width.mjs +10 -54
- package/i18n.mjs +7 -0
- package/ledger-cmd.mjs +314 -61
- package/ledger-db.mjs +22 -10
- package/ledger-migrate.mjs +15 -9
- package/ledger-tools.mjs +115 -69
- package/ledger.mjs +13 -10
- package/log.mjs +2 -2
- package/manifest-schema.mjs +7 -21
- 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/permission.mjs +7 -1
- package/prompts/advisor-design.md +95 -44
- package/prompts/advisor-round1.md +75 -27
- package/prompts/advisor-round2.md +68 -31
- package/prompts/advisor-round3.md +77 -33
- package/prompts/common.md +283 -113
- package/prompts/consult-base.md +16 -9
- package/prompts/discipline-engineering.md +213 -83
- package/prompts/discipline-normal.md +137 -85
- package/prompts/persona-coder.md +8 -6
- package/prompts/persona-eng-coder.md +42 -21
- package/prompts/persona-eng-designer.md +101 -43
- package/prompts/persona-engineering.md +376 -127
- package/prompts/persona-explore.md +3 -3
- package/prompts/persona-normal.md +29 -21
- package/prompts/persona-plan.md +7 -7
- package/prompts/subagent-base.md +4 -4
- package/provider/anthropic.mjs +4 -2
- package/provider/core.mjs +4 -2
- package/provider/google.mjs +5 -1
- package/provider/responses-request.mjs +3 -2
- package/session-lifecycle.mjs +6 -6
- package/team.mjs +2 -1
- package/token-window.mjs +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/doc_check.md +47 -12
- package/tool-docs/fetch.md +3 -2
- package/tool-docs/ledger.md +7 -5
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/ssh.md +23 -6
- package/tool-docs/subagent.md +7 -12
- package/tool-docs/websearch.md +3 -1
- package/tools/ssh.mjs +24 -11
- package/tools/web-deepseek.mjs +172 -0
- package/tools/web.mjs +29 -4
package/doc-check/index.mjs
CHANGED
|
@@ -1,61 +1,172 @@
|
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 判据参数 = **工具内置常量** `DOC_CHECK_BUILTINS`:不读任何项目的声明面,因而没有「缺参数 ⇒ 拒跑」这一层,
|
|
7
|
+
* 也没有常量校验面——改判据 = 改代码、走评审。
|
|
8
|
+
* 判定范围 = **调用方传入的文件清单**(判定范围 = 解析范围 = 传入的那几档;无目录遍历、无全域兜底):
|
|
9
|
+
* 不给清单 ⇒ 明确报错要求显式清单——判了本轮没碰过的档,别轮的存量问题会记到本轮头上。
|
|
10
|
+
* 两种问法(§3.20「工具形」——同一条判据):
|
|
11
|
+
* `report`(缺省)= 报告态——**零门禁 / 零退出码 / 零阻断**,输出不得被读作拒执行的理由(用户令 > 机检);
|
|
12
|
+
* 清单里不可判档(不在盘 ∥ 非 .md)⇒ 报错点名、整次零判定;
|
|
13
|
+
* `gate` = 交付时点的裁定——首行给机器可判 verdict;不可判档给跳过行(可见,不静默丢掉)。
|
|
14
|
+
* 强制面住交付协议 + 批次档 §6 前置,工具本体不阻断任何动作。
|
|
9
15
|
*/
|
|
16
|
+
import { statSync } from "node:fs"
|
|
10
17
|
import { relative, resolve } from "node:path"
|
|
11
|
-
import {
|
|
12
|
-
import { scanDomain, checkDocWidths } from "./width.mjs"
|
|
18
|
+
import { checkDocWidths } from "./width.mjs"
|
|
13
19
|
import { checkAnchors } from "./anchors.mjs"
|
|
14
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
|
+
|
|
15
29
|
/** 相对路径显示形(正斜杠——跨平台同形)。 */
|
|
16
30
|
const rel = (root, p) => relative(root, p).replace(/\\/g, "/")
|
|
17
31
|
|
|
32
|
+
/** 平台路径键(Windows 大小写不敏感——只用于清单去重,非判据面)。 */
|
|
33
|
+
const pathKey = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
34
|
+
|
|
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"
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
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 }
|
|
53
|
+
const abs = resolve(base, d)
|
|
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 }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** gate 报面(固定句形——§3.20「报面(固定句形 · 交付门 gate)」):首行 verdict = 唯一机判串。 */
|
|
65
|
+
function gateReport({ base, docsSource, checkFiles, skipped, anchors, widths, lineWidth }) {
|
|
66
|
+
const total = anchors.hits.length + widths.length
|
|
67
|
+
const src = docsSource === "session" ? "本会话写过的文档" : "显式 docs"
|
|
68
|
+
const out = [
|
|
69
|
+
total ? `DOC-CHECK GATE: REFUSED(命中 ${total} 条)` : (checkFiles.length ? "DOC-CHECK GATE: PASS" : "DOC-CHECK GATE: PASS(0 档)"),
|
|
70
|
+
"文档纪律机检(交付门 · gate)",
|
|
71
|
+
checkFiles.length
|
|
72
|
+
? `范围:${src} ${checkFiles.length} 档 · 阈值 ${lineWidth} 字符(工具内置常量)`
|
|
73
|
+
: `范围:${src} —— **0 档受检**(空清单:本次没有判任何档,不得读作「已检」)· 阈值 ${lineWidth} 字符(工具内置常量)`,
|
|
74
|
+
"判据:工具内置常量(锚 ∥ 行宽)——判定范围 = 解析范围 = 传入档",
|
|
75
|
+
]
|
|
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(" ∥ ")}`)
|
|
78
|
+
if (checkFiles.length === 0) {
|
|
79
|
+
// 空范围不静默当通过(K6)——照实说明 + 给出补充通道(两侧都不得读成「已检」)。
|
|
80
|
+
// 措辞按「有没有跳过行」分流:无跳过行时不得写「见上方跳过行」。
|
|
81
|
+
const hasSkips = skipped.missing.length > 0 || skipped.notMarkdown.length > 0
|
|
82
|
+
out.push(docsSource === "session"
|
|
83
|
+
? (hasSkips
|
|
84
|
+
? "范围为空:本会话触达的档均不可判(见上方跳过行)——若确认改过 ⇒ 显式传 docs"
|
|
85
|
+
: "范围为空:本会话没有写过 .md 档——若确认改过 ⇒ 显式传 docs")
|
|
86
|
+
: (hasSkips
|
|
87
|
+
? "范围为空:显式 docs 里没有可判档——见上方跳过行"
|
|
88
|
+
: "范围为空:显式 docs 是空清单——列出要判的档后复跑"))
|
|
89
|
+
} else if (total === 0) {
|
|
90
|
+
out.push(`结论:0 条命中(${checkFiles.length} 档全清)`)
|
|
91
|
+
} else {
|
|
92
|
+
for (const h of anchors.hits) {
|
|
93
|
+
out.push(h.clazz === "档缺"
|
|
94
|
+
? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
|
|
95
|
+
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
96
|
+
}
|
|
97
|
+
for (const w of widths) out.push(`✗ 行宽 ${rel(base, w.file)}:${w.line}(${w.len} 字符)`)
|
|
98
|
+
out.push("处置:同轮修正后复跑(改文档 ∥ 修引擎);不转台账;拿不准 ⇒ 上抛主 agent 裁决(主 agent 拿不准 ⇒ 转呈用户)")
|
|
99
|
+
}
|
|
100
|
+
return out.join("\n")
|
|
101
|
+
}
|
|
102
|
+
|
|
18
103
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @param {string} root
|
|
104
|
+
* 跑一次文档纪律机检。
|
|
105
|
+
* @param {string} root 项目根(解析序 ① 的基准)
|
|
106
|
+
* @param {{docs?:string[], mode?:'report'|'gate', docsSource?:'session'|'explicit'}} [o]
|
|
107
|
+
* `mode` 缺省 = `report`;`docs` = 被判档清单(相对 `root` 或绝对)——**必填**(两种问法同):
|
|
108
|
+
* 缺省 ∥ 非数组 ⇒ 报错要求显式清单(零遍历、零全域兜底);`report` 面**空数组也报错**,`gate` 面空数组 ⇒
|
|
109
|
+
* `PASS(0 档)` + 空范围说明(「本轮一档没碰」是真实读数);`report` 面不可判档 ⇒ 报错、整次零判定;
|
|
110
|
+
* `gate` 面不可判档 ⇒ 跳过行;`docsSource` = 范围来源标签(`session` = 本会话触达记账 ∥ `explicit` = 显式
|
|
111
|
+
* `docs`)——缺省 = `explicit`;只影响报面措辞。
|
|
21
112
|
* @returns {{report:string, hits:{total:number, anchors:number, width:number}, anchorHits:Array,
|
|
22
|
-
* widthHits:Array,
|
|
23
|
-
*
|
|
113
|
+
* widthHits:Array, files:string[], checkFiles:string[], lineWidth:number,
|
|
114
|
+
* skipped:{missing:string[], notMarkdown:string[]}}}
|
|
115
|
+
* 消费面判「清」须读 `report` 首行(gate)∥ `hits.total`(report),不得只看其中一个。
|
|
24
116
|
*/
|
|
25
|
-
export function runDocCheck(root) {
|
|
117
|
+
export function runDocCheck(root, { docs, mode = "report", docsSource } = {}) {
|
|
26
118
|
const base = resolve(root)
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
throw new Error(`doc_check:
|
|
119
|
+
const gate = mode === "gate"
|
|
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>"].`)
|
|
122
|
+
}
|
|
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.')
|
|
125
|
+
}
|
|
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.')
|
|
130
|
+
}
|
|
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.`)
|
|
30
136
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.`)
|
|
141
|
+
}
|
|
142
|
+
const checkFiles = range.check
|
|
143
|
+
const { lineWidth, widthExemptZones } = DOC_CHECK_BUILTINS
|
|
144
|
+
const anchors = checkAnchors(base, { files: checkFiles })
|
|
145
|
+
const widths = checkDocWidths({ lineWidth, exemptZones: widthExemptZones, files: checkFiles })
|
|
36
146
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
147
|
+
const hits = { total: anchors.hits.length + widths.length, anchors: anchors.hits.length, width: widths.length }
|
|
148
|
+
const shared = {
|
|
149
|
+
hits, anchorHits: anchors.hits, widthHits: widths, files: checkFiles, checkFiles, lineWidth,
|
|
150
|
+
skipped: { missing: range.missing, notMarkdown: range.notMarkdown },
|
|
151
|
+
}
|
|
152
|
+
if (gate) {
|
|
153
|
+
return { ...shared, report: gateReport({ base, docsSource: source, checkFiles, skipped: shared.skipped, anchors, widths, lineWidth }) }
|
|
154
|
+
}
|
|
40
155
|
|
|
41
|
-
const total = anchors.hits.length + widths.length
|
|
42
156
|
const out = [
|
|
43
157
|
"文档纪律机检(报告态 · 非门禁)",
|
|
44
|
-
`射程:${base} · ${
|
|
45
|
-
"
|
|
46
|
-
|
|
158
|
+
`射程:${base} · 传入清单 ${checkFiles.length} 档 · 阈值 ${lineWidth} 字符(工具内置常量)· 区带豁免:${widthExemptZones.length ? widthExemptZones.join(" ∥ ") : "无"}`,
|
|
159
|
+
"声明:判据参数 = 工具内置常量;范围 = 传入文件(判定范围 = 解析范围)",
|
|
160
|
+
`范围:传入清单 ${checkFiles.length} 档(整档入判 · 判定范围 = 解析范围)`,
|
|
161
|
+
`命中 ${hits.total} 条(锚 ${anchors.hits.length} · 行宽 ${widths.length})`,
|
|
47
162
|
]
|
|
48
|
-
// 报告面(不作判):域内同名 ≥2 ⇒ 多义不判(残余登记——fail-open 方向,归评审核验面)
|
|
49
|
-
for (const r of anchors.reports) {
|
|
50
|
-
out.push(`报告 锚·多义 ${rel(base, r.file)}:${r.line} \`${r.token}\`(同名 ${r.candidates.length}:${r.candidates.map((c) => rel(base, c)).join(" ∥ ")}——报告面,不作判)`)
|
|
51
|
-
}
|
|
52
163
|
for (const h of anchors.hits) {
|
|
53
164
|
out.push(h.clazz === "档缺"
|
|
54
165
|
? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
|
|
55
166
|
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
56
167
|
}
|
|
57
168
|
for (const w of widths) out.push(`✗ 行宽 ${rel(base, w.file)}:${w.line}(${w.len} 字符)`)
|
|
58
|
-
out.push(total ? `结论:${total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 行宽 均清)")
|
|
169
|
+
out.push(hits.total ? `结论:${hits.total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 行宽 均清)")
|
|
59
170
|
|
|
60
|
-
return { report: out.join("\n")
|
|
171
|
+
return { ...shared, report: out.join("\n") }
|
|
61
172
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doc-check/version.mjs — 引擎版本一致性的**单源**(纯函数 + 常量;两个消费面共用一个判据)。
|
|
3
|
+
*
|
|
4
|
+
* 权威规格 = `docs/core/design/DOC-DISCIPLINE.md` §3.20「引擎版本一致」(判据 / 比较 / 裁定 / 保守方向;
|
|
5
|
+
* 本档只实现,不重述口径)。消费面两处:工具面 `agent-tools/doc-check.mjs` 的 gate 分支(转口导出、
|
|
6
|
+
* 导出面不变)∥ 结构门 `agent-tools/batch-gate.mjs`(batch 两机械点)——两处不得各持一份清单。
|
|
7
|
+
*
|
|
8
|
+
* 问题形态:`thincoder-core/doc-check/**` 是 ESM,进程启动时加载即缓存;引擎档在启动后被改写 ⇒
|
|
9
|
+
* 本会话仍跑旧模块 ⇒ 假阳(旧误报消不掉)/ 假绿(引擎变严时旧版放过)。故 gate 在跑判据前先判版本。
|
|
10
|
+
*
|
|
11
|
+
* `ENGINE_FILES` 六档 = 判据面:三判据档 + 本档(版本函数单源,自身入清单 = fail-closed)+ 工具面 +
|
|
12
|
+
* 描述档。路径按本档 `import.meta.url` 解析到产品核根,**不随 target 变**。
|
|
13
|
+
*/
|
|
14
|
+
import { statSync } from "node:fs"
|
|
15
|
+
import { resolve } from "node:path"
|
|
16
|
+
import { fileURLToPath } from "node:url"
|
|
17
|
+
|
|
18
|
+
/** 产品核根(版本清单基线——本档住 `doc-check/`,上跳一级)。 */
|
|
19
|
+
const CORE_ROOT = fileURLToPath(new URL("..", import.meta.url))
|
|
20
|
+
|
|
21
|
+
/** 引擎六档 = 版本一致性的判据面(§3.20「引擎版本一致」· 判据)。 */
|
|
22
|
+
export const ENGINE_FILES = [
|
|
23
|
+
"doc-check/index.mjs",
|
|
24
|
+
"doc-check/anchors.mjs",
|
|
25
|
+
"doc-check/width.mjs",
|
|
26
|
+
"doc-check/version.mjs",
|
|
27
|
+
"agent-tools/doc-check.mjs",
|
|
28
|
+
"tool-docs/doc_check.md",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 引擎版本一致性(纯函数——时钟 ∥ stat 可注 ⇒ 可测,不碰产品档 mtime):
|
|
33
|
+
* 逐档 `mtimeMs` 与**本进程启动时刻**(`nowMs − uptimeSec × 1000`)比较——晚于启动 ⇒ 该档可能晚于模块加载 ⇒ stale。
|
|
34
|
+
* 不可 `stat` 的档同样入 stale(fail-closed,不静默忽略)。`mtime ≤ 启动` 一定不比缓存新;
|
|
35
|
+
* `mtime > 启动` 只是**可能**晚于加载 —— 取 fail-closed 方向(误判代价 = 一次会话重启)。
|
|
36
|
+
* @param {{nowMs?:number, uptimeSec?:number, stat?:Function}} [o] 注入缝(缺省 = 真时钟 / 真 `statSync`)
|
|
37
|
+
* @returns {{startMs:number, stale:{file:string, mtimeMs:number|null}[]}} `file` = 相对 `thincoder-core/` 的正斜杠路径
|
|
38
|
+
*/
|
|
39
|
+
export function staleEngineFiles({ nowMs = Date.now(), uptimeSec = process.uptime(), stat = statSync } = {}) {
|
|
40
|
+
const startMs = nowMs - uptimeSec * 1000
|
|
41
|
+
const stale = []
|
|
42
|
+
for (const file of ENGINE_FILES) {
|
|
43
|
+
let mtimeMs = null
|
|
44
|
+
try { mtimeMs = stat(resolve(CORE_ROOT, file)).mtimeMs } catch { stale.push({ file, mtimeMs: null }); continue }
|
|
45
|
+
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs)) { stale.push({ file, mtimeMs: null }); continue }
|
|
46
|
+
if (mtimeMs > startMs) stale.push({ file, mtimeMs })
|
|
47
|
+
}
|
|
48
|
+
return { startMs, stale }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** stale 列表形(`<文件> mtime <ISO 8601>`,`;` 连列)——gate ERROR 行 ∥ report 注记行共用一处格式。 */
|
|
52
|
+
function staleParts(stale) {
|
|
53
|
+
const when = (t) => new Date(t).toISOString()
|
|
54
|
+
return stale.map((s) => `${s.file} mtime ${s.mtimeMs === null ? "不可读(stat 失败)" : when(s.mtimeMs)}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 引擎版本 ERROR 报面(固定句形):多档 stale ⇒ 逐档 `<文件> mtime <时间>` 以 `;` 连列——**不产 PASS**。 */
|
|
58
|
+
export function gateEngineError({ startMs, stale }) {
|
|
59
|
+
const when = (t) => new Date(t).toISOString()
|
|
60
|
+
return [
|
|
61
|
+
`DOC-CHECK GATE: ERROR(引擎已更新(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)}),请重启会话后复跑)`,
|
|
62
|
+
"原因:本会话进程加载的引擎模块早于盘上引擎档——旧引擎读数不可信(假阳:旧误报消不掉 ∥ 假绿:引擎变严时旧版放过)",
|
|
63
|
+
"处置:重启会话后复跑 gate(`report` 不裁定、不产 verdict(陈旧时只在首行前加注记))",
|
|
64
|
+
].join("\n")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 报告态引擎陈旧注记(纯函数):stale 空 ⇒ `null`;非空 ⇒ 一行注记(逐字单源 = `DOC-DISCIPLINE.md`
|
|
68
|
+
* §3.20「gate 裁定 · report 注记」——多档以 `;` 连列,与 `gateEngineError` 同形)。 */
|
|
69
|
+
export function reportEngineNote({ startMs, stale } = {}) {
|
|
70
|
+
if (!Array.isArray(stale) || stale.length === 0) return null
|
|
71
|
+
const when = (t) => new Date(t).toISOString()
|
|
72
|
+
return `[doc_check note] 引擎陈旧(${staleParts(stale).join(";")} > 进程启动 ${when(startMs)})——本次读数由会话启动时加载的旧引擎产出,不可作为交付判据;重启会话后复跑。`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 报面 + 注记:有注记 ⇒ 注记行 + 原报面;无 ⇒ 原报面逐字返回(三不改:既有行 ∥ verdict ∥ PASS 语义)。 */
|
|
76
|
+
export function withEngineNote(report, version) {
|
|
77
|
+
const note = reportEngineNote(version ?? {})
|
|
78
|
+
return note ? `${note}\n${report}` : report
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 报面首行取形(唯一机判串 = 首行 `DOC-CHECK GATE:` 前缀——消费面共用一个取法,不各自 split)。 */
|
|
82
|
+
export function firstVerdictLine(report) {
|
|
83
|
+
return String(report ?? "").split("\n")[0] ?? ""
|
|
84
|
+
}
|
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,15 +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
|
-
* 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。
|
|
101
|
-
|
|
56
|
+
* 围栏未闭合 ⇒ 至档尾整块免(fail-open 方向——§3.18 残余登记 ①);`exemptZones` 缺省 ∥ 空集 ⇒ 零豁免。
|
|
57
|
+
* `files`(必填)= **判定集**(调用方传入的档清单——本档不遍历任何目录;空集 ⇒ 零命中)。 */
|
|
58
|
+
export function checkDocWidths({ lineWidth, exemptZones = [], files = [] } = {}) {
|
|
102
59
|
const zones = (Array.isArray(exemptZones) ? exemptZones : []).filter((z) => typeof z === "string" && z !== "")
|
|
103
60
|
const hits = []
|
|
104
|
-
|
|
105
|
-
for (const f of files ?? scanDomain(base, { scanDirs, exclude })) {
|
|
61
|
+
for (const f of files) {
|
|
106
62
|
let text = ""
|
|
107
63
|
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
108
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: "已终止" },
|
|
@@ -84,6 +89,8 @@ export const CORE_MESSAGES = Object.freeze({
|
|
|
84
89
|
"ssh.field.user": { en: "Username", zh: "用户名" },
|
|
85
90
|
"ssh.field.password": { en: "Password", zh: "密码" },
|
|
86
91
|
"ssh.field.secret": { en: "Secret", zh: "密文" },
|
|
92
|
+
// 保存面(用户裁定 A+B · 台账 #1602 · §6.22 ⑪):窗内勾选标签(三端 `ssh.field.<name>` 投影自动命中)
|
|
93
|
+
"ssh.field.save": { en: "Save credential (encrypted, this user account)", zh: "保存凭据(加密,本机用户账户)" },
|
|
87
94
|
// 无 UI 失败句(E6;三端投影单源):en 侧 = CLI 端 `SECRET_NO_UI_TEXT` 逐字(端常量收正为核键)。
|
|
88
95
|
"ssh.no_ui": { en: "ssh credential input requires an interactive UI (none in this context) - use key-based auth or ssh-agent", zh: "ssh 凭据输入需要交互界面(当前场合没有)——请改用密钥认证或 ssh-agent" },
|
|
89
96
|
})
|