@thincoder/core 0.9.1 → 0.9.3

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 (75) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +90 -0
  4. package/advisor/loop.mjs +2 -2
  5. package/advisor/run.mjs +1 -1
  6. package/agent/completion.mjs +3 -1
  7. package/agent/family-tools.mjs +24 -11
  8. package/agent/helpers.mjs +11 -2
  9. package/agent/run-stages.mjs +27 -5
  10. package/agent/setup-reminders.mjs +67 -11
  11. package/agent/setup.mjs +6 -0
  12. package/agent/write-gate.mjs +5 -5
  13. package/agent-tools/advisor-async.mjs +4 -4
  14. package/agent-tools/advisor.mjs +3 -3
  15. package/agent-tools/async-discard.mjs +1 -1
  16. package/agent-tools/audit-block.mjs +106 -0
  17. package/agent-tools/batch-lifecycle.mjs +301 -0
  18. package/agent-tools/batch-segment.mjs +16 -263
  19. package/agent-tools/batch-skeleton.mjs +156 -0
  20. package/agent-tools/batch.mjs +410 -0
  21. package/agent-tools/context.mjs +174 -0
  22. package/agent-tools/eng.mjs +4 -0
  23. package/agent-tools/goal.mjs +7 -0
  24. package/agent-tools/parent-channel.mjs +18 -1
  25. package/agent-tools/plan.mjs +39 -5
  26. package/agent-tools/read-history.mjs +122 -24
  27. package/agent-tools/settings.mjs +4 -2
  28. package/agent-tools/subagent-async.mjs +3 -3
  29. package/agent-tools/subagent-spawn.mjs +29 -101
  30. package/agent-tools/task.mjs +11 -0
  31. package/agent-tools.mjs +9 -2
  32. package/agent.mjs +10 -4
  33. package/config.mjs +1 -1
  34. package/context.mjs +66 -121
  35. package/fts-text.mjs +41 -0
  36. package/generate-title.mjs +6 -6
  37. package/i18n.mjs +4 -4
  38. package/ledger-cmd.mjs +30 -7
  39. package/ledger-db.mjs +22 -2
  40. package/ledger-executors.mjs +103 -0
  41. package/ledger-surface.mjs +15 -8
  42. package/ledger.mjs +15 -4
  43. package/manifest.mjs +172 -60
  44. package/memory/core.mjs +4 -18
  45. package/memory/schema.mjs +4 -11
  46. package/package.json +22 -1
  47. package/prompts/advisor-design.md +1 -1
  48. package/prompts/advisor-round2.md +1 -1
  49. package/prompts/advisor-round3.md +1 -1
  50. package/prompts/common.md +3 -3
  51. package/prompts/discipline-engineering.md +17 -2
  52. package/prompts/persona-eng-coder.md +1 -1
  53. package/prompts/persona-eng-designer.md +1 -1
  54. package/prompts/persona-engineering.md +9 -6
  55. package/session-gc.mjs +129 -74
  56. package/session-index-build.mjs +298 -0
  57. package/session-index-cmd.mjs +61 -0
  58. package/session-index-pass.mjs +95 -0
  59. package/session-index-query.mjs +102 -0
  60. package/session-index.mjs +285 -0
  61. package/session-lifecycle.mjs +18 -5
  62. package/session-slots-manifest.mjs +55 -3
  63. package/session-stale.mjs +247 -0
  64. package/token-window.mjs +188 -0
  65. package/tools/bash.mjs +4 -15
  66. package/tools/execute.mjs +5 -13
  67. package/tools/git-checkpoint.mjs +1 -1
  68. package/tools/git-ext.mjs +23 -20
  69. package/tools/git-run.mjs +141 -0
  70. package/tools/git.mjs +56 -45
  71. package/tools/index.mjs +3 -1
  72. package/tools/process-tree.mjs +20 -0
  73. package/tools/shared.mjs +8 -4
  74. package/traces/trace-cleanup.mjs +109 -0
  75. package/traces/trace-store.mjs +32 -36
