@thincoder/core 0.10.13 → 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.
Files changed (164) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/advisor/messages.mjs +4 -2
  3. package/advisor/notice.mjs +1 -1
  4. package/advisor/project-context.mjs +121 -11
  5. package/agent/completion.mjs +13 -1
  6. package/agent/digest-account.mjs +73 -46
  7. package/agent/dispatch-run.mjs +1 -1
  8. package/agent/dispatch.mjs +29 -21
  9. package/agent/family-tools.mjs +9 -5
  10. package/agent/helpers.mjs +1 -94
  11. package/agent/offload.mjs +88 -0
  12. package/agent/record-results.mjs +1 -6
  13. package/agent/run-stages.mjs +69 -48
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +3 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +32 -3
  18. package/agent/write-face.mjs +99 -0
  19. package/agent/write-gate.mjs +354 -5
  20. package/agent-tools/advisor.mjs +127 -11
  21. package/agent-tools/async-settle.mjs +2 -2
  22. package/agent-tools/bash-async.mjs +8 -2
  23. package/agent-tools/batch-closeout.mjs +206 -0
  24. package/agent-tools/batch-gate.mjs +287 -73
  25. package/agent-tools/batch-ledger-bind.mjs +183 -0
  26. package/agent-tools/batch-ledger-link.mjs +73 -0
  27. package/agent-tools/batch-lifecycle.mjs +113 -367
  28. package/agent-tools/batch-paths.mjs +45 -40
  29. package/agent-tools/batch-read.mjs +1 -1
  30. package/agent-tools/batch-reopen.mjs +142 -0
  31. package/agent-tools/batch-skeleton.mjs +275 -80
  32. package/agent-tools/batch.mjs +243 -50
  33. package/agent-tools/context.mjs +6 -3
  34. package/agent-tools/design-slots.mjs +24 -24
  35. package/agent-tools/design-token.mjs +96 -15
  36. package/agent-tools/doc-check.mjs +18 -2
  37. package/agent-tools/eng.mjs +1 -1
  38. package/agent-tools/escalate-async.mjs +3 -3
  39. package/agent-tools/manifest.mjs +8 -5
  40. package/agent-tools/read-history.mjs +5 -5
  41. package/agent-tools/review-facts.mjs +31 -13
  42. package/agent-tools/spawn-gates.mjs +10 -14
  43. package/agent-tools/ssh-session.mjs +120 -111
  44. package/agent-tools/ssh-transfer.mjs +210 -0
  45. package/agent-tools/ssh-transport.mjs +277 -46
  46. package/agent-tools/subagent-actions-query.mjs +1 -1
  47. package/agent-tools/subagent-actions.mjs +1 -1
  48. package/agent-tools/subagent-async.mjs +3 -2
  49. package/agent-tools/subagent-scheduler.mjs +43 -9
  50. package/agent-tools/subagent-spawn.mjs +68 -9
  51. package/agent-tools/subagent.mjs +16 -14
  52. package/client-identity.mjs +22 -6
  53. package/config-presets.mjs +5 -3
  54. package/context.mjs +34 -3
  55. package/declaration.mjs +2 -2
  56. package/doc-check/anchors.mjs +128 -28
  57. package/doc-check/index.mjs +68 -15
  58. package/doc-check/line-refs.mjs +112 -0
  59. package/doc-check/paragraph.mjs +18 -5
  60. package/explore-distill.mjs +1 -1
  61. package/i18n.mjs +1 -1
  62. package/index-discover.mjs +4 -1
  63. package/ledger-close-proof.mjs +19 -37
  64. package/ledger-cmd.mjs +71 -195
  65. package/ledger-db.mjs +29 -28
  66. package/ledger-migrate.mjs +4 -4
  67. package/ledger-read.mjs +10 -8
  68. package/ledger-surface.mjs +1 -1
  69. package/ledger-tools.mjs +6 -6
  70. package/ledger-variant-notice.mjs +1 -1
  71. package/ledger-write-ops.mjs +373 -0
  72. package/ledger.mjs +2 -2
  73. package/manifest-discovery.mjs +2 -2
  74. package/manifest-schema.mjs +4 -0
  75. package/memory/memory-tool.mjs +5 -2
  76. package/memory/schema.mjs +5 -26
  77. package/package.json +1 -1
  78. package/path-resolve.mjs +217 -0
  79. package/permission.mjs +4 -0
  80. package/prompt-overlays.mjs +5 -0
  81. package/prompts/advisor-design.md +9 -6
  82. package/prompts/advisor-round1.md +4 -3
  83. package/prompts/advisor-round2.md +6 -5
  84. package/prompts/advisor-round3.md +6 -5
  85. package/prompts/common.md +23 -10
  86. package/prompts/consult-base.md +2 -1
  87. package/prompts/discipline-engineering.md +29 -48
  88. package/prompts/discipline-normal.md +10 -5
  89. package/prompts/persona-eng-coder.md +8 -0
  90. package/prompts/persona-eng-designer.md +22 -12
  91. package/prompts/persona-engineering.md +81 -68
  92. package/prompts/persona-plan.md +2 -1
  93. package/prompts/persona-tester.md +55 -0
  94. package/provider/core.mjs +2 -2
  95. package/provider/google.mjs +15 -5
  96. package/provider/rate.mjs +11 -3
  97. package/provider/sse.mjs +15 -4
  98. package/session-identity.mjs +21 -0
  99. package/session-lifecycle.mjs +9 -4
  100. package/session-slots.mjs +9 -14
  101. package/session.mjs +5 -0
  102. package/skip-dirs.mjs +48 -0
  103. package/team-ledger.mjs +4 -4
  104. package/text-budget.mjs +4 -0
  105. package/token-window.mjs +90 -14
  106. package/tool-docs/advisor.md +2 -2
  107. package/tool-docs/apply_patch.md +2 -0
  108. package/tool-docs/bash.md +4 -0
  109. package/tool-docs/batch.md +85 -1
  110. package/tool-docs/browser.md +3 -1
  111. package/tool-docs/code_search.md +3 -1
  112. package/tool-docs/consult_start.md +6 -2
  113. package/tool-docs/consult_stop.md +3 -1
  114. package/tool-docs/delete.md +3 -0
  115. package/tool-docs/doc_check.md +43 -8
  116. package/tool-docs/doc_search.md +5 -1
  117. package/tool-docs/edit.md +22 -1
  118. package/tool-docs/execute.md +7 -1
  119. package/tool-docs/fetch.md +3 -1
  120. package/tool-docs/file_ops.md +3 -1
  121. package/tool-docs/git.md +9 -1
  122. package/tool-docs/goal.md +2 -0
  123. package/tool-docs/grep.md +1 -1
  124. package/tool-docs/hashline_edit.md +2 -0
  125. package/tool-docs/insert_after.md +3 -1
  126. package/tool-docs/ledger.md +5 -5
  127. package/tool-docs/lint.md +1 -0
  128. package/tool-docs/manifest-read.md +2 -1
  129. package/tool-docs/manifest.md +9 -2
  130. package/tool-docs/memory.md +11 -1
  131. package/tool-docs/notify_parent.md +1 -1
  132. package/tool-docs/plan.md +2 -0
  133. package/tool-docs/question.md +1 -1
  134. package/tool-docs/read_history.md +15 -3
  135. package/tool-docs/read_image.md +3 -1
  136. package/tool-docs/recent_changes.md +3 -1
  137. package/tool-docs/repo_outline.md +3 -1
  138. package/tool-docs/settings.md +10 -2
  139. package/tool-docs/skill.md +2 -0
  140. package/tool-docs/ssh.md +30 -17
  141. package/tool-docs/subagent.md +50 -13
  142. package/tool-docs/task.md +2 -0
  143. package/tool-docs/team_ledger.md +1 -1
  144. package/tool-docs/timer.md +7 -0
  145. package/tool-docs/verify.md +13 -1
  146. package/tool-docs/websearch.md +3 -1
  147. package/tool-docs/write.md +2 -0
  148. package/tools/bash.mjs +12 -2
  149. package/tools/edit-batch.mjs +40 -33
  150. package/tools/edit-diff.mjs +69 -1
  151. package/tools/execute.mjs +10 -4
  152. package/tools/file.mjs +17 -27
  153. package/tools/git.mjs +27 -1
  154. package/tools/ops.mjs +2 -1
  155. package/tools/patch.mjs +12 -3
  156. package/tools/process-doc-mark.mjs +139 -0
  157. package/tools/search.mjs +15 -6
  158. package/tools/shared.mjs +24 -1
  159. package/tools/ssh.mjs +48 -7
  160. package/tools/tree.mjs +4 -1
  161. package/tools/write-path.mjs +64 -3
  162. package/traces/trace-store.mjs +4 -3
  163. package/agent/light-round.mjs +0 -63
  164. 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
