@thincoder/core 0.10.9 → 0.10.11
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 +28 -2
- package/advisor/messages.mjs +4 -5
- package/advisor/project-context.mjs +69 -44
- package/agent/completion.mjs +4 -1
- package/agent/family-tools.mjs +12 -10
- package/agent/helpers.mjs +19 -11
- package/agent/light-round.mjs +1 -4
- package/agent/record-results.mjs +6 -1
- package/agent/run-start.mjs +1 -0
- package/agent/setup-reminders.mjs +1 -1
- package/agent/turn-loop.mjs +17 -13
- package/agent/unbooked-pen.mjs +62 -0
- package/agent-tools/advisor.mjs +15 -31
- package/agent-tools/audit-block.mjs +16 -62
- package/agent-tools/batch-gate.mjs +12 -21
- package/agent-tools/checkpoint.mjs +11 -6
- package/agent-tools/context.mjs +1 -1
- package/agent-tools/design-token.mjs +9 -2
- package/agent-tools/digest-budget.mjs +2 -0
- package/agent-tools/doc-check.mjs +17 -23
- package/agent-tools/manifest.mjs +45 -14
- package/agent-tools/review-facts.mjs +35 -81
- package/agent-tools/settings.mjs +65 -5
- package/agent-tools/spawn-gates.mjs +68 -31
- package/agent-tools/subagent-async.mjs +6 -3
- package/agent-tools/subagent-spawn.mjs +76 -80
- package/agent-tools/subagent.mjs +16 -16
- package/agent-tools.mjs +1 -1
- package/browser/launch.mjs +14 -3
- package/browser/queue.mjs +31 -3
- package/browser/session.mjs +4 -0
- package/config-defaults.mjs +8 -0
- package/config-presets.mjs +1 -1
- package/config.mjs +60 -11
- package/declaration.mjs +5 -12
- package/doc-check/anchors.mjs +53 -60
- package/doc-check/index.mjs +106 -140
- package/doc-check/paragraph.mjs +119 -0
- package/fts-text.mjs +4 -1
- package/i18n.mjs +5 -0
- package/ledger-cmd.mjs +17 -4
- package/ledger-db.mjs +14 -8
- package/ledger.mjs +1 -1
- package/manifest-discovery.mjs +19 -10
- package/manifest-schema.mjs +7 -35
- package/manifest.mjs +21 -9
- package/memory/code-search.mjs +5 -14
- package/memory/docs.mjs +5 -14
- package/memory/read-gate.mjs +26 -0
- package/model-context.mjs +98 -0
- package/model-specs.mjs +110 -37
- package/package.json +2 -2
- package/permission.mjs +3 -1
- package/prompts/advisor-design.md +104 -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 +214 -58
- package/prompts/consult-base.md +10 -3
- package/prompts/discipline-engineering.md +202 -54
- package/prompts/discipline-normal.md +95 -44
- package/prompts/persona-coder.md +3 -1
- package/prompts/persona-eng-coder.md +29 -9
- package/prompts/persona-eng-designer.md +101 -35
- package/prompts/persona-engineering.md +320 -98
- package/prompts/persona-explore.md +1 -1
- package/prompts/persona-normal.md +16 -8
- package/provider/anthropic.mjs +127 -4
- package/provider/core.mjs +16 -6
- package/provider/google.mjs +8 -8
- package/provider/list-models.mjs +37 -5
- package/provider/normalize.mjs +2 -2
- package/provider/responses.mjs +7 -7
- package/provider/sse.mjs +6 -5
- package/proxy-transport.mjs +5 -3
- package/proxy.mjs +2 -1
- package/session-guard.mjs +7 -1
- package/session-lifecycle.mjs +23 -8
- package/session-segments.mjs +1 -0
- package/session-slot-claims.mjs +4 -0
- package/session-slot-write.mjs +6 -0
- package/session-slots-manifest.mjs +4 -0
- package/token-ttl.mjs +10 -0
- package/tool-docs/advisor.md +24 -5
- package/tool-docs/doc_check.md +44 -25
- package/tool-docs/ledger.md +2 -2
- package/tool-docs/manifest-read.md +8 -0
- package/tool-docs/manifest.md +9 -8
- package/tool-docs/subagent.md +41 -16
- package/tools/web-deepseek.mjs +26 -11
- package/doc-check/version.mjs +0 -84
- package/doc-check/width.mjs +0 -132
package/declaration.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* declaration.mjs — 声明装载面(PROJECT-MANIFEST.json
|
|
2
|
+
* declaration.mjs — 声明装载面(PROJECT-MANIFEST.json 两族 `codePaths` / `index.*`)
|
|
3
3
|
* · 自 `conventions.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786。
|
|
4
4
|
* 投影单源 = `manifest.mjs`(ONE reader / ONE root resolution——`readManifest` + `manifestFilePath`;
|
|
5
5
|
* name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
|
|
@@ -62,11 +62,7 @@ function normalizePathList(v) {
|
|
|
62
62
|
return out
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
return typeof v === "string" && v.trim() ? v.trim() : ""
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Project the manifest three families into the frozen declaration object.
|
|
65
|
+
/** Project the manifest two families into the frozen declaration object.
|
|
70
66
|
* `declared` = VALUE comparison against the defaults — at least one family deviates
|
|
71
67
|
* (a key written with its default value still counts as NOT declared; the design-gate
|
|
72
68
|
* hint reads it to decide whether to point at the manifest, PORTABILITY §3.1).
|
|
@@ -80,11 +76,9 @@ function buildDeclaration(m, root = null) {
|
|
|
80
76
|
const excludePaths = normalizePathList(m?.index?.excludePaths)
|
|
81
77
|
const publicRepos = normalizePathList(m?.index?.publicRepos)
|
|
82
78
|
const batchRecords = batchSegments(m?.docRoot?.batches) // §6.14 面⑥ 声明值投影(`declared` 旗不涉——docRoot 族现状零改)
|
|
83
|
-
const docMap = normalizeString(m?.advisor?.docMap)
|
|
84
|
-
const standardsDoc = normalizeString(m?.advisor?.standardsDoc)
|
|
85
79
|
const defaults = normalizeCodePaths(DEFAULT_MANIFEST.codePaths) ?? []
|
|
86
80
|
const pathsDiffer = codePaths.length !== defaults.length || codePaths.some((p, i) => p !== defaults[i])
|
|
87
|
-
const declared = pathsDiffer || codeExtensions.length > 0 || docExtensions.length > 0 || excludePaths.length > 0 || publicRepos.length > 0
|
|
81
|
+
const declared = pathsDiffer || codeExtensions.length > 0 || docExtensions.length > 0 || excludePaths.length > 0 || publicRepos.length > 0
|
|
88
82
|
return Object.freeze({
|
|
89
83
|
declared,
|
|
90
84
|
codePaths: Object.freeze(codePaths),
|
|
@@ -95,7 +89,6 @@ function buildDeclaration(m, root = null) {
|
|
|
95
89
|
excludePaths: Object.freeze(excludePaths),
|
|
96
90
|
publicRepos: Object.freeze(publicRepos),
|
|
97
91
|
}),
|
|
98
|
-
advisor: Object.freeze({ docMap, standardsDoc }),
|
|
99
92
|
root,
|
|
100
93
|
})
|
|
101
94
|
}
|
|
@@ -317,7 +310,7 @@ onManifestInit(clearDeclarationCache)
|
|
|
317
310
|
|
|
318
311
|
/** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
|
|
319
312
|
function warnRetiredCarrier(root) {
|
|
320
|
-
console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions
|
|
313
|
+
console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions into PROJECT-MANIFEST.json`)
|
|
321
314
|
logEvent("declaration:retired-file", { cwd: root })
|
|
322
315
|
}
|
|
323
316
|
|
|
@@ -334,7 +327,7 @@ function warnRetiredCarrier(root) {
|
|
|
334
327
|
* @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
|
|
335
328
|
* @returns {Readonly<{declared: boolean, codePaths: readonly string[], batchRecords: readonly (readonly string[])[],
|
|
336
329
|
* index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
|
|
337
|
-
*
|
|
330
|
+
* root: string|null}>}
|
|
338
331
|
*/
|
|
339
332
|
export function loadProjectDeclaration(cwd) {
|
|
340
333
|
const file = manifestFilePath(cwd) // data-file path (single source — manifest.mjs KD-M1-18)
|
package/doc-check/anchors.mjs
CHANGED
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
* **行号坐标(`:N`)不作节指示**——行号不作引用;
|
|
7
7
|
* · 档不在盘 ⇒ `档缺`;带节指示 ∧ 节不在盘 ⇒ `节缺`。
|
|
8
8
|
*
|
|
9
|
-
* 抽取面:逐行扫;fenced 块整块跳过;可执行行(`isExecutableLine
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* 抽取面:逐行扫;fenced 块整块跳过;可执行行(`isExecutableLine`)整行跳过(命令保字面);
|
|
10
|
+
* 提示词面另剥 CANON 操作数(面判据 = `./paragraph.mjs` 的 `isPromptFace`——七名整串删除后再抽,见 `CANON_OPERANDS`)。
|
|
11
|
+
* 解析序**两级**:① 仓根相对全路径 → ② 引用方档所在目录相对。零命中 ⇒ `档缺`。
|
|
12
|
+
* **裸名不跨目录解析**:跨目录解析须先建全域目录索引——为认出一个裸名而重建索引,等于把全仓扫描请回来;
|
|
13
|
+
* 写全路径后解析零索引,逐档判就是全部(与 D4 实现坐标口径一致)。
|
|
14
|
+
* 节在盘:**只读解析命中的那一档**(点查,不遍历)——目标档内存在标题行,其标题编号
|
|
15
|
+
* (标题文本去 `^#{1,6}\s` 后的首个 `\d+(?:\.\d+)*`)= 指针节号。
|
|
16
|
+
* 残余登记(fail-open 方向——归评审核验面):未闭合围栏 ⇒ 至档尾免;指针行的坐标尾不入判、同行的档 token 照判。
|
|
15
17
|
*/
|
|
16
18
|
import { readFileSync, statSync } from "node:fs"
|
|
17
19
|
import { basename, dirname, resolve } from "node:path"
|
|
18
|
-
import {
|
|
19
|
-
import { fenceMark, isExecutableLine } from "./width.mjs"
|
|
20
|
+
import { fenceMark, isExecutableLine, isPromptFace } from "./paragraph.mjs"
|
|
20
21
|
|
|
21
22
|
/** 文档指针 token:`(目录段/)*名.md` + 可选行号坐标尾 + 可选节指示(`§` + 编号)。
|
|
22
23
|
* 左界守卫排除更长 token 的尾段;右界排除后续字母数字(`.md` 之后紧邻 `[\w]` ⇒ 非完整 token)。 */
|
|
@@ -34,12 +35,35 @@ const PLACEHOLDER_STEMS = new Set([
|
|
|
34
35
|
"x", "a", "b", "file", "out", "foo", "bar", "example", "sample", "name", "path",
|
|
35
36
|
])
|
|
36
37
|
|
|
38
|
+
/** CANON 操作数族(**规范豁免串** · 判据单源 = `docs/core/design/PROMPT-SYSTEM.md` §6.6「判保留面」):
|
|
39
|
+
* 七个名字在模型面是**模型要写出来 / 读写的文件名**(行为操作数 · 路径操作数),不是「去读这个档」的指针——
|
|
40
|
+
* 换个目录解析必然 `档缺`。**只对提示词面剥**(面判据 = `isPromptFace`,单源住 `./paragraph.mjs`):`docs/` 面不会因本条多放行一个名——
|
|
41
|
+
* `README.md` / `MANIFEST.md` 在那里仍按指针判(其余四名早已走 `FILENAME_CLASSES` 类名免,非本条引入)。
|
|
42
|
+
* **一处定义、两处消费**:`scripts/prompt-refs-check.mjs` 的 J3 豁免从本档 import 本表与剥除函数(不复制);
|
|
43
|
+
* 反方向零 import——判据链自持 = `docs/core/design/DOC-DISCIPLINE.md` A-DD29 ⑤(`thincoder-core/**` 对
|
|
44
|
+
* `scripts/**` 的 import 点 = 0)。闭集外的同形 token(非操作数)照报。 */
|
|
45
|
+
export const CANON_OPERANDS = [
|
|
46
|
+
"AGENTS.md", "SKILL.md", "README.md", "MANIFEST.md",
|
|
47
|
+
".thincoder/advisor.md", "project_rules.md",
|
|
48
|
+
"RTM.md",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/** 操作数剥除:逐行先整串删除七名 CANON 操作数再抽(与 `scripts/prompt-refs-check.mjs` 的 J3 豁免同式)。 */
|
|
52
|
+
export function stripCanonOperands(line) {
|
|
53
|
+
let out = line
|
|
54
|
+
for (const s of CANON_OPERANDS) out = out.split(s).join("")
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 操作数豁免的**作用面** = 提示词面(`thincoder-core/prompts/**`)——**只此一面**:`docs/` 面同名 token 仍按指针判
|
|
59
|
+
* (`README.md` / `MANIFEST.md` 在那里照报 `档缺`;其余四名走既有的 `FILENAME_CLASSES` 类名免,非本条引入)。
|
|
60
|
+
* 这是锚判自己的面判据——与段判据零共享开关:段判据按传入档全量判(提示词面照判——逐行裸判,无面级豁免)。 */
|
|
61
|
+
// 提示词面判据单源 = `./paragraph.mjs` 的 `isPromptFace`(§3.18 ②——形态与旧 `OPERAND_FACE` 逐字同)。
|
|
62
|
+
const isOperandFace = isPromptFace
|
|
63
|
+
|
|
37
64
|
/** 文件存在性(目录 / 读错 ⇒ false)。 */
|
|
38
65
|
function isFile(p) { try { return statSync(p).isFile() } catch { return false } }
|
|
39
66
|
|
|
40
|
-
/** 目录存在性(文件 / 读错 ⇒ false)。 */
|
|
41
|
-
function isDir(p) { try { return statSync(p).isDirectory() } catch { return false } }
|
|
42
|
-
|
|
43
67
|
/** 标题编号(标题文本去 `^#{1,6}\s` 后的首个 `\d+(?:\.\d+)*`);非标题行 ⇒ null。 */
|
|
44
68
|
function headingNumber(line) {
|
|
45
69
|
const m = /^#{1,6}\s+/.exec(line)
|
|
@@ -56,63 +80,32 @@ function sectionOnDisk(path, num) {
|
|
|
56
80
|
return false
|
|
57
81
|
}
|
|
58
82
|
|
|
59
|
-
/** 档解析(判据 ①
|
|
83
|
+
/** 档解析(判据 ① 的解析序两级):命中 ⇒ `{ ok, path }`;零命中 ⇒ `{ ok:false }`。
|
|
60
84
|
* 行号坐标尾(`:N`)不参与档解析——token 只取 `.md` 段(抽取面已剥离坐标尾与节指示)。
|
|
61
|
-
* `
|
|
62
|
-
function resolveDoc(token, refFile, base
|
|
85
|
+
* `num` = 指针节号(`§` 后编号;无节指示 ⇒ null)——仅用于判定节在盘,不参与档解析。 */
|
|
86
|
+
function resolveDoc(token, refFile, base) {
|
|
63
87
|
const p1 = resolve(base, token)
|
|
64
|
-
if (isFile(p1)) return { ok: true, path: p1 } // ①
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (isFile(p3)) return { ok: true, path: p3 } // ③ 引用方档所在目录相对
|
|
68
|
-
const cands = index.get(basename(token)) ?? [] // ④ 域内唯一 basename 索引
|
|
69
|
-
if (cands.length === 1) return { ok: true, path: cands[0] }
|
|
70
|
-
if (cands.length > 1) return { ambiguous: true, candidates: cands } // ④ 多义 ⇒ 终结,不进 ⑤
|
|
71
|
-
for (const r of roots) { const p = resolve(base, r, token); if (isFile(p)) return { ok: true, path: p } } // ⑤ 声明解析根相对
|
|
88
|
+
if (isFile(p1)) return { ok: true, path: p1 } // ① 仓根相对全路径
|
|
89
|
+
const p2 = resolve(dirname(refFile), token)
|
|
90
|
+
if (isFile(p2)) return { ok: true, path: p2 } // ② 引用方档所在目录相对
|
|
72
91
|
return { ok: false }
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
/** 声明解析根逐元素归类(口径单源 = `DOC-DISCIPLINE.md` §3.20 判据 ① 解析序 ⑤):
|
|
76
|
-
* `usable` = 参与解析的根(声明序);`gaps` = 报告面(不作判)。缺键 ⇒ 本步零效(不产报告行)。 */
|
|
77
|
-
function classifyRoots(base, roots) {
|
|
78
|
-
const out = { usable: [], gaps: [] }
|
|
79
|
-
if (roots === undefined) return out
|
|
80
|
-
if (!Array.isArray(roots)) { out.gaps.push({ kind: "invalid", root: roots, reason: "非字符串数组" }); return out }
|
|
81
|
-
for (const r of roots) {
|
|
82
|
-
const why = anchorRootError(r)
|
|
83
|
-
if (why) { out.gaps.push({ kind: "invalid", root: r, reason: why }); continue }
|
|
84
|
-
if (!isDir(resolve(base, r))) { out.gaps.push({ kind: "missing", root: r }); continue }
|
|
85
|
-
out.usable.push(r)
|
|
86
|
-
}
|
|
87
|
-
return out
|
|
88
|
-
}
|
|
89
|
-
|
|
90
94
|
/**
|
|
91
|
-
*
|
|
92
|
-
* @param {string} root
|
|
93
|
-
* @param {{
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `roots` = 声明解析根原值(值形 ∥ 根在盘的门归本函数——报告面逐元素弃用 / 列缺位)。
|
|
97
|
-
* @returns {{hits:Array, reports:Array, rootGaps:Array}} hits = 命中(`档缺` / `节缺`);reports = 报告面(多义——不作判);
|
|
98
|
-
* rootGaps = 报告面(解析根弃用 ∥ 缺位——不作判)
|
|
95
|
+
* 锚判据主行程:传入档逐档逐行抽取 + 解析 + 节在盘判定。
|
|
96
|
+
* @param {string} root 项目根(解析序 ① 的基准)
|
|
97
|
+
* @param {{files?:string[]}} o `files` = **判定集兼解析集**(调用方传入的档清单——判定范围 = 解析范围,
|
|
98
|
+
* 无目录遍历、无全域索引:清单外的档既不判也不参与解析)
|
|
99
|
+
* @returns {{hits:Array}} hits = 命中(`档缺` / `节缺`)
|
|
99
100
|
*/
|
|
100
|
-
export function checkAnchors(root, {
|
|
101
|
+
export function checkAnchors(root, { files = [] } = {}) {
|
|
101
102
|
const base = resolve(root)
|
|
102
|
-
const dirs = Array.isArray(scanDirs) ? scanDirs : []
|
|
103
|
-
const { usable, gaps } = classifyRoots(base, roots)
|
|
104
|
-
const index = new Map()
|
|
105
|
-
for (const f of files) {
|
|
106
|
-
const k = basename(f)
|
|
107
|
-
if (!index.has(k)) index.set(k, [])
|
|
108
|
-
index.get(k).push(f)
|
|
109
|
-
}
|
|
110
103
|
const hits = []
|
|
111
|
-
const
|
|
112
|
-
for (const f of checkFiles ?? files) {
|
|
104
|
+
for (const f of files) {
|
|
113
105
|
let text = ""
|
|
114
106
|
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
115
107
|
const re = new RegExp(ANCHOR_RE.source, "g") // 每档一份实例(零共享 lastIndex 状态)
|
|
108
|
+
const stripOperands = isOperandFace(f) // 只有提示词面剥(面判据 = `isPromptFace`)
|
|
116
109
|
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
117
110
|
text.split("\n").forEach((l, i) => {
|
|
118
111
|
const mark = fenceMark(l)
|
|
@@ -123,20 +116,20 @@ export function checkAnchors(root, { scanDirs = [], files = [], checkFiles, root
|
|
|
123
116
|
}
|
|
124
117
|
if (fence !== null) return
|
|
125
118
|
if (isExecutableLine(l)) return // 可执行行整行跳过
|
|
119
|
+
const scan = stripOperands ? stripCanonOperands(l) : l // 提示词面:操作数先剥再抽;文档面 = 原行
|
|
126
120
|
let m
|
|
127
|
-
while ((m = re.exec(
|
|
121
|
+
while ((m = re.exec(scan)) !== null) {
|
|
128
122
|
const token = m[1]
|
|
129
123
|
const num = m[2] ?? null
|
|
130
124
|
const bn = basename(token).toLowerCase()
|
|
131
125
|
const stem = bn.replace(/\.md$/, "")
|
|
132
126
|
// 类名 ∥ 占位符两免:抽取面不把它们当指针(判据 = DOC-DISCIPLINE §3.20 ① 的「档 token」定义面)
|
|
133
127
|
if (FILENAME_CLASSES.has(bn) || stem.length <= 1 || PLACEHOLDER_STEMS.has(stem)) continue
|
|
134
|
-
const r = resolveDoc(token, f, base
|
|
135
|
-
if (r.ambiguous) { reports.push({ file: f, line: i + 1, token, candidates: r.candidates }); continue }
|
|
128
|
+
const r = resolveDoc(token, f, base)
|
|
136
129
|
if (!r.ok) { hits.push({ file: f, line: i + 1, token, clazz: "档缺" }); continue }
|
|
137
130
|
if (num !== null && !sectionOnDisk(r.path, num)) hits.push({ file: f, line: i + 1, token, clazz: "节缺", section: num, target: r.path })
|
|
138
131
|
}
|
|
139
132
|
})
|
|
140
133
|
}
|
|
141
|
-
return { hits
|
|
134
|
+
return { hits }
|
|
142
135
|
}
|
package/doc-check/index.mjs
CHANGED
|
@@ -1,109 +1,95 @@
|
|
|
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
|
-
* ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;②
|
|
6
|
-
*
|
|
5
|
+
* ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;② 段判据(段太长 ∥ 行太长)——口径 = §3.18 条目 Q,实现 = `./paragraph.mjs`。
|
|
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 { checkParagraphs } from "./paragraph.mjs"
|
|
17
19
|
import { checkAnchors } from "./anchors.mjs"
|
|
18
20
|
|
|
21
|
+
/** 判据参数(内置常量 · 冻结):阈值 300 字符(一段 ∥ 一行同值——§3.18 ④);两项空列表 = 区带类 / 豁免类默认值且恒空
|
|
22
|
+
* ——豁免谓词(表行 / 围栏 / 列表项 / 引文)住代码,没有「登记后放过」的写入口(空列表 ≠ 可登记豁免)。 */
|
|
23
|
+
export const DOC_CHECK_BUILTINS = Object.freeze({
|
|
24
|
+
paragraphMax: 300,
|
|
25
|
+
lineMax: 300,
|
|
26
|
+
paragraphExemptZones: Object.freeze([]),
|
|
27
|
+
exemptions: Object.freeze([]),
|
|
28
|
+
})
|
|
29
|
+
|
|
19
30
|
/** 相对路径显示形(正斜杠——跨平台同形)。 */
|
|
20
31
|
const rel = (root, p) => relative(root, p).replace(/\\/g, "/")
|
|
21
32
|
|
|
22
|
-
/** 平台路径键(Windows
|
|
33
|
+
/** 平台路径键(Windows 大小写不敏感——只用于清单去重,非判据面)。 */
|
|
23
34
|
const pathKey = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
24
35
|
|
|
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
|
-
}
|
|
36
|
+
/** 不可判档的两因(闭集 · 判序 = 存在性 → 类型):不在盘 ∥ 非 markdown 档(目录归后因)。 */
|
|
37
|
+
function undecidableReason(abs) {
|
|
38
|
+
let st
|
|
39
|
+
try { st = statSync(abs) } catch { return "not on disk" }
|
|
40
|
+
if (!st.isFile() || !abs.toLowerCase().endsWith(".md")) return "not a markdown document"
|
|
66
41
|
return null
|
|
67
42
|
}
|
|
68
43
|
|
|
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
|
|
44
|
+
/** 清单 → 可判集(去重保序)+ 不可判档两类(`missing` ∥ `notMarkdown`)+ 非法条目下标(非字符串 / 空白)。 */
|
|
45
|
+
function resolveRange(base, docs) {
|
|
46
|
+
const seen = new Set()
|
|
47
|
+
const check = []
|
|
48
|
+
const undecidable = []
|
|
49
|
+
const missing = []
|
|
50
|
+
const notMarkdown = []
|
|
51
|
+
const invalid = []
|
|
52
|
+
docs.forEach((d, i) => {
|
|
53
|
+
if (typeof d !== "string" || d.trim() === "") { invalid.push(i); return }
|
|
81
54
|
const abs = resolve(base, d)
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
55
|
+
const key = pathKey(abs)
|
|
56
|
+
const reason = undecidableReason(abs)
|
|
57
|
+
if (reason === null) { if (!seen.has(key)) { seen.add(key); check.push(abs) } return }
|
|
58
|
+
undecidable.push({ abs, reason })
|
|
59
|
+
if (reason === "not on disk") missing.push(abs)
|
|
60
|
+
else notMarkdown.push(abs)
|
|
61
|
+
})
|
|
62
|
+
return { check: check.sort(), undecidable, missing, notMarkdown, invalid }
|
|
88
63
|
}
|
|
89
64
|
|
|
65
|
+
/** 段判据命中行(报面单源——两类命中同形,按 `clazz` 分词)。 */
|
|
66
|
+
const paragraphHitLine = (base, h) => `✗ ${h.clazz} ${rel(base, h.file)}:${h.line}(${h.len} 字符)`
|
|
67
|
+
|
|
90
68
|
/** gate 报面(固定句形——§3.20「报面(固定句形 · 交付门 gate)」):首行 verdict = 唯一机判串。 */
|
|
91
|
-
function gateReport({ base, docsSource, checkFiles,
|
|
92
|
-
const total = anchors.hits.length +
|
|
69
|
+
function gateReport({ base, docsSource, checkFiles, skipped, anchors, paragraphs, paragraphMax }) {
|
|
70
|
+
const total = anchors.hits.length + paragraphs.length
|
|
93
71
|
const src = docsSource === "session" ? "本会话写过的文档" : "显式 docs"
|
|
94
72
|
const out = [
|
|
95
73
|
total ? `DOC-CHECK GATE: REFUSED(命中 ${total} 条)` : (checkFiles.length ? "DOC-CHECK GATE: PASS" : "DOC-CHECK GATE: PASS(0 档)"),
|
|
96
74
|
"文档纪律机检(交付门 · gate)",
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
checkFiles.length
|
|
76
|
+
? `范围:${src} ${checkFiles.length} 档 · 阈值 ${paragraphMax} 字符(工具内置常量)`
|
|
77
|
+
: `范围:${src} —— **0 档受检**(空清单:本次没有判任何档,不得读作「已检」)· 阈值 ${paragraphMax} 字符(工具内置常量)`,
|
|
78
|
+
"判据:工具内置常量(锚 ∥ 段判据)——判定范围 = 解析范围 = 传入档",
|
|
99
79
|
]
|
|
100
|
-
if (skipped.
|
|
101
|
-
if (skipped.
|
|
80
|
+
if (skipped.missing.length) out.push(`跳过 ${skipped.missing.length} 档(已不在盘,未检):${skipped.missing.map((p) => rel(base, p)).join(" ∥ ")}`)
|
|
81
|
+
if (skipped.notMarkdown.length) out.push(`跳过 ${skipped.notMarkdown.length} 档(非 markdown 档,未检):${skipped.notMarkdown.map((p) => rel(base, p)).join(" ∥ ")}`)
|
|
102
82
|
if (checkFiles.length === 0) {
|
|
103
|
-
// 空范围不静默当通过(K6)——照实说明 +
|
|
83
|
+
// 空范围不静默当通过(K6)——照实说明 + 给出补充通道(两侧都不得读成「已检」)。
|
|
84
|
+
// 措辞按「有没有跳过行」分流:无跳过行时不得写「见上方跳过行」。
|
|
85
|
+
const hasSkips = skipped.missing.length > 0 || skipped.notMarkdown.length > 0
|
|
104
86
|
out.push(docsSource === "session"
|
|
105
|
-
?
|
|
106
|
-
|
|
87
|
+
? (hasSkips
|
|
88
|
+
? "范围为空:本会话触达的档均不可判(见上方跳过行)——若确认改过 ⇒ 显式传 docs"
|
|
89
|
+
: "范围为空:本会话没有写过 .md 档——若确认改过 ⇒ 显式传 docs")
|
|
90
|
+
: (hasSkips
|
|
91
|
+
? "范围为空:显式 docs 里没有可判档——见上方跳过行"
|
|
92
|
+
: "范围为空:显式 docs 是空清单——列出要判的档后复跑"))
|
|
107
93
|
} else if (total === 0) {
|
|
108
94
|
out.push(`结论:0 条命中(${checkFiles.length} 档全清)`)
|
|
109
95
|
} else {
|
|
@@ -112,7 +98,7 @@ function gateReport({ base, docsSource, checkFiles, domainCount, skipped, anchor
|
|
|
112
98
|
? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
|
|
113
99
|
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
114
100
|
}
|
|
115
|
-
for (const w of
|
|
101
|
+
for (const w of paragraphs) out.push(paragraphHitLine(base, w))
|
|
116
102
|
out.push("处置:同轮修正后复跑(改文档 ∥ 修引擎);不转台账;拿不准 ⇒ 上抛主 agent 裁决(主 agent 拿不准 ⇒ 转呈用户)")
|
|
117
103
|
}
|
|
118
104
|
return out.join("\n")
|
|
@@ -120,91 +106,71 @@ function gateReport({ base, docsSource, checkFiles, domainCount, skipped, anchor
|
|
|
120
106
|
|
|
121
107
|
/**
|
|
122
108
|
* 跑一次文档纪律机检。
|
|
123
|
-
* @param {string} root
|
|
109
|
+
* @param {string} root 项目根(解析序 ① 的基准)
|
|
124
110
|
* @param {{docs?:string[], mode?:'report'|'gate', docsSource?:'session'|'explicit'}} [o]
|
|
125
|
-
* `mode` 缺省 = `report
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
111
|
+
* `mode` 缺省 = `report`;`docs` = 被判档清单(相对 `root` 或绝对)——**必填**(两种问法同):
|
|
112
|
+
* 缺省 ∥ 非数组 ⇒ 报错要求显式清单(零遍历、零全域兜底);`report` 面**空数组也报错**,`gate` 面空数组 ⇒
|
|
113
|
+
* `PASS(0 档)` + 空范围说明(「本轮一档没碰」是真实读数);`report` 面不可判档 ⇒ 报错、整次零判定;
|
|
114
|
+
* `gate` 面不可判档 ⇒ 跳过行;`docsSource` = 范围来源标签(`session` = 本会话触达记账 ∥ `explicit` = 显式
|
|
115
|
+
* `docs`)——缺省 = `explicit`;只影响报面措辞。
|
|
116
|
+
* @returns {{report:string, hits:{total:number, anchors:number, paragraph:number}, anchorHits:Array,
|
|
117
|
+
* paragraphHits:Array, files:string[], checkFiles:string[], paragraphMax:number,
|
|
118
|
+
* skipped:{missing:string[], notMarkdown:string[]}}}
|
|
119
|
+
* 消费面判「清」须读 `report` 首行(gate)∥ `hits.total`(report),不得只看其中一个。
|
|
133
120
|
*/
|
|
134
121
|
export function runDocCheck(root, { docs, mode = "report", docsSource } = {}) {
|
|
135
122
|
const base = resolve(root)
|
|
136
123
|
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)] })
|
|
124
|
+
if (docs !== undefined && !Array.isArray(docs)) {
|
|
125
|
+
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
126
|
}
|
|
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(";")})`)
|
|
127
|
+
if (!Array.isArray(docs)) {
|
|
128
|
+
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
129
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
if (gate && (
|
|
154
|
-
|
|
130
|
+
// 报告态空清单 = 报错(「没跑」不得装成「跑了且清」);gate 面空清单照实出 `PASS(0 档)` + 空范围说明
|
|
131
|
+
// ——交付面「本轮一档没碰」是一种真实读数,不是省略。
|
|
132
|
+
if (!gate && (docs.length === 0 || docs.every((d) => typeof d !== "string" || d.trim() === ""))) {
|
|
133
|
+
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
134
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
135
|
+
const source = docsSource ?? "explicit"
|
|
136
|
+
const range = resolveRange(base, docs)
|
|
137
|
+
// 清单条目本身非法(非字符串 / 空白)——调用方错误,两种问法同拒:静默丢掉一条就是不静默地收窄范围。
|
|
138
|
+
if (range.invalid.length) {
|
|
139
|
+
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.`)
|
|
140
|
+
}
|
|
141
|
+
// 报告态 fail-closed:清单里有一个不可判档 ⇒ 报错点名、整次零判定——清单写错却报 0 命中就是「没跑」装成「跑了且清」。
|
|
142
|
+
if (!gate && range.undecidable.length) {
|
|
143
|
+
const named = range.undecidable.map((u) => `${rel(base, u.abs)}(${u.reason})`).join(" ∥ ")
|
|
144
|
+
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
145
|
}
|
|
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
146
|
const checkFiles = range.check
|
|
170
|
-
const
|
|
171
|
-
const
|
|
147
|
+
const { paragraphMax, lineMax, paragraphExemptZones } = DOC_CHECK_BUILTINS
|
|
148
|
+
const anchors = checkAnchors(base, { files: checkFiles })
|
|
149
|
+
const paragraphs = checkParagraphs({ paragraphMax, lineMax, exemptZones: paragraphExemptZones, files: checkFiles })
|
|
172
150
|
|
|
173
|
-
const hits = { total: anchors.hits.length +
|
|
151
|
+
const hits = { total: anchors.hits.length + paragraphs.length, anchors: anchors.hits.length, paragraph: paragraphs.length }
|
|
174
152
|
const shared = {
|
|
175
|
-
hits, anchorHits: anchors.hits,
|
|
176
|
-
|
|
153
|
+
hits, anchorHits: anchors.hits, paragraphHits: paragraphs, files: checkFiles, checkFiles, paragraphMax,
|
|
154
|
+
skipped: { missing: range.missing, notMarkdown: range.notMarkdown },
|
|
177
155
|
}
|
|
178
156
|
if (gate) {
|
|
179
|
-
return {
|
|
180
|
-
...shared,
|
|
181
|
-
report: gateReport({ base, docsSource: source, checkFiles, domainCount: files.length, skipped: shared.skipped, anchors, widths, lineWidth, exclude }),
|
|
182
|
-
}
|
|
157
|
+
return { ...shared, report: gateReport({ base, docsSource: source, checkFiles, skipped: shared.skipped, anchors, paragraphs, paragraphMax }) }
|
|
183
158
|
}
|
|
184
159
|
|
|
185
|
-
const total = hits.total
|
|
186
160
|
const out = [
|
|
187
161
|
"文档纪律机检(报告态 · 非门禁)",
|
|
188
|
-
`射程:${base} · ${
|
|
189
|
-
"
|
|
190
|
-
|
|
162
|
+
`射程:${base} · 传入清单 ${checkFiles.length} 档 · 阈值 ${paragraphMax} 字符(工具内置常量)· 区带豁免:${paragraphExemptZones.length ? paragraphExemptZones.join(" ∥ ") : "无"}`,
|
|
163
|
+
"声明:判据参数 = 工具内置常量;范围 = 传入文件(判定范围 = 解析范围)",
|
|
164
|
+
`范围:传入清单 ${checkFiles.length} 档(整档入判 · 判定范围 = 解析范围)`,
|
|
165
|
+
`命中 ${hits.total} 条(锚 ${anchors.hits.length} · 段太长 ${paragraphs.filter((h) => h.clazz === "段太长").length} · 行太长 ${paragraphs.filter((h) => h.clazz === "行太长").length})`,
|
|
191
166
|
]
|
|
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
167
|
for (const h of anchors.hits) {
|
|
202
168
|
out.push(h.clazz === "档缺"
|
|
203
169
|
? `✗ 锚·档缺 ${rel(base, h.file)}:${h.line} \`${h.token}\``
|
|
204
170
|
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
205
171
|
}
|
|
206
|
-
for (const w of
|
|
207
|
-
out.push(total ? `结论:${total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥
|
|
172
|
+
for (const w of paragraphs) out.push(paragraphHitLine(base, w))
|
|
173
|
+
out.push(hits.total ? `结论:${hits.total} 条命中——报告态,不作拒执行的理由` : "结论:0 条命中(锚 ∥ 段太长 ∥ 行太长 均清)")
|
|
208
174
|
|
|
209
175
|
return { ...shared, report: out.join("\n") }
|
|
210
176
|
}
|