@thincoder/core 0.10.12 → 0.11.1
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 +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
* agent-tools/batch-skeleton.mjs — 批次档骨架模板 + 状态行机械形态单源(KD-4 拆分档)。
|
|
3
3
|
*
|
|
4
4
|
* 本档是纯常量/纯函数单源(零副作用、零 fs):`batch` 工具的 create 骨架、STATUS_WORDS
|
|
5
|
-
*
|
|
5
|
+
* 分段词表、状态行形态与解析、轮次块标题形态与计数全部只住这里——其他档 import 消费,**不复制字面**
|
|
6
6
|
* (BATCH-RECORD.md §4.10 / §4.12 / D-BR19;轮 2 #8——骨架占位状态行由 STATUS_WORDS §1 项
|
|
7
7
|
* 引用生成,全仓无第二份独立词表字面)。
|
|
8
8
|
*
|
|
9
|
-
* 状态行解析(readBatchStatusLine
|
|
10
|
-
*
|
|
11
|
-
* 生命周期三 action 的判定字面零漂移。
|
|
9
|
+
* 状态行解析(readBatchStatusLine)住本档:冻结门与 lifecycle(status/close 写入、在飞批扫描)消费
|
|
10
|
+
* **同一个解析器**——解析单源即 gate 与三 action 的判定字面零漂移。
|
|
12
11
|
*
|
|
13
|
-
* 段标题定位(sectionHeaderRe)同住本档:append 段尾定位与 status/close
|
|
12
|
+
* 段标题定位(sectionHeaderRe)同住本档:append 段尾定位与 status/close 段头区状态行定位
|
|
14
13
|
* 消费**同一个段头正则**——`## §N` 形态单源(`## §20` 不误命中 §2)。
|
|
15
14
|
*
|
|
16
15
|
* F11-C:**骨架死占位枚举**(TEMPLATE_PLACEHOLDERS)与残留扫描纯函数(findPlaceholderResidue——
|
|
@@ -20,24 +19,39 @@
|
|
|
20
19
|
* 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。
|
|
21
20
|
*/
|
|
22
21
|
export const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
|
|
22
|
+
|
|
23
|
+
/** §1 段头正则(单源——`readBatchStatusLine` 与 `isBatchRecord` 共用;`## §10` 不误命中 §1)。 */
|
|
24
|
+
const SECTION_1_HEADER_RE = /^## §1(?=\s|$)/m
|
|
25
|
+
/** 批档谓词(只读 · 零 fs):档内有 §1 段头 ⇒ 批次档——判别式 = 上面那条同一正则(「是不是批档」与
|
|
26
|
+
* 「§1 状态行是什么」两问不各持一份);消费面 = 台账核销机检(`ledger-close-proof.mjs`)。 */
|
|
27
|
+
export function isBatchRecord(src) {
|
|
28
|
+
return typeof src === "string" && SECTION_1_HEADER_RE.test(src)
|
|
29
|
+
}
|
|
23
30
|
/**
|
|
24
31
|
* STATUS_WORDS——状态行合法关键字**分段词表常量单源**(D-BR19 / fix 轮 #1;轮 2 #3 裁定②):
|
|
25
32
|
* - §1 项 = gate 词表 `{ open: 进行中, closed: 已收口 }`——**gate 只消费 §1 项**(单源仍成立);
|
|
26
33
|
* - §2/§3/§5 各带段内生命周期词表(`done` 词 = 段属主的完成态关键字);
|
|
27
34
|
* - §4/§6 无项——主 agent 的 status 写域**仅 §1**(§4/§6 状态面走普通文档写,与 D-BR18 分工一致)。
|
|
28
|
-
* 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* 全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
|
|
35
|
+
* 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status)拒绝内嵌另一冻结词的
|
|
36
|
+
* 值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。emoji / 括注 / 日期后缀容忍
|
|
37
|
+
* (**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词自本批起收紧为「剥装饰白名单后余核 = 关键词」,
|
|
38
|
+
* 见 batch-lifecycle.mjs);全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
|
|
33
39
|
*/
|
|
34
40
|
export const STATUS_WORDS = Object.freeze({
|
|
35
41
|
1: Object.freeze({ open: "进行中", closed: "已收口" }),
|
|
36
42
|
2: Object.freeze({ open: "进行中", done: "设计完成" }),
|
|
37
|
-
3: Object.freeze({ open: "进行中", done: "
|
|
43
|
+
3: Object.freeze({ open: "进行中", done: "评审通过" }),
|
|
38
44
|
5: Object.freeze({ open: "进行中", done: "实施完成" }),
|
|
39
45
|
})
|
|
40
46
|
|
|
47
|
+
/** 段状态行**旧字面兼容别名表**(只用于**读侧**:写侧词表零别名——旧字面写不进去,词表外拒)。
|
|
48
|
+
* §3 自 2026-10-27 起只留一个完成词 `评审通过`(只有 pass 叫完成;`changes-required` 不写状态词——
|
|
49
|
+
* 「评审没通过」= 评审未完成)。存量批档里的旧字面 `评审完成` 读侧等义于 `评审通过`(存量档**不回填**
|
|
50
|
+
* ——已冻结档不改字节)。**退场条件**:盘上再无该字面(判据 = `grep -r '评审完成' docs/batches`)。 */
|
|
51
|
+
export const STATUS_WORD_ALIASES = Object.freeze({
|
|
52
|
+
3: Object.freeze({ "评审完成": "评审通过" }),
|
|
53
|
+
})
|
|
54
|
+
|
|
41
55
|
/** 身份 → 可写段号(append 段白名单的**子代理面**——原 batch-segment.mjs 逐字迁移;主 agent
|
|
42
56
|
* 的 depth-0 白名单(模式感知——工程 §1/§4/§6 · §1 ∥ 普通 §1/§2/§4/§5/§6 · §1/§2/§5,§4.16)
|
|
43
57
|
* 由身份判据在 batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价
|
|
@@ -60,6 +74,8 @@ export const DEPTH0_SEGMENTS = Object.freeze({
|
|
|
60
74
|
/** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
|
|
61
75
|
export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
|
|
62
76
|
|
|
77
|
+
const ZONE_END_RE = /^#{2,}/ // 段头区界:任意 `##` / `###` 起的行(写面与触发读面同一条判据)
|
|
78
|
+
|
|
63
79
|
/**
|
|
64
80
|
* 骨架死占位**枚举单源**(F11-C 判据——create 骨架产出过的全部死占位字面):
|
|
65
81
|
* - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
|
|
@@ -78,10 +94,9 @@ const HEADER_STRUCTURAL_RE = [/^#\s/, /^>\s*编制/, /^>\s*台账/]
|
|
|
78
94
|
|
|
79
95
|
/**
|
|
80
96
|
* 死占位残留扫描(F11-C——append/status 落笔前的机检;**纯函数 · 零 fs**)。
|
|
81
|
-
* 判定域 = **档头结构行**(区域 = 档首至首个 `## §N` 段头前;值形 = 骨架三类结构行:标题行
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* §6.20;原「档头 + 目标段」全域子串扫描分不清「未填占位」与「正文引用」——本批 §1 首投实害)。
|
|
97
|
+
* 判定域 = **档头结构行**(区域 = 档首至首个 `## §N` 段头前;值形 = 骨架三类结构行:标题行 `# …` / 编制行
|
|
98
|
+
* `> 编制` / 台账行 `> 台账`)——行内出现枚举字面 ⇒ 残留;段体(首段头起)任意位置出现字面 = 正文引用 ⇒
|
|
99
|
+
* **豁免**(引用非未填——2026-10-05 判据可辨性收正,`TOOLS.md` §6.20;原全域子串扫描分不清两者——本批 §1 首投实害)。
|
|
85
100
|
* @param {string} src — 档全文
|
|
86
101
|
* @returns {Array<{line: number, text: string}>} 残留列表(空 = 无残留);
|
|
87
102
|
* `line` = 档头域内 1-based 行号(档头域自档首行起——便于定位),`text` = 命中行 trim 原文
|
|
@@ -113,20 +128,16 @@ export function placeholderResidueError(residues) {
|
|
|
113
128
|
|
|
114
129
|
/**
|
|
115
130
|
* §1 状态行解析(冻结门与 lifecycle 共用的**单源解析器**——原 batch-segment.mjs 逐字迁移,
|
|
116
|
-
* 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1
|
|
117
|
-
* `**状态行**:`
|
|
118
|
-
*
|
|
131
|
+
* 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1 **段头区**内
|
|
132
|
+
* `**状态行**:` 前缀行(`## §1` 标题之后、下一 `^#{2,}` 之前——与写面同一条定位面;
|
|
133
|
+
* 段体 / 轮次块里引述的状态行不参与判定)。判定只认关键字(emoji / 括号装饰 / 日期后缀容忍):含「已收口」→ "closed"
|
|
119
134
|
* (冻结优先)、含「进行中」→ "open";无 §1 / 无该行 / 两关键字皆不命中 → "unknown"
|
|
120
135
|
* (fail-closed 视为冻结)。
|
|
121
136
|
*/
|
|
122
137
|
export function readBatchStatusLine(src) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
const
|
|
126
|
-
nextRe.lastIndex = s1.index + s1[0].length
|
|
127
|
-
const next = nextRe.exec(src)
|
|
128
|
-
const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
|
|
129
|
-
const m = body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
|
|
138
|
+
const lines = sectionHeaderZoneLines(src, 1) // 段头单源(段头缺失 ⇒ null;与 isBatchRecord 同一段头正则)
|
|
139
|
+
if (lines === null) return "unknown"
|
|
140
|
+
const m = lines.map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
|
|
130
141
|
if (!m) return "unknown"
|
|
131
142
|
const value = m[1].trim()
|
|
132
143
|
if (value.includes(STATUS_WORDS[1].closed)) return "closed"
|
|
@@ -135,11 +146,45 @@ export function readBatchStatusLine(src) {
|
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
149
|
+
* 段内状态行**单行改写**(status / close / 重开三处共用——改写面之一,域限状态行;另两处 = §2 段体区间改写
|
|
150
|
+
* (`edit`,§4.19)∥ 重开追记行(§4.18-B),三处都不动其余字节):定位 `## §N` 段
|
|
151
|
+
* (sectionHeaderRe 单源),**段头区**(标题行之后、下一 `##` / `###` 起行之前)内首命中行原位替换(行尾随命中行
|
|
152
|
+
* 自身);区内无该行 ⇒ 沿用既有插入形(状态行 + 两空行,段头后原有空行不动;插入块行尾 = 标题行自身行尾)。
|
|
153
|
+
* 区外正文(轮次块引述的 `**状态行**:…` 行)字节零变——住所只有段头区,§1–§5 同一规则,无按段分支。
|
|
154
|
+
* 字节零变(行切分 = /\r?\n/:EOL 形态无关);段标题缺失 ⇒ throw(create 是骨架唯一权威入口);纯字符串变换(零 fs)。
|
|
155
|
+
* @param {string} src — 档全文
|
|
156
|
+
* @param {number|string} seg — 段号
|
|
157
|
+
* @param {string} value — 状态行值(`**状态行**:<value>`)
|
|
158
|
+
* @returns {string} 改写后全文
|
|
159
|
+
*/
|
|
160
|
+
export function updateSectionStatusLine(src, seg, value) {
|
|
161
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
162
|
+
if (!hdr) {
|
|
163
|
+
throw new Error(`batch: the batch record has no "## §${seg}" section header — cannot update its status line (create is the skeleton's only authoritative entry; append/status never add one). Nothing was written.`)
|
|
164
|
+
}
|
|
165
|
+
const nextRe = /^## §\d/gm
|
|
166
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
167
|
+
const next = nextRe.exec(src)
|
|
168
|
+
const endIdx = next ? next.index : src.length
|
|
169
|
+
const line = `**状态行**:${value}`
|
|
170
|
+
const parts = src.slice(hdr.index, endIdx).split(/(\r?\n)/) // 保留分隔符(奇数下标 = 行尾)——替换只动命中行
|
|
171
|
+
const zoneEnd = parts.findIndex((p, i) => i >= 2 && ZONE_END_RE.test(p)) // 段头区界下标(0 = 段头行自身,不判)
|
|
172
|
+
const limit = zoneEnd < 0 ? parts.length : zoneEnd
|
|
173
|
+
const idx = parts.findIndex((p, i) => i < limit && STATUS_LINE_RE.test(p))
|
|
174
|
+
if (idx >= 0) { parts[idx] = line } else {
|
|
175
|
+
const eol = parts[1] === "\r\n" ? "\r\n" : "\n" // 缺行插入块行尾 = 标题行自身行尾
|
|
176
|
+
if (parts.length === 1) parts.splice(1, 0, eol, line, eol, "", eol, "")
|
|
177
|
+
else parts.splice(2, 0, line, eol, "", eol, "", eol)
|
|
178
|
+
}
|
|
179
|
+
return src.slice(0, hdr.index) + parts.join("") + src.slice(endIdx)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* §N 段状态词解析(读面单源,设计档 §2.5 的单源表):段头(sectionHeaderRe)/ 状态行正则(STATUS_LINE_RE)/
|
|
184
|
+
* 词表(STATUS_WORDS)全消费既有单源常量——与冻结门字面零漂移。**取值区只有一个 = 段头区**(`## §N` 标题行
|
|
185
|
+
* 之后、下一 `^#{2,}` 之前,与写面 `updateSectionStatusLine` 同一条定位面);区外正文引述的 `**状态行**:…`
|
|
186
|
+
* 既不被改写,也不构成结论。区内命中按**终态词优先**(closed / done 先于 open——镜像冻结门取值序)⇒ 返回该关键词;
|
|
187
|
+
* 段头缺失 / 无该行 / 无命中 ⇒ null。`§4` / `§6` 无词条 ⇒ 字面 `无状态词`(不造词)。
|
|
143
188
|
* @param {string} src — 档全文
|
|
144
189
|
* @param {number|string} seg — 段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
|
|
145
190
|
* @returns {string|null} 已识别关键词 / null / 字面「无状态词」(§4 / §6)
|
|
@@ -147,15 +192,15 @@ export function readBatchStatusLine(src) {
|
|
|
147
192
|
export function readSectionStatusWord(src, seg) {
|
|
148
193
|
const words = STATUS_WORDS[seg]
|
|
149
194
|
if (!words) return "无状态词"
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
152
|
-
const
|
|
153
|
-
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
154
|
-
const next = nextRe.exec(src)
|
|
155
|
-
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
156
|
-
const m = body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
|
|
195
|
+
const lines = sectionHeaderZoneLines(src, seg)
|
|
196
|
+
if (lines === null) return null
|
|
197
|
+
const m = lines.map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
|
|
157
198
|
if (!m) return null
|
|
158
199
|
const value = m[1].trim()
|
|
200
|
+
// 旧字面先映到规范词(读侧等义——写侧词表零别名)
|
|
201
|
+
for (const [alias, canonical] of Object.entries(STATUS_WORD_ALIASES[seg] ?? {})) {
|
|
202
|
+
if (value.includes(alias)) return canonical
|
|
203
|
+
}
|
|
159
204
|
for (const w of [words.closed, words.done, words.open]) {
|
|
160
205
|
if (w !== undefined && value.includes(w)) return w
|
|
161
206
|
}
|
|
@@ -163,15 +208,174 @@ export function readSectionStatusWord(src, seg) {
|
|
|
163
208
|
}
|
|
164
209
|
|
|
165
210
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
211
|
+
* 轮次块标题形态(**写面与读面的单源正则**):§3 的轮次块只由工具戳
|
|
212
|
+
* `### 轮次 N(评审子代理)` 标题——写入面 `batch.mjs` 丢弃调用方自带的同名行、按本形态计数
|
|
213
|
+
* 生成 N;读面(结算点 §3 落地门的轮次块计数)读同一形态。两份正则必漂,故只留这一处
|
|
214
|
+
* (`docs/core/design/ADVISOR-GUARDS.md` §1.1 判据 1)。`### 轮次与发现(…)` 骨架行不匹配。
|
|
215
|
+
*/
|
|
216
|
+
export const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* §N 段内轮次块计数(纯函数 · 零 fs):段体逐行测 `ROUND_HEADING_RE`。段头缺失 ⇒ 0
|
|
220
|
+
* (无 §3 段 = 评审写不进 §3 ⇒ 结算面判 `no-section3-block`,不是「跳过」)。
|
|
221
|
+
* 消费点两处、同一形态:`batch.mjs`(写入面生成下一轮 N)∥ `design-token.mjs`
|
|
222
|
+
* (结算点判「§3 已被本轮写过」——块数 > 点火基线)。
|
|
223
|
+
* @param {string} src — 批档全文
|
|
224
|
+
* @param {number|string} [seg] — 段号(缺省 3——轮次块只落 §3)
|
|
225
|
+
* @returns {number} 段内轮次块数
|
|
226
|
+
*/
|
|
227
|
+
export function countRoundBlocks(src, seg = 3) {
|
|
228
|
+
if (typeof src !== "string") return 0
|
|
229
|
+
const lines = sectionBodyLines(src, seg)
|
|
230
|
+
if (lines === null) return 0
|
|
231
|
+
return lines.filter((l) => ROUND_HEADING_RE.test(l)).length
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* §N 段内**最后一个轮次块的文本**(纯函数 · 零 fs):块首行 = 该 `### 轮次 N(评审子代理)` 标题行,
|
|
236
|
+
* 块尾 = 段体末尾——本函数只取最后一块,故不需要「到下一块止」的切分(后随行都属该块)。
|
|
237
|
+
* 消费面 = `§5` 写入门判据 2:「门所依赖的那个块,自己有没有说结论」(设计档
|
|
238
|
+
* `BATCH-LEDGER-REVIEW-UNIFY.md` §2.6)。
|
|
239
|
+
* **只切块、不判裁定**:裁定一律由两条既有锚判——`VERDICT_CHANGES_LINE`(本档)∥ `VERDICT_PASS_LINE`
|
|
240
|
+
* (本档锚块),判定面不在这里。切块形态消费 `ROUND_HEADING_RE`
|
|
241
|
+
* (与 `countRoundBlocks` / `nextRoundNumber` 同一份正则,不另写一套)。
|
|
242
|
+
* @param {string} src — 批档全文
|
|
243
|
+
* @param {number|string} [seg] — 段号(缺省 3——轮次块只落 §3)
|
|
244
|
+
* @returns {string|null} 末块文本(含块首行)∥ `null`(段头缺失 ∥ 段内无块)
|
|
245
|
+
*/
|
|
246
|
+
export function lastRoundBlock(src, seg = 3) {
|
|
247
|
+
if (typeof src !== "string") return null
|
|
248
|
+
const lines = sectionBodyLines(src, seg)
|
|
249
|
+
if (lines === null) return null
|
|
250
|
+
let start = -1
|
|
251
|
+
for (let i = 0; i < lines.length; i++) if (ROUND_HEADING_RE.test(lines[i])) start = i
|
|
252
|
+
return start < 0 ? null : lines.slice(start).join("\n")
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 段内**下一轮**轮次号(`countRoundBlocks + 1` 的组合名——**不二次实现计数**):写入面
|
|
257
|
+
* (`batch.mjs` 的 `insertIntoSection` 盖戳 `### 轮次 N`)与打回重置(`statusResetPlan` 的留痕行
|
|
258
|
+
* 「第 N 轮打回」)必须取同一个 N——各算一遍,将来改了 `ROUND_HEADING_RE` 就会一处变一处不变。
|
|
259
|
+
* @param {string} src — 批档全文(写盘前)
|
|
260
|
+
* @param {number|string} [seg] — 段号(缺省 3——轮次块只落 §3)
|
|
261
|
+
* @returns {number} 本轮 N(1 起)
|
|
262
|
+
*/
|
|
263
|
+
export function nextRoundNumber(src, seg = 3) {
|
|
264
|
+
return countRoundBlocks(src, seg) + 1
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 裁定行锚(**写入面与签发面共读一条正则**,2026-10-28 自 `design-token.mjs` 迁入):打回重置
|
|
269
|
+
* (`statusResetPlan`)判「本笔带不带打回裁定」,设计凭证裁定闸(`design-token.mjs` 的
|
|
270
|
+
* `verdictGateFailure`)判「回执给没给打回」,两处读同一份锚形——锚只此一份,不复制到第二档、
|
|
271
|
+
* 也不由两档各写一遍。四条锚(`_V_PRE` / `_V_SUF` / `VERDICT_CHANGES_LINE` / `VERDICT_PASS_LINE`)全住本档锚块——
|
|
272
|
+
* 签发面、`§5` 写入门与通过置位经 import 同读,零复制(pass 锚迁入:置位判据住本档,留在签发面就要回 import 而成环)。
|
|
273
|
+
* 本地化等价形(GitHub #32 / #14):评审席用中文回执很常见,只认英文会让 pass 落成 `no-pass-line`、
|
|
274
|
+
* 凭证永不签发;判据仍只认**整行逐字面**,不做「句中含『通过』」这类模糊匹配——那样会把「未通过」
|
|
275
|
+
* 也放过(散文行内提及不触发)。
|
|
276
|
+
* **两个中文分支的尾句号可省可带**(`结论:通过。` ∥ `VERDICT: 通过。` 同判 pass,打回侧同):同一个锚、
|
|
277
|
+
* 两条分支,对同一语义必须给同一结论——`VERDICT: 通过。` 判是而 `结论:通过。` 判否是锚内的不对称;
|
|
278
|
+
* 打回侧缺它则 `结论:不通过。` 读成「无裁定行」⇒ `append §3` 误拒一笔真打回,且打回重置不触发。
|
|
279
|
+
* 英文形不带句号(`VERDICT: pass。` 不认,与改前一致);混合形(`**结论** = VERDICT: pass`)不是句号问题,
|
|
280
|
+
* 是把两种写法混成第三种,仍不认。
|
|
281
|
+
*/
|
|
282
|
+
export const _V_PRE = "^[ \\t]*[*_`]*[ \\t]*"
|
|
283
|
+
export const _V_SUF = "[ \\t]*[*_`]*[ \\t]*$"
|
|
284
|
+
export const VERDICT_CHANGES_LINE = new RegExp(
|
|
285
|
+
_V_PRE + "(?:VERDICT:[ \\t]*(?:changes-required|不通过。?|需修改。?)|结论[::][ \\t]*(?:不通过。?|需修改。?|changes-required))" + _V_SUF, "mi")
|
|
286
|
+
/** pass 裁定行锚(与打回锚同处同形):签发面 ∥ `§5` 写入门判据 2 ∥ 通过置位三处同读;`design-token.mjs` 原样再导出。
|
|
287
|
+
* 两锚的组合序只住下面一个函数 `verdictGapOf`——三个消费面经它取值,谁也不自写一遍顺序。 */
|
|
288
|
+
export const VERDICT_PASS_LINE = new RegExp(
|
|
289
|
+
_V_PRE + "(?:VERDICT:[ \\t]*(?:pass|通过。?)|结论[::][ \\t]*(?:通过。?|pass))" + _V_SUF, "mi")
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 裁定读数(**判据单源**——设计档 `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.6.1):值域四格 =
|
|
293
|
+
* `null`(说了 pass)∥ `「无裁定行」` ∥ `「VERDICT: changes-required」` ∥ `「两者同携」`;判定序
|
|
294
|
+
* **先打回、后 pass**(两行同携 ⇒ 判打回——同一份回执在两个面上不能结论相反)。
|
|
295
|
+
* 三个消费面同读这一个函数,各自只做自己那一层的映射,不再各写一遍组合序:
|
|
296
|
+
* - `§5` 写入门判据 2(`batch-closeout.mjs` 的 `lastRoundVerdictGap`)——原样取四格;
|
|
297
|
+
* - `append §3` 写入闸(`batch.mjs`)——只拒 `「无裁定行」` ∥ `「两者同携」`,判打回的一笔照写;
|
|
298
|
+
* - 签发面(`design-token.mjs` 的 `verdictGateFailure`)——四格映回它原有的两个词
|
|
299
|
+
* (`changes-required` ∥ `no-pass-line`),对外说法零改。
|
|
300
|
+
* 入参是**文本**而不是块:写面手上是「本笔净化后正文」,落档的就是它(每笔 `§3` 自成一块)——为复用切块
|
|
301
|
+
* 函数把未落盘文本伪装成块,会把「段内无块」的 `null` 语义带进写面。
|
|
302
|
+
* @param {string|null|undefined} text — 一段文本(本笔净化后正文 ∥ 末块正文 ∥ 评审回执)
|
|
303
|
+
* @returns {string|null} 上方四格之一;非串 ⇒ `「无裁定行」`(无文本即无裁定,fail-closed)
|
|
304
|
+
*/
|
|
305
|
+
export function verdictGapOf(text) {
|
|
306
|
+
if (typeof text !== "string") return "无裁定行"
|
|
307
|
+
const changes = VERDICT_CHANGES_LINE.test(text)
|
|
308
|
+
const pass = VERDICT_PASS_LINE.test(text)
|
|
309
|
+
if (changes) return pass ? "两者同携" : "VERDICT: changes-required"
|
|
310
|
+
return pass ? null : "无裁定行"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 打回即重置的计划形(**纯函数 · 零 fs**;`batch.mjs` 与 `insertIntoSection` 在同一笔写盘里消费)。三合取全中
|
|
315
|
+
* ⇒ `{ roundN, statusValue, traceLine }`,否则 `null`(零写 = 零回归):① 本笔写 §3;② 本笔**净化后**正文命中
|
|
316
|
+
* 打回裁定行(`VERDICT_CHANGES_LINE` 单源);③ 写这一笔之前 §3 **段头区**状态行 = `STATUS_WORDS[3].done`
|
|
317
|
+
* (**取词表,不写死字面**——写死则词表一改这条判据静默失效;读区与写面同一条定位面)。
|
|
318
|
+
* 为什么必须机械:状态行是下游 `§5` 写入门 ∥ 收口门 ∥ 核销门③ 的唯一判据面;「评审记得把状态行改回去」
|
|
319
|
+
* 是一条纪律,而纪律在打回那一轮最易漏。为什么第 ③ 条要「上一轮 = done」而非无条件:§3 已是 `进行中`
|
|
320
|
+
* (本来写着「评审未完成」)⇒ 重置无事可做,零写零留痕;顺序交接的批(首轮 pass)因此不误伤。
|
|
321
|
+
* 状态行值 = 关键词 + 括注(**不新增第三态词**:词表零扩即读侧零改);留痕行把「上一轮 pass 被这一轮推翻」
|
|
322
|
+
* 留在 §3 段体——重置不等于清空,否则读档的人会以为从没评过。权威规格 = `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.5。
|
|
323
|
+
* @param {string} src — 写盘前的档全文
|
|
324
|
+
* @param {number|string} seg — 本笔的目标段号
|
|
325
|
+
* @param {string} body — 本笔净化后正文(凭证已剥、伪造戳已丢)
|
|
326
|
+
* @param {string} date — 留痕行的日期戳 `YYYY-MM-DD`(调用方取 `todayLocal`——日期口径只一处)
|
|
327
|
+
* @returns {{roundN:number, statusValue:string, traceLine:string}|null}
|
|
328
|
+
*/
|
|
329
|
+
export function statusResetPlan(src, seg, body, date) {
|
|
330
|
+
if (Number(seg) !== 3) return null
|
|
331
|
+
if (typeof body !== "string" || !VERDICT_CHANGES_LINE.test(body)) return null
|
|
332
|
+
if (readSectionStatusWord(src, 3) !== STATUS_WORDS[3].done) return null
|
|
333
|
+
const roundN = nextRoundNumber(src, 3)
|
|
334
|
+
const { open, done } = STATUS_WORDS[3]
|
|
335
|
+
return {
|
|
336
|
+
roundN,
|
|
337
|
+
statusValue: `${open}(第 ${roundN} 轮打回)`,
|
|
338
|
+
traceLine: `> 状态行重置:第 ${roundN} 轮打回(changes-required)· 上一轮状态行 = ${done} → ${open} · ${date}`,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 通过即置位(**纯函数 · 零 fs**;与 `statusResetPlan` 同签名同位置、同写面)= 「打回即重置」的对称另一半。三合取
|
|
344
|
+
* ① 本笔写 §3 ② 净化后正文命中 `VERDICT_PASS_LINE` **且不命中** `VERDICT_CHANGES_LINE` ③ 写前 §3 **段头区**状态行
|
|
345
|
+
* ≠ `STATUS_WORDS[3].done`(读区与写面同一条定位面——区外引述行不构成结论)⇒ `{ roundN, statusValue }`,否则 `null`。
|
|
346
|
+
* 为什么必须机械:结算面(`design-token.mjs` 判据 2)读这一行,≠ 评审通过 就拒签——「评审记得补一次 `status`」是
|
|
347
|
+
* 纪律,漏一次白跑一轮评审。为什么 ③ 与打回反向:打回要「上一轮 = done」(推翻已落的字),通过要「当前 ≠ done」
|
|
348
|
+
* (补上没落的字)⇒ 两条计划天然互斥,不另立「同现即拒」判据面;② 排除打回行则因同携两行是自相矛盾的回执,签发面
|
|
349
|
+
* 先打回后 pass ⇒ 同判打回(状态行与门同结论)。不写留痕行:块自己说了 pass,落进 §3 的块即记录(打回留痕是因为
|
|
350
|
+
* 重置**抹掉**了一条已落的 pass);`roundN` / `date` 无消费方,保留 = 同签名同形。权威规格 = `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.5。
|
|
351
|
+
* @param {string} src — 写盘前的档全文
|
|
352
|
+
* @param {number|string} seg — 本笔的目标段号
|
|
353
|
+
* @param {string} body — 本笔净化后正文(凭证已剥、伪造戳已丢)
|
|
354
|
+
* @param {string} date — 与 `statusResetPlan` 同签名(本方向无留痕行,不消费)
|
|
355
|
+
* @returns {{roundN:number, statusValue:string}|null}
|
|
356
|
+
*/
|
|
357
|
+
export function statusPassPlan(src, seg, body, date) {
|
|
358
|
+
if (Number(seg) !== 3) return null
|
|
359
|
+
if (typeof body !== "string" || !VERDICT_PASS_LINE.test(body)) return null
|
|
360
|
+
if (VERDICT_CHANGES_LINE.test(body)) return null
|
|
361
|
+
if (readSectionStatusWord(src, 3) === STATUS_WORDS[3].done) return null
|
|
362
|
+
return { roundN: nextRoundNumber(src, 3), statusValue: STATUS_WORDS[3].done }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 「不适用」声明行形态(**声明机制已退役**——2026-11-02 用户裁定封掉这条口子):形 = `> 不适用:§N —— <理由>`,
|
|
367
|
+
* 理由非空(`\S.*` = 至少一个非空白字符起头);识别域 = §1 段体。退役后本正则只服务**痕迹检测**
|
|
368
|
+
* (`malformedInapplicableLine`):`§1` 里还写着声明行的人当场被拒并读到「这条路没有了」,而不是静默当作没写。
|
|
369
|
+
* 删掉它,残留的声明行就成了一句无人过问的散文——写它的人不会知道自己抄的是一条已停用的路。
|
|
170
370
|
*/
|
|
171
371
|
const INAPPLICABLE_RE = /^>\s*不适用[::]\s*§(\d+)\s*——\s*(\S.*)$/
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
372
|
+
/** 可声明的段号集 = **空集**(声明机制退役):任何段都不得声明不适用——§2 设计 / §3 评审 / §4 批准 /
|
|
373
|
+
* §6 验证各段一律要有内容,§5 段号豁免不参与链门。清空这一个常量即达成判据:窄网(`INAPPLICABLE_RE`)
|
|
374
|
+
* 与宽网(`INAPPLICABLE_PREFIX_RE`)之下,凡 `§1` 出现声明行都是非法行;段内容判据只剩「有内容 / 没内容」
|
|
375
|
+
* 两态。旧的可读面函数与 `not-applicable` 分支已随本批删除——不另立「恒 false 的插座」,插座留着就会被
|
|
376
|
+
* 重新接上。 */
|
|
377
|
+
export const INAPPLICABLE_SEGMENTS = []
|
|
378
|
+
/** §1 声明行的识别前缀(痕迹扫描的宽网——行首命中它即违规,不再区分形合规与否)。 */
|
|
175
379
|
const INAPPLICABLE_PREFIX_RE = /^>\s*不适用/
|
|
176
380
|
|
|
177
381
|
/** §N 段体行集(段头缺失 ⇒ null——create 是骨架唯一权威入口;行切分 = /\r?\n/——EOL 形态无关)。 */
|
|
@@ -186,32 +390,33 @@ function sectionBodyLines(src, seg) {
|
|
|
186
390
|
return (nl >= 0 ? body.slice(nl + 1) : "").split(/\r?\n/)
|
|
187
391
|
}
|
|
188
392
|
|
|
393
|
+
/** §N **段头区**行集(段级状态行的唯一住所:`## §N` 标题行之后、下一个 `##` / `###` 起行之前;段头缺失 ⇒ null)。 */
|
|
394
|
+
function sectionHeaderZoneLines(src, seg) {
|
|
395
|
+
const lines = sectionBodyLines(src, seg)
|
|
396
|
+
if (lines === null) return null
|
|
397
|
+
const end = lines.findIndex((l) => ZONE_END_RE.test(l))
|
|
398
|
+
return end < 0 ? lines : lines.slice(0, end)
|
|
399
|
+
}
|
|
400
|
+
|
|
189
401
|
/**
|
|
190
402
|
* 段内容判据**单源**(§4.18-A closeout chain gate 的判空口径;D-BR30 判据单源——**不许两套**;
|
|
191
403
|
* §4.13 的 §6 旧判据 `sectionSixHasContent` 改由本谓词承担——§6 为唯一缺段时拒串仍取旧函数)。
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
404
|
+
* 返回**两态**:`content`(段体至少一行非排除类)∥ `empty`。旧第三态 `not-applicable` 随声明机制退役
|
|
405
|
+
* (2026-11-02:任何段都不得声明不适用 ⇒ 这条事实没有第二个满足面,判据只剩「有内容 / 没内容」)。
|
|
406
|
+
* 排除类(判空依据——「标题在、正文零行」判空):空行 ∥ 标题行(`^#{1,6}\s`)∥ `**状态行**:` 行 ∥
|
|
407
|
+
* `<§N 模板占位:…>` 行 ∥ 「不适用」声明行(判空口径零改——那类行是停用机制的痕迹,本来就不算内容)。
|
|
195
408
|
* 口径收正注:旧 `sectionSixHasContent` 对「只有子标题(`### …`)」判非空;本谓词按 §4.18-A 判空
|
|
196
409
|
* (子标题属排除类标题行)——设计档 §4.18-A 明文收窄。
|
|
197
410
|
* @param {string} src — 档全文
|
|
198
411
|
* @param {number|string} seg — 段号(§1–§6;§1 恒 `content`——主 agent 自述段不设前置)
|
|
199
|
-
* @returns {"content"|"
|
|
412
|
+
* @returns {"content"|"empty"} 段内容两态
|
|
200
413
|
*/
|
|
201
414
|
export function sectionContentState(src, seg) {
|
|
202
415
|
const n = Number(seg)
|
|
203
416
|
if (n === 1) return "content"
|
|
204
417
|
const lines = sectionBodyLines(src, n)
|
|
205
418
|
if (lines === null) return "empty" // 段头缺失 = 空(骨架由 create 一次预写;缺失即无内容)
|
|
206
|
-
|
|
207
|
-
// 段体无实质内容 ⇒ 查 §1 声明(仅 §2/§3/§4 可声明;理由非空——INAPPLICABLE_RE 的 (\S.*) 已保证)
|
|
208
|
-
if (INAPPLICABLE_SEGMENTS.includes(n)) {
|
|
209
|
-
for (const l of sectionBodyLines(src, 1) ?? []) {
|
|
210
|
-
const m = INAPPLICABLE_RE.exec(l)
|
|
211
|
-
if (m && Number(m[1]) === n) return "not-applicable"
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return "empty"
|
|
419
|
+
return lines.some((l) => NONEMPTY_RE.test(l) && !EXCLUDED_LINE_RE.test(l)) ? "content" : "empty"
|
|
215
420
|
}
|
|
216
421
|
|
|
217
422
|
/** 非空白行谓词(空行排除)。 */
|
|
@@ -221,24 +426,24 @@ const NONEMPTY_RE = /\S/
|
|
|
221
426
|
const EXCLUDED_LINE_RE = /^#{1,6}\s|^\s*\*\*状态行\*\*[::]|^<§\d+ 模板占位[::]|^>\s*不适用/
|
|
222
427
|
|
|
223
428
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
429
|
+
* 声明行拒绝句(§4.18-A J3——**声明机制已退役**:`§1` 段体出现任何 `> 不适用` 行 ⇒ 拒):
|
|
430
|
+
* 拒文说清三件——声明这条路没有了 ∥ 每段各自要有内容(§5 豁免)∥ 点名违规行。
|
|
431
|
+
* 措辞不再给「合法声明形」——给形 = 给一条可照抄的路,而本批封的正是这条路。
|
|
227
432
|
* @param {string} line — 违规行原文
|
|
228
433
|
* @returns {string} 完整拒绝句(调用方 throw new Error(...))
|
|
229
434
|
*/
|
|
230
435
|
export function inapplicableDeclarationError(line) {
|
|
231
|
-
return "batch:
|
|
232
|
-
"
|
|
436
|
+
return "batch: `不适用` 声明已停用(inapplicability declaration retired)— no declaration is accepted: " +
|
|
437
|
+
"§2 / §3 / §4 / §6 must each carry content (§5 exempt). " +
|
|
233
438
|
`Offending line: ${line}. Nothing was written.`
|
|
234
439
|
}
|
|
235
440
|
|
|
236
441
|
/**
|
|
237
|
-
* §1
|
|
238
|
-
*
|
|
239
|
-
*
|
|
442
|
+
* §1 声明行机械扫描(链门前置判据——**出现即拒**,不静默):识别域 = §1 段体。
|
|
443
|
+
* 段号集 `INAPPLICABLE_SEGMENTS` 已清空 ⇒ `includes` 恒 false ⇒ 行首命中 `^>\s*不适用` 的每一行
|
|
444
|
+
* (形合规与否一律)都是违规行,返回首个。本函数现在是**停用机制的痕迹检测器**,不是合法形校验器。
|
|
240
445
|
* @param {string} src — 档全文
|
|
241
|
-
* @returns {string|null}
|
|
446
|
+
* @returns {string|null} 首个声明行原文 / null
|
|
242
447
|
*/
|
|
243
448
|
export function malformedInapplicableLine(src) {
|
|
244
449
|
for (const l of sectionBodyLines(src, 1) ?? []) {
|
|
@@ -268,18 +473,15 @@ export function sectionHasStatusLine(src, seg) {
|
|
|
268
473
|
}
|
|
269
474
|
|
|
270
475
|
/**
|
|
271
|
-
* create 骨架模板(BATCH-RECORD.md §4.10
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
|
|
281
|
-
* 模式取形(§4.16——普通模式变体**四处替换**、余行逐字同;`mode` 缺省 = "engineering" ⇒
|
|
282
|
-
* 工程产出逐字不变):① 汇总行 §2/§5 标签;② `## §2` 段头;③ §2 状态占位句;④ `## §5` 段头。
|
|
476
|
+
* create 骨架模板(BATCH-RECORD.md §4.10 行为规格的**代码单源**——红线:模板只覆盖骨架与占位,不模板化内容):
|
|
477
|
+
* `## §1–§6` 段头(含职责署名)+ 档头 boilerplate(**两句形**:六段一段一作者句 + 每段只有一个作者句(作者对本段可 `append` 加内容 ∥ 可 `edit` 就地改,§4.10「档头两句」/ §4.19),加编制行 + 台账/前情指针行)+ §1 段内
|
|
478
|
+
* **占位状态行**(含 gate 合法关键字「进行中」——建档即过 gate;整行不含「已收口」——gate 已收口优先,占位
|
|
479
|
+
* 行不得携带冻结词)+ 各段模板子标题占位。已知实参替换:date / topic / **source** / prev(§4.11 参数面 +
|
|
480
|
+
* F11-B——source 必填、prev 入值过幂等剥前缀,归一化在 lifecycle 面);`ledger` = **必填**(2026-10-27 起建档
|
|
481
|
+
* 同拍建台账行——真实行号由绑定事务回填,永不落回 `#<编号>` 占位);`board` 可选(未给 ⇒ 括注写 `—`——`board`
|
|
482
|
+
* 是分类不是身份,不拒)。本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
|
|
483
|
+
* 模式取形(§4.16——普通模式变体**四处替换**、余行逐字同;`mode` 缺省 = "engineering" ⇒ 工程产出逐字不变):
|
|
484
|
+
* ① 汇总行 §2/§5 标签;② `## §2` 段头;③ §2 状态占位句;④ `## §5` 段头。
|
|
283
485
|
*/
|
|
284
486
|
export function batchSkeleton({ date, topic, source, prev, ledger = null, board = null, mode = "engineering" }) {
|
|
285
487
|
const normal = mode === "normal"
|
|
@@ -287,9 +489,10 @@ export function batchSkeleton({ date, topic, source, prev, ledger = null, board
|
|
|
287
489
|
const implementerSeat = normal ? "实施者:主 agent ∥ coder" : "eng-coder" // ① 汇总行 §5 标签 ∥ ④ §5 段头
|
|
288
490
|
return [
|
|
289
491
|
`# ${date} · ${topic}`,
|
|
290
|
-
`>
|
|
492
|
+
`> 六段,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(${designerSeat})· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(${implementerSeat})· §6 验证与收口(父代理)。`,
|
|
493
|
+
`> 每段只有一个作者:作者的段可 \`append\` 加内容、可就地改(\`edit\`,§4.19);非作者一概拒。`,
|
|
291
494
|
`> 编制:主 agent · ${date} · 来源 = ${source}。`,
|
|
292
|
-
`> 台账 = ${ledger ?? "#<编号>"}(${board ?? "<板块>"} · 归批)。前情 = ${prev}。`,
|
|
495
|
+
`> 台账 = ${ledger ?? "#<编号>"}(${board ?? (ledger ? "—" : "<板块>")} · 归批)。前情 = ${prev}。`,
|
|
293
496
|
"## §1 讨论(主 agent)",
|
|
294
497
|
`**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
|
|
295
498
|
"<§1 模板占位:本批条目 / 关键判据 / 授权口径>",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch-unit-test-gate.mjs — 本批批内单测档门(收口前置门 · 单源叶档)。
|
|
3
|
+
*
|
|
4
|
+
* 权威规格 = `docs/core/design/TESTING.md` §3.5(判据 ∥ 门位 ∥ 报面 ∥ 用例表 ∥ 验收)——本档只实现,
|
|
5
|
+
* 不重述口径(D2)。业务判据一条:**收口时,本批自己的批内单测档仍在盘 ⇒ 拒收口**
|
|
6
|
+
*(`§3.1 ①` 零留存纪律的结构兜底:纪律句此前没有兜底结构,2026-10-26 实测 13 档全未删)。
|
|
7
|
+
*
|
|
8
|
+
* 归属判据(闭集 · 机判)——给定批档绝对路径 `abs`,目录 `D = dirname(abs)`,本批 stem `S` = 档名去 `.md`:
|
|
9
|
+
* ① 候选集 = `D` 下后缀 `.test.mjs` 的**文件**(目录不计);
|
|
10
|
+
* ② 候选 `c` 的**归属批档** = `D` 内所有 `.md` 档名去 `.md` 后为 `c` 文件名面前缀者中的**最长者**;
|
|
11
|
+
* ③ 本批的档 = 归属批档 = `S` 的候选;非空 ⇒ 拒。无主候选(无任何 `.md` 为其前缀)不判。
|
|
12
|
+
* 为什么最长前缀而非同名 = 本批实测反证(13 档里 1 档名与批档不同名 ⇒ 同名判据漏 1∕13);为什么只管
|
|
13
|
+
* 本批 = 全扫遗留等于让新批替旧账背债(用户 2026-10-26 裁定)。两条理由的完整论证 = §3.5。
|
|
14
|
+
*
|
|
15
|
+
* 门位:`batch` 收口两路(`close` ∥ `status` 写 §1「已收口」)在**文档机检门之前**过本门;`append` 面不过。
|
|
16
|
+
* 调用点 = `batch-lifecycle.mjs` 的 `closeBatchRecord` 与 `statusBatchRecord`(两处;本档不自挂挂点)。
|
|
17
|
+
*
|
|
18
|
+
* 边界:门只拒、**不代删**(删除是实施者 / 人的动作);不扫他批 ∥ 无主遗留;不依赖 manifest
|
|
19
|
+
*(无声明面时文档机检门为「不适用」,本门照判)。
|
|
20
|
+
*
|
|
21
|
+
* 依赖单向(KD-4):本档是**叶档**,只 import `node:*`——不 import skeleton ∥ lifecycle ∥ gate,
|
|
22
|
+
* 判据可被单测直取(零环、零装配)。
|
|
23
|
+
*/
|
|
24
|
+
import { readdirSync } from "node:fs"
|
|
25
|
+
import { basename, dirname, resolve } from "node:path"
|
|
26
|
+
|
|
27
|
+
/** 候选后缀(闭集)——本批单元测试档的形态(§3.1 ①:`docs/batches/*.test.mjs`)。 */
|
|
28
|
+
const TEST_SUFFIX = ".test.mjs"
|
|
29
|
+
/** 批档后缀(归属判据的候选前缀来源 = 同目录 `.md` 档)。 */
|
|
30
|
+
const DOC_SUFFIX = ".md"
|
|
31
|
+
|
|
32
|
+
/** 档名去后缀(形态定长、大小写敏感——`.test.mjs` ∥ `.md` 是本仓固定形态,不做模糊匹配)。 */
|
|
33
|
+
const stripSuffix = (name, suffix) => name.slice(0, -suffix.length)
|
|
34
|
+
|
|
35
|
+
/** 归属判据本体(纯函数):候选 stem 的**最长批档前缀**;无任何批档名为其前缀 ⇒ `null`(无主)。
|
|
36
|
+
* 最长 = 同目录并存两份批档、一份 stem 是另一份前缀时,短前缀不会把别人的档记到本批。 */
|
|
37
|
+
function owningBatchStem(candidateStem, batchStems) {
|
|
38
|
+
let best = null
|
|
39
|
+
for (const stem of batchStems) {
|
|
40
|
+
if (!candidateStem.startsWith(stem)) continue
|
|
41
|
+
if (best === null || stem.length > best.length) best = stem
|
|
42
|
+
}
|
|
43
|
+
return best
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 本批自己的单元测试档集(判据本体 · 只读 · 零写)。
|
|
48
|
+
* @param {string} batchAbsPath — 本批批档的绝对路径(`pickTarget` 解析所得)
|
|
49
|
+
* @returns {string[]} 本批自己的单元测试档绝对路径(升序;空集 = 放行面)
|
|
50
|
+
* @throws 目录不可读(EACCES ∥ ENOENT ∥ 非目录)⇒ fail-closed:判不了就是不许过——收口被拒并点名目录。
|
|
51
|
+
* 设计未定此支(评审 🔵#11),父侧 2026-10-26 裁决按本仓既有 fail-closed 口径落
|
|
52
|
+
* (`docs/core/design/BATCH-RECORD.md` §4.1「fail-closed」行;裁决与读数 = 本批 §5)。
|
|
53
|
+
* 本支用**独立锚串** `unit-test gate cannot determine`:机检锚 `unit test file(s) still on disk`
|
|
54
|
+
* 专表「档真在盘」这一拒因,两者不可互混。
|
|
55
|
+
*/
|
|
56
|
+
export function batchUnitTestFiles(batchAbsPath) {
|
|
57
|
+
const dir = dirname(resolve(batchAbsPath))
|
|
58
|
+
const stem = stripSuffix(basename(resolve(batchAbsPath)), DOC_SUFFIX)
|
|
59
|
+
let entries
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"batch: 本批批内单测档门无法判定(unit-test gate cannot determine)— cannot list the batch record's directory " +
|
|
65
|
+
"to tell whether this batch's own unit test file remains on disk, so the closeout is refused (fail-closed: 判不了就不许过). " +
|
|
66
|
+
`Directory: ${dir}. Cause: ${e?.message ?? String(e)}. Nothing was written.`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
const batchStems = entries
|
|
70
|
+
.filter((e) => e.isFile() && e.name.endsWith(DOC_SUFFIX))
|
|
71
|
+
.map((e) => stripSuffix(e.name, DOC_SUFFIX))
|
|
72
|
+
const hits = []
|
|
73
|
+
for (const e of entries) {
|
|
74
|
+
if (!e.isFile() || !e.name.endsWith(TEST_SUFFIX)) continue // 目录不计;不同后缀不属候选集
|
|
75
|
+
if (owningBatchStem(stripSuffix(e.name, TEST_SUFFIX), batchStems) !== stem) continue // 他批的档 ∥ 无主档 ⇒ 不判
|
|
76
|
+
hits.push(resolve(dir, e.name))
|
|
77
|
+
}
|
|
78
|
+
return hits.sort()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 拒收文案(单源——两路收口共用;`action` 只标通路:`close` ∥ `status → 已收口`)。
|
|
82
|
+
* 三件 = 判据句(机检锚串 `unit test file(s) still on disk`)+ 逐档一行绝对路径 + 修法(删)+ `Nothing was written.` */
|
|
83
|
+
function unitTestFilesStillOnDiskError(files, action) {
|
|
84
|
+
const route = action === "status" ? "status → 已收口" : "close"
|
|
85
|
+
return [
|
|
86
|
+
"batch: 本批批内单测档仍在盘(收口前置门 · unit-test file gate)— " +
|
|
87
|
+
`${route} refuses: this batch's own unit test file(s) still on disk, and closing would freeze the record with them left behind ` +
|
|
88
|
+
"(零留存:单元测试档用毕即删,读数一行留批档 §5).",
|
|
89
|
+
`Files (${files.length}):`,
|
|
90
|
+
...files.map((f) => ` ${f}`),
|
|
91
|
+
"处置:删掉上面点名的档(用毕即删——读数一行留批档 §5),再复跑收口;本门只拒、不代删。" +
|
|
92
|
+
"判据只认本批自己的档(同目录最长批档名前缀),他批遗留 ∥ 无主档不判。Nothing was written.",
|
|
93
|
+
].join("\n")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 收口前置门(两路收口共调):本批自己的单元测试档仍在盘 ⇒ throw(写盘前——`§1` 状态行字节不变)。
|
|
98
|
+
* 放行零回执(与其余收口前置同形:不写回执段、不改回执字面)。
|
|
99
|
+
* @param {string} batchAbsPath — 本批批档的绝对路径
|
|
100
|
+
* @param {"close"|"status"} [action] — 拒串标通路(`status` ⇒ 标 `status → 已收口`)
|
|
101
|
+
* @throws 非空命中 ∥ 目录不可读(fail-closed,见 `batchUnitTestFiles`)
|
|
102
|
+
*/
|
|
103
|
+
export function assertUnitTestFilesCleared(batchAbsPath, action = "close") {
|
|
104
|
+
const files = batchUnitTestFiles(batchAbsPath)
|
|
105
|
+
if (files.length === 0) return
|
|
106
|
+
throw new Error(unitTestFilesStillOnDiskError(files, action))
|
|
107
|
+
}
|