- * 分段词表、状态行形态与解析全部只住这里——其他档 import 消费,**不复制字面**
5
+ * 分段词表、状态行形态与解析、轮次块标题形态与计数全部只住这里——其他档 import 消费,**不复制字面**
6
6
  * (BATCH-RECORD.md §4.10 / §4.12 / D-BR19;轮 2 #8——骨架占位状态行由 STATUS_WORDS §1 项
7
7
  * 引用生成,全仓无第二份独立词表字面)。
8
8
  *
9
- * 状态行解析(readBatchStatusLine)住本档的理由:冻结门(append/status 写前判定)与
10
- * lifecycle(status/close 写入、在飞批扫描)消费**同一个解析器**——解析单源即 gate 与
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——
@@ -33,19 +32,26 @@ export function isBatchRecord(src) {
33
32
  * - §1 项 = gate 词表 `{ open: 进行中, closed: 已收口 }`——**gate 只消费 §1 项**(单源仍成立);
34
33
  * - §2/§3/§5 各带段内生命周期词表(`done` 词 = 段属主的完成态关键字);
35
34
  * - §4/§6 无项——主 agent 的 status 写域**仅 §1**(§4/§6 状态面走普通文档写,与 D-BR18 分工一致)。
36
- * 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status)拒绝
37
- * 内嵌另一冻结词的值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。
38
- * emoji / 括注 / 日期后缀容忍(**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词
39
- * 自本批起收紧为「剥装饰白名单后余核 = 关键词」,见 batch-lifecycle.mjs);
40
- * 全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
35
+ * 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status)拒绝内嵌另一冻结词的
36
+ * 值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。emoji / 括注 / 日期后缀容忍
37
+ * (**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词自本批起收紧为「剥装饰白名单后余核 = 关键词」,
38
+ * 见 batch-lifecycle.mjs);全词表并集 = 各段项去重集合(note 字段零命中的判据——括注永不误触 gate)。
41
39
  */