@@ -1,265 +1,18 @@
1
1
  /**
2
- * agent-tools/batch-segment.mjs — 批次档段写入工具(ENGINEERING-MODE.md §2.20 · FR22 F1-F7)。
3
- *
4
- * 规则、守卫与 fail-closed 清单的**权威正文在 §2.20.1**(本文件不重述——D2 单一权威源);
5
- * 实现要点(正文未定死、由代码承载):
6
- * - 身份判据 = 评审实例绑定(`review` 形态)/ `agent._role`;
7
- * - append-only:在段尾(下一条 `## §N` 或 EOF)插入,既有行字节不变;
8
- * - 来源戳 `### 轮次 N(评审子代理)`(仅 §3)——N = §3 内该形态行计数 + 1,**调用方自带**
9
- * 的同名标题行被丢弃(否则伪造戳会污染 N 计数——§2.20.8 #5);
10
- * - 凭证剥除:含凭证形态的行剥掉该子串,剥后为空则整行丢弃(「零命中」+「其余逐字保留」);
11
- * - 路径门禁:`resolveBatchDocPath`(评审侧「若传则须可读」);工具内再查一次可读性。
12
- * - v2 增量(ENGINEERING-MODE-V2-MODULE-BATCH-SEGMENT.md §2.1):§1 状态行解析
13
- * (`readBatchStatusLine`——「已收口」→ 冻结拒写、「进行中」→ 放行、缺失/不可解析 →
14
- * fail-closed 拒);`resolveBatchDocPath` 加 manifest `docRoot.batches` 双基底
15
- * (值形态 = 串 | 多根数组——逐基底按序复判;manifest 缺失/非法/读错 → v1 单基底
16
- * 语义零变);冻结只覆盖 `batch_segment` 通道(§2.5)。
17
- *
18
- * 导出面:`batchSegmentTool` · `resolveBatchDocPath` · `batchDocForReview`(异步评审实例键取绑定)·
19
- * `configureBatchSegment` / `resetBatchSegment`(#84 记账面注入缝——缺省 no-op,见下)。
20
- *
21
- * #84 记账面(CORE-UNIFICATION §2.13.4):写入成功后的记账(把绑定档记入调用者写域)
22
- * **按端注入**(形态参 §2.13.5 注入缝)——核内缺省 no-op = CLI 语义零行为变;端装配
23
- * 经 `configureBatchSegment({ onWrite })` 覆盖(VSC 侧现形 = `_touchedFiles` 记账)。
2
+ * agent-tools/batch-segment.mjs — **过渡 shim**(batch 工具单名化收口,BATCH-RECORD §4.14):
3
+ * 本档仅 re-export `batch.mjs` 主档导出面(旧 import 面 = VSC `setup-tooltable.mjs` 经此 import
4
+ * `configureBatchSegment` / 旧测试消费——导出全超集,行为零变)。权威实现 = batch.mjs
5
+ * (batchTool 四 action + append 迁移面 + 过渡别名 batchSegmentTool);撤除判据 = §4.14
6
+ * (消费面清零后删本档——先例债:删档需同步 tooltable 装配 import,随批登记)。
24
7
  */