42
40
  export const STATUS_WORDS = Object.freeze({
43
41
  1: Object.freeze({ open: "进行中", closed: "已收口" }),
44
42
  2: Object.freeze({ open: "进行中", done: "设计完成" }),
45
- 3: Object.freeze({ open: "进行中", done: "评审完成" }),
43
+ 3: Object.freeze({ open: "进行中", done: "评审通过" }),
46
44
  5: Object.freeze({ open: "进行中", done: "实施完成" }),
47
45
  })
48
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
+
49
55
  /** 身份 → 可写段号(append 段白名单的**子代理面**——原 batch-segment.mjs 逐字迁移;主 agent
50
56
  * 的 depth-0 白名单(模式感知——工程 §1/§4/§6 · §1 ∥ 普通 §1/§2/§4/§5/§6 · §1/§2/§5,§4.16)
51
57
  * 由身份判据在 batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价
@@ -68,6 +74,8 @@ export const DEPTH0_SEGMENTS = Object.freeze({
68
74
  /** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
69
75
  export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
70
76
 
77
+ const ZONE_END_RE = /^#{2,}/ // 段头区界:任意 `##` / `###` 起的行(写面与触发读面同一条判据)
78
+
71
79
  /**
72
80
  * 骨架死占位**枚举单源**(F11-C 判据——create 骨架产出过的全部死占位字面):
73
81
  * - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
@@ -86,10 +94,9 @@ const HEADER_STRUCTURAL_RE = [/^#\s/, /^>\s*编制/, /^>\s*台账/]
86
94
 
87
95
  /**
88
96
  * 死占位残留扫描(F11-C——append/status 落笔前的机检;**纯函数 · 零 fs**)。
89
- * 判定域 = **档头结构行**(区域 = 档首至首个 `## §N` 段头前;值形 = 骨架三类结构行:标题行
90
- * `# …` / 编制行 `> 编制` / 台账行 `> 台账`)——行内出现枚举字面 ⇒ 残留;段体(首段头起)
91
- * 任意位置出现字面 = 正文引用 ⇒ **豁免**(引用非未填——2026-10-05 判据可辨性收正,`TOOLS.md`
92
- * §6.20;原「档头 + 目标段」全域子串扫描分不清「未填占位」与「正文引用」——本批 §1 首投实害)。
97
+ * 判定域 = **档头结构行**(区域 = 档首至首个 `## §N` 段头前;值形 = 骨架三类结构行:标题行 `# …` / 编制行
98
+ * `> 编制` / 台账行 `> 台账`)——行内出现枚举字面 ⇒ 残留;段体(首段头起)任意位置出现字面 = 正文引用 ⇒
99
+ * **豁免**(引用非未填——2026-10-05 判据可辨性收正,`TOOLS.md` §6.20;原全域子串扫描分不清两者——本批 §1 首投实害)。
93
100
  * @param {string} src — 档全文
94
101
  * @returns {Array<{line: number, text: string}>} 残留列表(空 = 无残留);
95
102
  * `line` = 档头域内 1-based 行号(档头域自档首行起——便于定位),`text` = 命中行 trim 原文
@@ -121,20 +128,16 @@ export function placeholderResidueError(residues) {
121
128
 
122
129
  /**
123
130
  * §1 状态行解析(冻结门与 lifecycle 共用的**单源解析器**——原 batch-segment.mjs 逐字迁移,
124
- * 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1 段内
125
- * `**状态行**:` 前缀行(§1 边界 = `## §1` 标题到下一 `## §` 标题;其他段内「状态行」字样
126
- * 不参与判定)。判定只认关键字(emoji / 括号装饰 / 日期后缀容忍):含「已收口」→ "closed"
131
+ * 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1 **段头区**内
132
+ * `**状态行**:` 前缀行(`## §1` 标题之后、下一 `^#{2,}` 之前——与写面同一条定位面;
133
+ * 段体 / 轮次块里引述的状态行不参与判定)。判定只认关键字(emoji / 括号装饰 / 日期后缀容忍):含「已收口」→ "closed"
127
134
  * (冻结优先)、含「进行中」→ "open";无 §1 / 无该行 / 两关键字皆不命中 → "unknown"
128
135
  * (fail-closed 视为冻结)。
129
136
  */
130
137
  export function readBatchStatusLine(src) {
131
- const s1 = SECTION_1_HEADER_RE.exec(src) // 段头单源(与 isBatchRecord 同一正则)
132
- if (!s1) return "unknown"
133
- const nextRe = /^## §\d/gm
134
- nextRe.lastIndex = s1.index + s1[0].length
135
- const next = nextRe.exec(src)
136
- const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
137
- 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)
138
141
  if (!m) return "unknown"
139
142
  const value = m[1].trim()
140
143
  if (value.includes(STATUS_WORDS[1].closed)) return "closed"
@@ -143,11 +146,45 @@ export function readBatchStatusLine(src) {
143
146
  }
144
147
 
145
148
  /**
146
- * §N 段状态词解析(read-data-interface 批——`batch/list` 读面单源,设计档 §2.5):段头
147
- * (sectionHeaderRe)/ 状态行正则(STATUS_LINE_RE)/ 词表(STATUS_WORDS)全消费既有单源常量——
148
- * 与冻结门字面零漂移。`§1`–`§3` / `§5`:段内首个状态行的值按**终态词优先**(closed / done 先于
149
- * open——镜像冻结门取值序)命中已识别关键词 ⇒ 返回该关键词;段头缺失 / 无状态行 / 无命中 ⇒ null。
150
- * `§4` / `§6` 无词条 ⇒ 字面 `无状态词`(不造词)。
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` 无词条 ⇒ 字面 `无状态词`(不造词)。
151
188
  * @param {string} src — 档全文
152
189
  * @param {number|string} seg — 段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
153
190
  * @returns {string|null} 已识别关键词 / null / 字面「无状态词」(§4 / §6)
@@ -155,15 +192,15 @@ export function readBatchStatusLine(src) {
155
192
  export function readSectionStatusWord(src, seg) {
156
193
  const words = STATUS_WORDS[seg]
157
194
  if (!words) return "无状态词"
158
- const hdr = sectionHeaderRe(seg).exec(src)
159
- if (!hdr) return null
160
- const nextRe = /^## §\d/gm
161
- nextRe.lastIndex = hdr.index + hdr[0].length
162
- const next = nextRe.exec(src)
163
- const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
164
- 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)
165
198
  if (!m) return null
166
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
+ }
167
204
  for (const w of [words.closed, words.done, words.open]) {
168
205
  if (w !== undefined && value.includes(w)) return w
169
206
  }
@@ -171,15 +208,174 @@ export function readSectionStatusWord(src, seg) {
171
208
  }
172
209
 
173
210
  /**
174
- * 「不适用」声明行形态单源(§4.18-A——**一行一档**):`> 不适用:§N —— <理由>`,§N ∈ {2,3,4}、
175
- * 理由非空(`\S.*` = 至少一个非空白字符起头)。识别域 = §1 段体内(声明住 §1——D-BR30:工程模式
176
- * 主 agent 的 append 域 = §1/§4/§6,写不进 §2/§3,而声明恰发生在 designer / 评审本轮不跑时 ⇒
177
- * 段内声明形在需要的场合不可写)。声明 = 链门(`sectionContentState`)的合法满足面。
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
+ * 删掉它,残留的声明行就成了一句无人过问的散文——写它的人不会知道自己抄的是一条已停用的路。
178
370
  */