25
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
26
- import { resolve } from "node:path"
27
-
28
- import { docRootPaths, readManifest } from "../manifest.mjs"
29
-
30
- /** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。 */
31
- export const MAX_TEXT_CHARS = 20000
32
- /** 身份 → 可写段号(§2.20.1 段白名单)。 */
33
- export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
34
- /** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
35
- const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
36
- /** 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。 */
37
- const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
38
- /** 凭证形态(§2.7 冒号态):`[DESIGN-TOKEN:…]` 与 `designId: …`——本节自有正则。 */
39
- const CRED_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/g
40
- const CRED_TEST_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/
41
- /** §1 状态行前缀(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
42
- const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
43
-
44
- // ─── #84 记账面注入缝(「记账面按端注入」,形态参 §2.13.5 注入缝)───────────────
45
- /**
46
- * 写入成功后的记账注入面(**缺省 no-op** = CLI 语义零行为变):端装配层可覆盖——把写成的
47
- * 批次档记入调用者写域(VSC 侧现形 = `agent._touchedFiles` 记账,供子代理合入 / recent_changes /
48
- * verify / advisor 默认范围消费)。核内零端名分支(契约 5——本档只认 `onWrite` 函数名)。
49
- * 形态与 `tools/write-path.mjs` 的 `configureWritePath` 同款:模块级、缺省不覆盖、null 撤销。
50
- */
51
- let injectedOnWrite = null
52
- export function configureBatchSegment(impl) {
53
- injectedOnWrite = impl && typeof impl.onWrite === "function" ? impl.onWrite : null
54
- }
55
- /** 撤销注入(测试与端装配生命周期用——缺省态 = 零记账)。 */
56
- export function resetBatchSegment() { injectedOnWrite = null }
57
-
58
- /** 可读文件判据(存在且为文件——目录/缺失同判不可读)。 */
59
- function readableFile(abs) {
60
- try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
61
- }
62
-
63
- /**
64
- * 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
65
- * 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
66
- * v2:cwd 不可读 → manifest `docRoot.batches` 复判(M3 模块设计 §2.1#4——N3 可迁移;
67
- * 值形态 = 串 | 多根数组——逐基底按序复判,首个可读者胜)。
68
- */
69
- export function resolveBatchDocPath(cwd, given) {
70
- const base = cwd ?? process.cwd()
71
- const raw = typeof given === "string" ? given.trim() : ""
72
- if (!raw) {
73
- throw new Error("batchDoc must be a non-empty path to the batch record — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
74
- }
75
- const abs = resolve(base, raw.replace(/\\/g, "/"))
76
- if (readableFile(abs)) return abs
77
- // 双基底(M3 模块设计 §2.1#4):cwd 不可读 → manifest docRoot.batches 复判(逐基底按序,
78
- // 首个可读者胜);manifest 缺失 / 非法 / 读错 → 无第二基底(v1 单基底语义零变),
79
- // 全不可读 → throw。
80
- let man = { ok: false }
81
- try { man = readManifest(base) } catch { /* 权限等读错——按无 manifest 处理(v1 语义) */ }
82
- if (man.ok) {
83
- for (const root of docRootPaths(man.manifest?.docRoot?.batches, base)) {
84
- const alt = resolve(root, raw.replace(/\\/g, "/"))
85
- if (readableFile(alt)) return alt
86
- }
87
- }
88
- throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
89
- }
90
-
91
- /**
92
- * §1 状态行解析(内部——M3 模块设计 §2.1#1):解析对象 = §1 段内 `**状态行**:` 前缀行
93
- * (§1 边界 = `## §1` 标题到下一 `## §` 标题;其他段内「状态行」字样不参与判定)。
94
- * 判定只认关键字(emoji / 括号装饰 / 日期后缀容忍):含「已收口」→ "closed"(冻结优先)、
95
- * 含「进行中」→ "open";无 §1 / 无该行 / 两关键字皆不命中 → "unknown"(fail-closed 视为冻结)。
96
- */
97
- function readBatchStatusLine(src) {
98
- const s1 = /^## §1(?=\s|$)/m.exec(src)
99
- if (!s1) return "unknown"
100
- const nextRe = /^## §\d/gm
101
- nextRe.lastIndex = s1.index + s1[0].length
102
- const next = nextRe.exec(src)
103
- const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
104
- const m = body.split("\n").map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
105
- if (!m) return "unknown"
106
- const value = m[1].trim()
107
- if (value.includes("已收口")) return "closed"
108
- if (value.includes("进行中")) return "open"
109
- return "unknown"
110
- }
111
-
112
- /**
113
- * 设计评审的实例绑定解析(§2.20.2/§2.20.3——batch_segment 的唯一路径来源):
114
- * - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
115
- * 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
116
- * - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
117
- */
118
- export function batchDocForReview(agent, documents, callbacks = null) {
119
- if (callbacks && "batchDoc" in callbacks) return callbacks.batchDoc ?? null
120
- const key = JSON.stringify([...(documents ?? [])].sort())
121
- for (const e of agent?._asyncAdvisors?.values?.() ?? []) {
122
- if (e.status === "running" && e.reviewType === "design" && JSON.stringify([...(e.documents ?? [])].sort()) === key) {
123
- return e.run?.batchDoc ?? null
124
- }
125
- }
126
- return null
127
- }
128
-
129
- /** 段号解析:`§2` / `2` / `§2 批次任务` 均可;无法解析 → null。 */
130
- function segmentNumber(raw) {
131
- const m = /^§?\s*(\d+)/.exec(String(raw ?? "").trim())
132
- return m ? Number(m[1]) : null
133
- }
134
-
135
- /** 身份 → 可写段号;无写权身份 → null。评审实例绑定优先(评审者的身份即"设计评审"。) */
136
- function allowedSegment(agent, review) {
137
- if (review) return 3
138
- return SEGMENT_BY_ROLE[agent?._role] ?? null
139
- }
140
-
141
- /**
142
- * 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
143
- * 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
144
- * `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
145
- * 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
146
- */
147
- function sanitizeText(text, dropStamp) {
148
- const kept = []
149
- for (const line of text.replace(/\r\n?/g, "\n").split("\n")) {
150
- if (dropStamp && ROUND_HEADING_RE.test(line)) continue
151
- if (!CRED_TEST_RE.test(line)) { kept.push(line); continue }
152
- const cleaned = line.replace(CRED_RE, "").replace(/[ \t]+$/, "")
153
- if (cleaned.trim()) kept.push(cleaned)
154
- }
155
- return kept.join("\n")
156
- }
157
-
158
- /** §3 内工具写入的轮次行计数(N = 计数 + 1;收窄口径——只数工具戳形态)。 */
159
- function roundCount(src, headerMatch, endIdx) {
160
- const body = src.slice(headerMatch.index + headerMatch[0].length, endIdx)
161
- return body.split("\n").filter((l) => ROUND_HEADING_RE.test(l)).length
162
- }
163
-
164
- /** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。 */
165
- function insertIntoSection(src, seg, text) {
166
- const hdr = sectionHeaderRe(seg).exec(src)
167
- if (!hdr) {
168
- throw new Error(`batch_segment: the bound batch record has no "## §${seg}" section header — the six-section skeleton is written by the record's creator before any segment write. Fix: ask the parent/creator to add the "## §${seg} …" heading (with the template's sub-headings) first, then call batch_segment again. Nothing was written.`)
169
- }
170
- const nextRe = /^## §\d/gm
171
- nextRe.lastIndex = hdr.index + hdr[0].length
172
- const next = nextRe.exec(src)
173
- const endIdx = next ? next.index : src.length
174
- // 来源戳(仅 §3):工具生成标题 + N——调用方写不进自己的标题(sanitize 已丢弃同名行)。
175
- const roundN = seg === 3 ? roundCount(src, hdr, endIdx) + 1 : 0
176
- const payload = (seg === 3 ? `### 轮次 ${roundN}(评审子代理)\n\n` : "") + text
177
- const before = src.slice(0, endIdx)
178
- const after = src.slice(endIdx)
179
- const eol = src.includes("\r\n") ? "\r\n" : "\n"
180
- const lead = before === "" ? "" : before.endsWith(eol + eol) ? "" : before.endsWith(eol) ? eol : eol + eol
181
- // 尾随 eol 恒加(块自成行);后面还有段标题时再补一个空行(保持模板的空行分隔)。
182
- const written = before + lead + payload.replaceAll("\n", eol) + eol + (after === "" ? "" : eol) + after
183
- return { written, roundN }
184
- }
185
-
186
- /**
187
- * 批次档段写入工具工厂。
188
- * @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
189
- * @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
190
- */
191
- export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
192
- const own = review ? "§3" : null
193
- return {
194
- name: "batch_segment",
195
- description:
196
- "Append your own section of the batch record (一段一作者). " +
197
- "There is NO path parameter: the target record is bound to you (at spawn for eng-designer/eng-coder, per review instance for a design review) and your identity fixes the section you may write " +
198
- "(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
199
- "Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
200
- "Credential values are stripped mechanically before writing (never write a token or designId value). " +
201
- "A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
202
- "Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
203
- parameters: {
204
- type: "object",
205
- properties: {
206
- segment: {
207
- type: "string",
208
- description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable.`,
209
- },
210
- text: {
211
- type: "string",
212
- description: "The markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
213
- },
214
- },
215
- required: ["segment", "text"],
216
- },
217
- async execute(args, ctx) {
218
- const agent = ctx?.agent ?? {}
219
- const seg = allowedSegment(agent, review)
220
- if (seg === null) {
221
- throw new Error("batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes.")
222
- }
223
- const n = segmentNumber(args?.segment)
224
- if (n === null) {
225
- throw new Error(`batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}").`)
226
- }
227
- if (n !== seg) {
228
- throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5).`)
229
- }
230
- if (!batchDoc) {
231
- throw new Error("batch_segment: no batch record is bound to this caller — there is no path parameter by design (the target arrives via the spawn binding / the review instance key). Report the section as not written.")
232
- }
233
- const abs = resolveBatchDocPath(agent.cwd ?? process.cwd(), batchDoc)
234
- const src = readFileSync(abs, "utf8")
235
- const status = readBatchStatusLine(src)
236
- if (status === "closed") {
237
- throw new Error("batch_segment: 已收口档不回改 — the bound batch record's §1 status line contains 「已收口」, so the record is frozen: its body is never written to again (整档冻结;改 = 新批新档). Nothing was written. Report the section as not written.")
238
- }
239
- if (status === "unknown") {
240
- throw new Error("batch_segment: 状态行不可解析或缺失 — the bound batch record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Ask the record's creator to set the §1 status line, then call again. Nothing was written.")
241
- }
242
- if (typeof args?.text !== "string") {
243
- throw new Error("batch_segment: text must be a string (the markdown to append).")
244
- }
245
- if (args.text.length > MAX_TEXT_CHARS) {
246
- throw new Error(`batch_segment: text is ${args.text.length} characters — the limit is ${MAX_TEXT_CHARS} per call. Split it into multiple calls: each call becomes its own section and the round number N continues (分段追加——每次调用各成节、N 顺延).`)
247
- }
248
- if (/^## §\d/m.test(args.text)) {
249
- throw new Error("batch_segment: the text contains a section header line matching `^## §N` — that would break section location and the append-only guarantee. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (骨架保护). Nothing was written.")
250
- }
251
- const body = sanitizeText(args.text, seg === 3).replace(/^\n+/, "").replace(/\s+$/, "")
252
- if (!body.trim()) {
253
- throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record).")
254
- }
255
- const { written, roundN } = insertIntoSection(src, seg, body)
256
- writeFileSync(abs, written)
257
- // #84(S1 续轮第二批——VSC 侧并入 ④):记账面**按端注入**(缺省 no-op = CLI 语义
258
- // 零行为变——端装配经 configureBatchSegment 覆盖;VSC 侧现形 = 把绑定档记入调用者
259
- // 写域 `_touchedFiles`)。核内不内建记账(设计裁定形态——CORE-UNIFICATION §2.13.4 #84
260
- // 「记账面按端注入」)。
261
- injectedOnWrite?.(agent, abs)
262
- return `batch_segment: appended ${body.length} characters to §${seg} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.`
263
- },
264
- }
265
- }
8
+ export {
9
+ MAX_TEXT_CHARS,
10
+ SEGMENT_BY_ROLE,
11
+ batchDocBases,
12
+ batchDocForReview,
13
+ batchSegmentTool,
14
+ batchTool,
15
+ configureBatchSegment,
16
+ resetBatchSegment,
17
+ resolveBatchDocPath,
18
+ } from "./batch.mjs"
@@ -0,0 +1,156 @@
1
+ /**
2
+ * agent-tools/batch-skeleton.mjs — 批次档骨架模板 + 状态行机械形态单源(KD-4 拆分档)。
3
+ *
4
+ * 本档是纯常量/纯函数单源(零副作用、零 fs):`batch` 工具的 create 骨架、STATUS_WORDS
5
+ * 分段词表、状态行形态与解析全部只住这里——其他档 import 消费,**不复制字面**
6
+ * (BATCH-RECORD.md §4.10 / §4.12 / D-BR19;轮 2 #8——骨架占位状态行由 STATUS_WORDS §1 项
7
+ * 引用生成,全仓无第二份独立词表字面)。
8
+ *
9
+ * 状态行解析(readBatchStatusLine)住本档的理由:冻结门(append/status 写前判定)与
10
+ * lifecycle(status/close 写入、在飞批扫描)消费**同一个解析器**——解析单源即 gate 与
11
+ * 生命周期三 action 的判定字面零漂移。
12
+ *
13
+ * 段标题定位(sectionHeaderRe)同住本档:append 段尾定位与 status/close 段内状态行定位
14
+ * 消费**同一个段头正则**——`## §N` 形态单源(`## §20` 不误命中 §2)。
15
+ *
16
+ * F11-C(本批):**骨架死占位枚举**(TEMPLATE_PLACEHOLDERS)与残留扫描纯函数
17
+ * (findPlaceholderResidue)同住本档——与骨架模板共址即判据不会与模板脱节。
18
+ */
19
+ /**
20
+ * 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。
21
+ */
22
+ export const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
23
+ /**
24
+ * STATUS_WORDS——状态行合法关键字**分段词表常量单源**(D-BR19 / fix 轮 #1;轮 2 #3 裁定②):
25
+ * - §1 项 = gate 词表 `{ open: 进行中, closed: 已收口 }`——**gate 只消费 §1 项**(单源仍成立);
26
+ * - §2/§3/§5 各带段内生命周期词表(`done` 词 = 段属主的完成态关键字);
27
+ * - §4/§6 无项——主 agent 的 status 写域**仅 §1**(§4/§6 状态面走普通文档写,与 D-BR18 分工一致)。
28
+ * 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status)拒绝
29
+ * 内嵌另一冻结词的值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。
30
+ * emoji / 括注 / 日期后缀容忍(**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词
31
+ * 自本批起收紧为「剥装饰白名单后余核 = 关键词」,见 batch-lifecycle.mjs);
32
+ * 全词表并集 = 各段项去重集合(note 字段零命中的判据面——括注永不误触 gate)。
33
+ */
34
+ export const STATUS_WORDS = Object.freeze({
35
+ 1: Object.freeze({ open: "进行中", closed: "已收口" }),
36
+ 2: Object.freeze({ open: "进行中", done: "设计完成" }),
37
+ 3: Object.freeze({ open: "进行中", done: "评审完成" }),
38
+ 5: Object.freeze({ open: "进行中", done: "实施完成" }),
39
+ })
40
+
41
+ /** 身份 → 可写段号(append 段白名单的**子代理面**——原 batch-segment.mjs 逐字迁移;主 agent
42
+ * 的 depth-0 白名单(append = §1/§4/§6 · status = §1——轮 2 #3 裁定②)由身份判据在
43
+ * batch.mjs / batch-lifecycle.mjs 分支,不经本表。旧 batch-segment.mjs 导出面等价保持——
44
+ * 本符号经 batch.mjs re-export(过渡 shim 面零触)。 */
45
+ export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
46
+
47
+ /** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
48
+ export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
49
+
50
+ /**
51
+ * 骨架死占位**枚举单源**(F11-C 判据面——create 骨架产出过的全部死占位字面):
52
+ * - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
53
+ * 建档后、本档首个 append/status 之前);
54
+ * - `<BATCH-ID>` / `<讨论来源>` = 改形**删除**的两处旧占位(create 不再产出;旧档残留同判)。
55
+ * 判据 = 枚举而非泛形正则:`<[^>]{1,40}>` 一类泛形会误杀正文合法尖括号(`Error: <message>` /
56
+ * 泛型 `Array<T>` / 讨论字面本身)与 `<§N 模板占位:…>` 合法暂存行(append 不删行——段内留存
57
+ * 是常态)——设计档 `TOOLS.md` 6.15 裁定点④。
58
+ */
59
+ export const TEMPLATE_PLACEHOLDERS = ["<BATCH-ID>", "<讨论来源>", "#<编号>", "<板块>"]
60
+
61
+ /**
62
+ * 死占位残留扫描(F11-C——append/status 落笔前的机检;**纯函数 · 零 fs**)。
63
+ * 判定域 = **档头**(档首行起至首个 `## §N` 段头前)+ **本次目标段**段内——他段模板占位不归
64
+ * 本段作者管(一段一作者:§2 作者扫 §5 的占位 = 越权)。
65
+ * @param {string} src — 档全文
66
+ * @param {number|string} seg — 本次目标段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
67
+ * @returns {Array<{line: number, text: string, where: "header"|"section"}>} 残留列表(空 = 无残留);
68
+ * `line` = 所在判定域内 1-based 行号(档头域自档首行起 · 段域自 `## §N` 标题行起——便于定位),
69
+ * `text` = 命中行 trim 原文,`where` = 命中域(档头 / 目标段——错误句标注位置用)
70
+ */
71
+ export function findPlaceholderResidue(src, seg) {
72
+ const out = []
73
+ const scan = (text, where) => {
74
+ text.split("\n").forEach((line, i) => {
75
+ if (TEMPLATE_PLACEHOLDERS.some((ph) => line.includes(ph))) out.push({ line: i + 1, text: line.trim(), where })
76
+ })
77
+ }
78
+ const firstSection = /^## §\d/m.exec(src)
79
+ scan(src.slice(0, firstSection ? firstSection.index : 0), "header")
80
+ const hdr = sectionHeaderRe(seg).exec(src)
81
+ if (hdr) {
82
+ const nextRe = /^## §\d/gm
83
+ nextRe.lastIndex = hdr.index + hdr[0].length
84
+ const next = nextRe.exec(src)
85
+ scan(src.slice(hdr.index, next ? next.index : src.length), "section")
86
+ }
87
+ return out
88
+ }
89
+
90
+ /**
91
+ * 占位残留拒绝句(F11-C——append / status 两个挂点共用单源文案;逐行列残留:位置标注 + 行号 + 原文)。
92
+ * 错误句前缀 = `batch:`(本批新增错误面——与 append 迁移面的 `batch_segment:` 旧面不混)。
93
+ * @param {Array<{line:number,text:string,where:string}>} residues — findPlaceholderResidue 的返回值(非空)
94
+ * @returns {string} 完整拒绝句(调用方 throw new Error(...))
95
+ */
96
+ export function placeholderResidueError(residues) {
97
+ const where = (r) => (r.where === "header" ? "档头" : "目标段")
98
+ const list = residues.map((r) => ` ${where(r)} line ${r.line}: ${r.text}`).join("\n")
99
+ return "batch: 骨架死占位残留 — the batch record still carries skeleton placeholders (档头 or your target section). " +
100
+ "Fill them before writing: create ⇒ the main agent fills the header's 台账编号 / 板块 placeholders ⇒ the record's first append/status opens. " +
101
+ `Nothing was written. Residue:\n${list}`
102
+ }
103
+
104
+
105
+ /**
106
+ * §1 状态行解析(冻结门与 lifecycle 共用的**单源解析器**——原 batch-segment.mjs 逐字迁移,
107
+ * 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1 段内
108
+ * `**状态行**:` 前缀行(§1 边界 = `## §1` 标题到下一 `## §` 标题;其他段内「状态行」字样
109
+ * 不参与判定)。判定只认关键字(emoji / 括号装饰 / 日期后缀容忍):含「已收口」→ "closed"
110
+ * (冻结优先)、含「进行中」→ "open";无 §1 / 无该行 / 两关键字皆不命中 → "unknown"
111
+ * (fail-closed 视为冻结)。
112
+ */
113
+ export function readBatchStatusLine(src) {
114
+ const s1 = /^## §1(?=\s|$)/m.exec(src)
115
+ if (!s1) return "unknown"
116
+ const nextRe = /^## §\d/gm
117
+ nextRe.lastIndex = s1.index + s1[0].length
118
+ const next = nextRe.exec(src)
119
+ const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
120
+ const m = body.split("\n").map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
121
+ if (!m) return "unknown"
122
+ const value = m[1].trim()
123
+ if (value.includes(STATUS_WORDS[1].closed)) return "closed"
124
+ if (value.includes(STATUS_WORDS[1].open)) return "open"
125
+ return "unknown"
126
+ }
127
+
128
+ /**
129
+ * create 骨架模板(BATCH-RECORD.md §4.10 行为规格的**代码单源**——红线:模板只覆盖骨架与
130
+ * 占位,不模板化内容):`## §1–§6` 段头(含职责署名)+ 档头 boilerplate(六段一段一作者句 +
131
+ * 编制行 + 台账/前情指针行)+ §1 段内**占位状态行**(含 gate 合法关键字「进行中」——建档即过
132
+ * gate;整行不含「已收口」——gate 已收口优先,占位行不得携带冻结词)+ 各段模板子标题占位。
133
+ * 已知实参替换:date / topic / **source** / prev(§4.11 参数面 + F11-B——source 必填、prev 传
134
+ * 入值过幂等剥前缀,归一化在 lifecycle 面);档头 `#<编号>` / `<板块>` 仍保持占位字面(台账
135
+ * 登记 = 主 agent 既有义务,create 不代建——填充时点 = 建档后、本档首个 append/status 之前)。
136
+ * 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
137
+ */
138
+ export function batchSkeleton({ date, topic, source, prev }) {
139
+ return [
140
+ `# ${date} · ${topic}`,
141
+ "> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(eng-designer)· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(eng-coder)· §6 验证与收口(父代理)。",
142
+ `> 编制:主 agent · ${date} · 来源 = ${source}。`,
143
+ `> 台账 = #<编号>(<板块> · 归批)。前情 = ${prev}。`,
144
+ "## §1 讨论(主 agent)",
145
+ `**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
146
+ "<§1 模板占位:本批条目 / 关键判据 / 授权口径>",
147
+ "## §2 批次任务与设计(eng-designer)",
148
+ "**状态行**:(eng-designer 写入时更新)",
149
+ "<§2 模板占位:本批条目(覆盖) / 设计档落点 / 机制设计 / 受影响文件与测试面 / 验收对照 / 关键决策 / 上抛项>",
150
+ "## §3 设计评审(评审子代理)",
151
+ "## §4 用户批准(主 agent)",
152
+ "## §5 实施记录(eng-coder)",
153
+ "## §6 验证与收口(父代理)",
154
+ "",
155
+ ].join("\n")
156
+ }