179
371
  const INAPPLICABLE_RE = /^>\s*不适用[::]\s*§(\d+)\s*——\s*(\S.*)$/
180
- /** 可声明的段号集(§5 段号豁免——免声明;§6 不可声明——收口段必须带验证内容)。 */
181
- export const INAPPLICABLE_SEGMENTS = [2, 3, 4]
182
- /** §1 声明行的识别前缀(形不合扫描的宽网——行首命中它而完整形不合 ⇒ 拒,不静默当作无声明)。 */
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 声明行的识别前缀(痕迹扫描的宽网——行首命中它即违规,不再区分形合规与否)。 */
183
379
  const INAPPLICABLE_PREFIX_RE = /^>\s*不适用/
184
380
 
185
381
  /** §N 段体行集(段头缺失 ⇒ null——create 是骨架唯一权威入口;行切分 = /\r?\n/——EOL 形态无关)。 */
@@ -194,32 +390,33 @@ function sectionBodyLines(src, seg) {
194
390
  return (nl >= 0 ? body.slice(nl + 1) : "").split(/\r?\n/)
195
391
  }
196
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
+
197
401
  /**
198
402
  * 段内容判据**单源**(§4.18-A closeout chain gate 的判空口径;D-BR30 判据单源——**不许两套**;
199
403
  * §4.13 的 §6 旧判据 `sectionSixHasContent` 改由本谓词承担——§6 为唯一缺段时拒串仍取旧函数)。
200
- * 返回三态:`content`(段体至少一行非排除类)∥ `not-applicable`(§1 声明该段不适用且理由非空——
201
- * 仅 §2/§3/§4)∥ `empty`。排除类(判空依据——「标题在、正文零行」判空):空行 ∥ 标题行
202
- * (`^#{1,6}\s`)∥ `**状态行**:` 行 ∥ `<§N 模板占位:…>` 行 ∥ 「不适用」声明行。
404
+ * 返回**两态**:`content`(段体至少一行非排除类)∥ `empty`。旧第三态 `not-applicable` 随声明机制退役
405
+ * (2026-11-02:任何段都不得声明不适用 ⇒ 这条事实没有第二个满足面,判据只剩「有内容 / 没内容」)。
406
+ * 排除类(判空依据——「标题在、正文零行」判空):空行 ∥ 标题行(`^#{1,6}\s`)∥ `**状态行**:` 行 ∥
407
+ * `<§N 模板占位:…>` 行 ∥ 「不适用」声明行(判空口径零改——那类行是停用机制的痕迹,本来就不算内容)。
203
408
  * 口径收正注:旧 `sectionSixHasContent` 对「只有子标题(`### …`)」判非空;本谓词按 §4.18-A 判空
204
409
  * (子标题属排除类标题行)——设计档 §4.18-A 明文收窄。
205
410
  * @param {string} src — 档全文
206
411
  * @param {number|string} seg — 段号(§1–§6;§1 恒 `content`——主 agent 自述段不设前置)
207
- * @returns {"content"|"not-applicable"|"empty"} 段内容三态
412
+ * @returns {"content"|"empty"} 段内容两态
208
413
  */
209
414
  export function sectionContentState(src, seg) {
210
415
  const n = Number(seg)
211
416
  if (n === 1) return "content"
212
417
  const lines = sectionBodyLines(src, n)
213
418
  if (lines === null) return "empty" // 段头缺失 = 空(骨架由 create 一次预写;缺失即无内容)
214
- if (lines.some((l) => NONEMPTY_RE.test(l) && !EXCLUDED_LINE_RE.test(l))) return "content"
215
- // 段体无实质内容 ⇒ 查 §1 声明(仅 §2/§3/§4 可声明;理由非空——INAPPLICABLE_RE 的 (\S.*) 已保证)
216
- if (INAPPLICABLE_SEGMENTS.includes(n)) {
217
- for (const l of sectionBodyLines(src, 1) ?? []) {
218
- const m = INAPPLICABLE_RE.exec(l)
219
- if (m && Number(m[1]) === n) return "not-applicable"
220
- }
221
- }
222
- return "empty"
419
+ return lines.some((l) => NONEMPTY_RE.test(l) && !EXCLUDED_LINE_RE.test(l)) ? "content" : "empty"
223
420
  }
224
421
 
225
422
  /** 非空白行谓词(空行排除)。 */
@@ -229,24 +426,24 @@ const NONEMPTY_RE = /\S/
229
426
  const EXCLUDED_LINE_RE = /^#{1,6}\s|^\s*\*\*状态行\*\*[::]|^<§\d+ 模板占位[::]|^>\s*不适用/
230
427
 
231
428
  /**
232
- * 声明形不合拒绝句(§4.18-A J3——形不合 ⇒ 拒,**不静默当作无声明**):行首命中
233
- * `^>\s*不适用` 却不满足完整形(缺 `——` ∥ 缺理由 ∥ § 号 ∉ {2,3,4})。
234
- * 逐字文本 = `TOOLS.md` §6.24 J3。
429
+ * 声明行拒绝句(§4.18-A J3——**声明机制已退役**:`§1` 段体出现任何 `> 不适用` 行 ⇒ 拒):
430
+ * 拒文说清三件——声明这条路没有了 ∥ 每段各自要有内容(§5 豁免)∥ 点名违规行。
431
+ * 措辞不再给「合法声明形」——给形 = 给一条可照抄的路,而本批封的正是这条路。
235
432
  * @param {string} line — 违规行原文
236
433
  * @returns {string} 完整拒绝句(调用方 throw new Error(...))
237
434
  */
238
435
  export function inapplicableDeclarationError(line) {
239
- return "batch: 「不适用」声明形不合(malformed inapplicability declaration)— one line per section: " +
240
- "`> 不适用:§N —— <reason>` with a non-empty reason, §N ∈ {2,3,4} (§5 needs no declaration; §6 is never declarable). " +
436
+ return "batch: `不适用` 声明已停用(inapplicability declaration retired)— no declaration is accepted: " +
437
+ "§2 / §3 / §4 / §6 must each carry content (§5 exempt). " +
241
438
  `Offending line: ${line}. Nothing was written.`
242
439
  }
243
440
 
244
441
  /**
245
- * §1 声明行机械扫描(链门前置判据——形不合 ⇒ 拒,不静默):识别域 = §1 段体。行首命中
246
- * `^>\s*不适用` 而完整形不合(INAPPLICABLE_RE 不中 ∥ § 号 ∉ {2,3,4})⇒ 返回违规行原文;
247
- * § 号 ∈ {2,3,4} 且理由非空 ⇒ 合法。无违规 ⇒ null。
442
+ * §1 声明行机械扫描(链门前置判据——**出现即拒**,不静默):识别域 = §1 段体。
443
+ * 段号集 `INAPPLICABLE_SEGMENTS` 已清空 ⇒ `includes` 恒 false ⇒ 行首命中 `^>\s*不适用` 的每一行
444
+ * (形合规与否一律)都是违规行,返回首个。本函数现在是**停用机制的痕迹检测器**,不是合法形校验器。
248
445
  * @param {string} src — 档全文
249
- * @returns {string|null} 首个形不合声明行原文 / null
446
+ * @returns {string|null} 首个声明行原文 / null
250
447
  */
251
448
  export function malformedInapplicableLine(src) {
252
449
  for (const l of sectionBodyLines(src, 1) ?? []) {
@@ -276,18 +473,15 @@ export function sectionHasStatusLine(src, seg) {
276
473
  }
277
474
 
278
475
  /**
279
- * create 骨架模板(BATCH-RECORD.md §4.10 行为规格的**代码单源**——红线:模板只覆盖骨架与
280
- * 占位,不模板化内容):`## §1–§6` 段头(含职责署名)+ 档头 boilerplate(六段一段一作者句 +
281
- * 编制行 + 台账/前情指针行)+ §1 段内**占位状态行**(含 gate 合法关键字「进行中」——建档即过
282
- * gate;整行不含「已收口」——gate 已收口优先,占位行不得携带冻结词)+ 各段模板子标题占位。
283
- * 已知实参替换:date / topic / **source** / prev(§4.11 参数面 + F11-B——source 必填、prev 传
284
- * 入值过幂等剥前缀,归一化在 lifecycle 面);`ledger` / `board` = 可选参(2026-10-05 建即填——
285
- * 给则台账行实参化、未给保持占位字面;4 参旧调用形输出零变)。档头 `#<编号>` / `<板块>` 占位
286
- * 留存时(未给参)由主 agent 文件编辑填(台账登记 = 主 agent 既有义务,create 不代建——填充
287
- * 时点 = 建档后、本档首个 append/status 之前)。
288
- * 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
289
- * 模式取形(§4.16——普通模式变体**四处替换**、余行逐字同;`mode` 缺省 = "engineering" ⇒
290
- * 工程产出逐字不变):① 汇总行 §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` 段头。
291
485
  */
292
486
  export function batchSkeleton({ date, topic, source, prev, ledger = null, board = null, mode = "engineering" }) {
293
487
  const normal = mode === "normal"
@@ -295,9 +489,10 @@ export function batchSkeleton({ date, topic, source, prev, ledger = null, board
295
489
  const implementerSeat = normal ? "实施者:主 agent ∥ coder" : "eng-coder" // ① 汇总行 §5 标签 ∥ ④ §5 段头
296
490
  return [
297
491
  `# ${date} · ${topic}`,
298
- `> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(${designerSeat})· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(${implementerSeat})· §6 验证与收口(父代理)。`,
492
+ `> 六段,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(${designerSeat})· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(${implementerSeat})· §6 验证与收口(父代理)。`,
493
+ `> 每段只有一个作者:作者的段可 \`append\` 加内容、可就地改(\`edit\`,§4.19);非作者一概拒。`,
299
494
  `> 编制:主 agent · ${date} · 来源 = ${source}。`,
300
- `> 台账 = ${ledger ?? "#<编号>"}(${board ?? "<板块>"} · 归批)。前情 = ${prev}。`,
495
+ `> 台账 = ${ledger ?? "#<编号>"}(${board ?? (ledger ? "—" : "<板块>")} · 归批)。前情 = ${prev}。`,
301
496
  "## §1 讨论(主 agent)",
302
497
  `**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
303
498
  "<§1 模板占位:本批条目 / 关键判据 / 授权口径>",