@thincoder/core 0.10.11 → 0.10.12

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.
@@ -13,6 +13,10 @@
13
13
 
14
14
  import { randomUUID } from "node:crypto"
15
15
  import { tokenExpiryMs } from "../token-ttl.mjs"
16
+ // 批归属派生(DESIGN-TOKEN-SETTLEMENT §10)——判据与清点面同源(同一个 `lastIndexOf("batches")`)。
17
+ // 本档 ⇄ `design-slots.mjs` 为**静态环**(对方取 `effectiveTokenTtlMs`):两侧绑定只在函数体内
18
+ // 运行时使用,求值期零顶层调用 ⇒ 环安全(与 token-ttl ⇄ session 同构)。
19
+ import { batchAbsPath } from "./design-slots.mjs"
16
20
 
17
21
  /**
18
22
  * F2c/F2e (§29.1 2026-09-07): the engine-generated Approved suffix — ONE builder
@@ -120,8 +124,10 @@ const VERDICT_GATE_MARK = (reason) => `裁定非通过——token 未签发 (rev
120
124
  * IS the verdict — the advisor echoes it only on approval. On echo (full string
121
125
  * OR truncated — uuid present; §5.1①): slot the token under designId (always the
122
126
  * FULL token; + eng-coder gate flag; single-value mirror retired per
123
- * DESIGN-TOKEN-SETTLEMENT D3) and return the clean output with the Approved
124
- * suffix (truncated echo: the M2 marker sits before the suffix); the review
127
+ * DESIGN-TOKEN-SETTLEMENT D3), record its batch attribution (`_engDesignTokenBatches`
128
+ * — the reviewed doc-set's `batches/` file, §10 E1; no batch in the set ⇒ no
129
+ * attribution) and return the clean output with the
130
+ * Approved suffix (truncated echo: the M2 marker sits before the suffix); the review
125
131
  * instance CLOSES (a later review of the same doc-set starts a fresh full
126
132
  * review). On non-echo: strip every echo form (single source §5.1③) and return
127
133
  * the findings text + the constant M1 marker — slots stay untouched
@@ -164,6 +170,16 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
164
170
  // Echoed the token → review passed. Issue it to the parent for eng-coder.
165
171
  agent._engDesignTokens ??= new Map()
166
172
  agent._engDesignTokens.set(run.designId, designToken)
173
+ // §10 E1(DESIGN-TOKEN-SETTLEMENT):批归属旁列同拍写——派生 = 本评审文档集 `docSetKey` 中
174
+ // `batches/` 段档的绝对路径(零新输入)。文档集不含批档 ⇒ 归属 = 无(不猜):**删**该 designId
175
+ // 的旧行——否则一次改了范围的复评会让新槽继承上一轮的归属,收口时消费掉本不该消费的授权。
176
+ const batchAbs = batchAbsPath(run.docSetKey)
177
+ if (batchAbs) {
178
+ agent._engDesignTokenBatches ??= new Map()
179
+ agent._engDesignTokenBatches.set(run.designId, batchAbs)
180
+ } else if (agent._engDesignTokenBatches instanceof Map) {
181
+ agent._engDesignTokenBatches.delete(run.designId)
182
+ }
167
183
  // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
168
184
  // 只写多槽 Map(AC3 零镜像写);settle 落盘由调用方(settleAdvisorRun)当场做。
169
185
  // Unlock the dispatch design gate for eng-coder SELF-review (defense-in-depth —
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * 目标面(KD-M1-39——判据复用零新解析器):`read` = `projectView`;`init` / `write` =
21
21
  * `projectRootView`(工作区 ⇒ 拒 + 下辖项目全列 + 显式 `target` 指引);`resolveProjectRoot` 对
22
- * 工作区塌缩 `null` 再回落 cwd ⇒ 写 / 建两动作**不得**直用。
22
+ * 工作区塌缩 `null`(**回落发生在调用方 `writeRoot`,非本函数**)⇒ 写 / 建两动作**不得**直用。
23
23
  *
24
24
  * 写路径复用(零新写路径):落盘恒经 `writeManifest` / `initManifest`——`writer` 恒显式 `'main'`
25
25
  * (缺省 `'subagent'` = 写门 fail-closed 缺省,KD-M1-3 / AC-M1-5 零改)。
@@ -87,7 +87,7 @@ export function resolveReviewDocPaths(documents, cwd) {
87
87
  if (!abs && view.state !== "ok") { invalid.push({ doc, resolved: null, reason: "no-project-root" }); continue }
88
88
  const p = abs ? doc : resolve(view.root, doc)
89
89
  if (!abs && !isInside(p, view.root)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
90
- if (abs && !owningProject(p)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
90
+ if (abs && !owningProject(p, cwd)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
91
91
  if (!isDocFile(p)) { invalid.push({ doc, resolved: p, reason: "not-doc-file" }); continue }
92
92
  resolved.set(doc, p)
93
93
  }
package/ledger-cmd.mjs CHANGED
@@ -40,9 +40,9 @@ function isRowAged(row, now) {
40
40
  * `batch` 同形同口径)。 */
41
41
  const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
42
42
 
43
- /** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd)?.root
44
- * ?? resolve(cwd ?? ".")`。 */
45
- const baseOf = (cwd) => resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
43
+ /** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd, boundary)?.root
44
+ * ?? resolve(cwd ?? ".")`。`boundary` = 会话锚(与 `openLedger` 同一枚上界——同一笔动作的判据不能两界)。 */
45
+ const baseOf = (cwd, boundary) => resolveLedgerRoot(cwd, boundary)?.root ?? resolve(cwd ?? ".")
46
46
 
47
47
  /** `evidence` 超限读数(设计档 §5 P4——**读数非闸门**):缺 / 空 ⇒ false;`query` 逐行携(含归档行——
48
48
  * 行值事实);`count` 的统计集 = 未决四态(§7.1)。 */
@@ -92,8 +92,8 @@ export function overdueOf(row, { now = Date.now(), base = process.cwd() } = {})
92
92
  * (§3.7 出口态字串 ∥ null——机判单源 `overdueOf`);`task_book` = 第 5 枚等值过滤(**原值等值**——列串直比,与 §3.3 选行的
93
93
  * 「文件部分解析同址」两径口径不同、各自单源);`trigger` 过滤仅三枚举等值(`NULL` 不过滤);
94
94
  * `now` = 注入缝(确定性用例)。 */
95
- export function ledgerQuery({ cwd, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
96
- const db = openLedger(cwd, { readOnly: true })
95
+ export function ledgerQuery({ cwd, boundary, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
96
+ const db = openLedger(cwd, { readOnly: true, boundary })
97
97
  if (!db) return []
98
98
  try {
99
99
  const where = [], args = []
@@ -103,7 +103,7 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
103
103
  if (trigger) { where.push("trigger = ?"); args.push(trigger) }
104
104
  if (task_book) { where.push("task_book = ?"); args.push(task_book) }
105
105
  const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
106
- const base = baseOf(cwd)
106
+ const base = baseOf(cwd, boundary)
107
107
  return rows.map((r) => ({ ...r, aged: isRowAged(r, now), evidenceOversize: isEvidenceOversize(r), taskBookOnDisk: pointerOnDisk(r, base), overdue: overdueOf(r, { now, base }) }))
108
108
  } finally { db.close() }
109
109
  }
@@ -113,14 +113,14 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
113
113
  * 机判单源);`evidenceOversize` = 其中 `evidence` 超限行数(§5)。**一次 SELECT + 逐行判**——
114
114
  * `evidenceOversize` 与 `overdue` 同一次遍历(零额外 IO;成本模型 = §7.2);不设缓存(读数须反映
115
115
  * 即时盘面)。库不在 = 三零。 */
116
- export function ledgerCount({ cwd, task_book = null } = {}) {
117
- const db = openLedger(cwd, { readOnly: true })
116
+ export function ledgerCount({ cwd, boundary, task_book = null } = {}) {
117
+ const db = openLedger(cwd, { readOnly: true, boundary })
118
118
  if (!db) return { count: 0, overdue: 0, evidenceOversize: 0 }
119
119
  try {
120
120
  const where = [`status IN (${PENDING_STATUSES.map(() => "?").join(",")})`], args = [...PENDING_STATUSES]
121
121
  if (task_book) { where.push("task_book = ?"); args.push(task_book) }
122
122
  const rows = db.prepare(`SELECT * FROM items WHERE ${where.join(" AND ")}`).all(...args)
123
- const now = Date.now(), base = baseOf(cwd)
123
+ const now = Date.now(), base = baseOf(cwd, boundary)
124
124
  let overdue = 0, oversize = 0
125
125
  for (const r of rows) {
126
126
  if (overdueOf(r, { now, base }) !== null) overdue++
@@ -132,8 +132,8 @@ export function ledgerCount({ cwd, task_book = null } = {}) {
132
132
 
133
133
  /** `evidence` 超限行数(设计档 §7.1 导出面——`count` 回报读数**同一单源**:本函数为其薄委托,
134
134
  * 不另立第二判定面)。 */
135
- export function ledgerEvidenceOversize({ cwd, task_book = null } = {}) {
136
- return ledgerCount({ cwd, task_book }).evidenceOversize
135
+ export function ledgerEvidenceOversize({ cwd, boundary, task_book = null } = {}) {
136
+ return ledgerCount({ cwd, boundary, task_book }).evidenceOversize
137
137
  }
138
138
 
139
139
  /** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
@@ -149,7 +149,7 @@ const namesPointer = (v) => v != null && String(v).trim() !== ""
149
149
  * 判位 = 与迁移表判同层(落盘前);文案前缀 = 调用函数名;基准 = §6.1 口径 2(根推导单源同子表达式)。
150
150
  * @param {{status:string, trigger:string|null, taskBook:string|null, wroteTaskBook:boolean, wroteBatchTrigger:boolean}} face 本笔写入面
151
151
  */
152
- function assertTaskBookGate(cwd, fnName, face) {
152
+ function assertTaskBookGate(cwd, fnName, face, boundary) {
153
153
  const { status, trigger, taskBook, wroteTaskBook, wroteBatchTrigger } = face
154
154
  if ((status === "在途" || status === "待核销" || trigger === "归批") && !namesPointer(taskBook)) {
155
155
  throw new Error(`${fnName}:task_book 必填(在途 / 待核销 / 归批 须携任务书指针)`)
@@ -157,14 +157,14 @@ function assertTaskBookGate(cwd, fnName, face) {
157
157
  if (!wroteTaskBook && !wroteBatchTrigger) return // 臂② 之外零判:旧指针所指档已清不阻任何此类写入
158
158
  const part = filePartOf(taskBook)
159
159
  if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
160
- const base = baseOf(cwd) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
160
+ const base = baseOf(cwd, boundary) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
161
161
  const { ok, abs } = resolveDeclaredRef(base, part)
162
162
  if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
163
163
  }
164
164
 
165
165
  /** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。`row.resume_when` 可空(§3.4)。 */
166
- export function ledgerAdd({ cwd, row }) {
167
- const db = openLedger(cwd, { create: true })
166
+ export function ledgerAdd({ cwd, row, boundary }) {
167
+ const db = openLedger(cwd, { create: true, boundary })
168
168
  try {
169
169
  // 结果行状态 = INSERT 硬编码「待讨论」⇒ 臂① 的 `status` 半幅当下不触;`trigger=归批` 半幅与臂② 照判
170
170
  assertTaskBookGate(cwd, "ledgerAdd", {
@@ -173,7 +173,7 @@ export function ledgerAdd({ cwd, row }) {
173
173
  taskBook: row.task_book ?? null,
174
174
  wroteTaskBook: namesPointer(row.task_book),
175
175
  wroteBatchTrigger: row.trigger === "归批",
176
- })
176
+ }, boundary)
177
177
  const now = nowIso()
178
178
  const r = db.prepare(`INSERT INTO items (kind, status, title, board, req_doc, task_book, evidence, trigger, resume_when, created_at, updated_at) VALUES (?, '待讨论', ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
179
179
  .run(row.kind, row.title, row.board ?? null, row.req_doc ?? null, row.task_book ?? null, row.evidence ?? null, row.trigger ?? null, row.resume_when ?? null, now, now)
@@ -218,7 +218,7 @@ function assertEvidenceValue(fnName, evidence) {
218
218
  * (含两拒面)→ 写门 → UPDATE(判序 = §13.9 逐字未变)。`to` = `patch.status ?? 行现态`
219
219
  * ——批量形纯字段补丁(不携 `status`)时 `to === row.status` ⇒ 迁移表不判、`status` 零写(§3.3)。
220
220
  * `resume_when` = 普通可空字段补丁(`patch.resume_when ?? 行值`——§3.4)。 */
221
- function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
221
+ function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary) {
222
222
  const to = patch.status ?? row.status
223
223
  if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
224
224
  throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
@@ -237,7 +237,7 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
237
237
  taskBook: nextTaskBook,
238
238
  wroteTaskBook: namesPointer(patch.task_book),
239
239
  wroteBatchTrigger: patch.trigger === "归批",
240
- }) // → 写门 → UPDATE
240
+ }, boundary) // → 写门 → UPDATE
241
241
  const now = nowIso()
242
242
  db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, resume_when = ?, executor = ?, updated_at = ? WHERE id = ?`)
243
243
  .run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, nextEvidence, patch.trigger ?? row.trigger, patch.resume_when ?? row.resume_when, nextExecutor, now, row.id)
@@ -249,12 +249,12 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
249
249
  * 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
250
250
  * `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
251
251
  * / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。**单行形**(`id`)——逐行落笔 = `applyRowUpdate`。 */
252
- export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
253
- const db = openLedger(cwd, { create: true })
252
+ export function ledgerUpdate({ cwd, id, patch, executorSessionId, boundary }) {
253
+ const db = openLedger(cwd, { create: true, boundary })
254
254
  try {
255
255
  const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
256
256
  if (!row) throw new Error(`ledgerUpdate:行 ${id} 不存在`)
257
- const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId)
257
+ const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary)
258
258
  return { id: rowId }
259
259
  } finally { db.close() }
260
260
  }
@@ -286,9 +286,9 @@ function applyRowClose(db, row, status, evidence, evidenceReplace) {
286
286
  * 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 → 追认门(原样——判**结果值**缺 /
287
287
  * 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3 收口两源)。**单行形**
288
288
  * (`id`)——逐行落笔 = `applyRowClose`。 */
289
- export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
289
+ export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, boundary }) {
290
290
  if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
291
- const db = openLedger(cwd, { create: true })
291
+ const db = openLedger(cwd, { create: true, boundary })
292
292
  try {
293
293
  db.exec("BEGIN")
294
294
  try {
@@ -330,16 +330,16 @@ function resolveBatchRows(db, base, abs) {
330
330
  * 并抛(零行改)——故「部分落地」只由逐行门拒产生,「中途失败」不留半批。
331
331
  * `BEGIN IMMEDIATE`:写锁一次先把(逐行不升级锁 ⇒ 事务级失败在首行之前即失败)。
332
332
  * @returns {{batch: string, status: string|null, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
333
- export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId }) {
334
- const db = openLedger(cwd, { create: true })
333
+ export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, boundary }) {
334
+ const db = openLedger(cwd, { create: true, boundary })
335
335
  try {
336
- const base = baseOf(cwd)
336
+ const base = baseOf(cwd, boundary)
337
337
  const abs = assertBatchArg("ledgerUpdate", base, batch)
338
338
  db.exec("BEGIN IMMEDIATE")
339
339
  try {
340
340
  const applied = [], skipped = []
341
341
  for (const row of resolveBatchRows(db, base, abs)) {
342
- try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId).id) }
342
+ try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary).id) }
343
343
  catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
344
344
  }
345
345
  db.exec("COMMIT")
@@ -355,11 +355,11 @@ export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId })
355
355
  * (`evidenceValue` 同源)。回执 `status` 恒 `已核销`(批量形缺省 = 勾销单值;携则守卫已收窄为
356
356
  * `{已核销}`——§3.2 `whenPresent`)。事务语义同 `ledgerUpdateBatch`。
357
357
  * @returns {{batch: string, status: string, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
358
- export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace }) {
358
+ export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, boundary }) {
359
359
  const status = "已核销"
360
- const db = openLedger(cwd, { create: true })
360
+ const db = openLedger(cwd, { create: true, boundary })
361
361
  try {
362
- const base = baseOf(cwd)
362
+ const base = baseOf(cwd, boundary)
363
363
  const abs = assertBatchArg("ledgerClose", base, batch)
364
364
  db.exec("BEGIN IMMEDIATE")
365
365
  try {
@@ -387,13 +387,14 @@ const REANCHOR_STATUSES = ["待讨论", "待设计", "在途"]
387
387
 
388
388
  /** 本批行集(只读,任意状态——设计档 §7.1):批身份单源 = `resolveBatchRows` 解析同址(零二写);
389
389
  * 逐行携读数 `taskBookOnDisk`(不入库、不入 `DATA_COLUMNS`)。`batch` 不可解析 ∥ 无根 ⇒ 抛(同族
390
- * `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd` 基准——不用会话锚)。 */
391
- export function ledgerBatchRows({ cwd, batch } = {}) {
392
- const derived = cwd == null ? null : resolveLedgerRoot(cwd)
390
+ * `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd` 基准——不用会话锚);
391
+ * `boundary` = 上溯上界(会话锚——与工具面同一枚界;缺省 = 进程 cwd)。 */
392
+ export function ledgerBatchRows({ cwd, batch, boundary } = {}) {
393
+ const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
393
394
  if (!derived) throw new Error(`ledgerBatchRows:批档所属项目根不可推导(${cwd ?? "(缺 cwd)"})`)
394
395
  const base = derived.root
395
396
  const abs = assertBatchArg("ledgerBatchRows", base, batch) // 失败语义先于「库不在 ⇒ 空集」(不可解析就是不可解析)
396
- const db = openLedger(base, { readOnly: true })
397
+ const db = openLedger(base, { readOnly: true, boundary })
397
398
  if (!db) return []
398
399
  try {
399
400
  return resolveBatchRows(db, base, abs).map((r) => ({ ...r, taskBookOnDisk: pointerOnDisk(r, base) }))
@@ -406,14 +407,15 @@ export function ledgerBatchRows({ cwd, batch } = {}) {
406
407
  * 复句追加 `evidence`。单事务:任一步失败 ⇒ 整集 `ROLLBACK`(零行改)。**不抛**——失败经返回形 `failed`
407
408
  * 报出(`null` 常态);`cwd` 无根 ⇒ 不取行、不重锚(**不落兜底基准**——落了就是静默零命中)。
408
409
  * `updated_at` 零触(机器重锚不是人工推进的痕迹——行龄读数照旧)。
410
+ * `boundary` = 上溯上界(会话锚——与工具面 / `ledgerBatchRows` 同一枚界;缺省 = 进程 cwd)。
409
411
  * @returns {{batch:string, reanchored:{id:number,status:string,title:string,sentenceTo:"resume_when"|"evidence"}[], alreadyAnchored:{id:number,status:string,title:string}[], failed:string|null}} */
410
- export function ledgerReanchorBatch({ cwd, batch } = {}) {
412
+ export function ledgerReanchorBatch({ cwd, batch, boundary } = {}) {
411
413
  const failedResult = (why) => ({ batch, reanchored: [], alreadyAnchored: [], failed: why })
412
- const derived = cwd == null ? null : resolveLedgerRoot(cwd)
414
+ const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
413
415
  if (!derived) return failedResult("批档所属项目根不可推导")
414
416
  const base = derived.root
415
417
  let db
416
- try { db = openLedger(base) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
418
+ try { db = openLedger(base, { boundary }) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
417
419
  if (!db) return { batch, reanchored: [], alreadyAnchored: [], failed: null } // 库不在 ⇒ 零行可锚
418
420
  try {
419
421
  let abs
package/ledger-db.mjs CHANGED
@@ -6,8 +6,9 @@
6
6
  * 靠 DDL(AC-M2-2),不靠应用层 if。归档 = 软删除(状态值,无物理 DELETE——KD-M2-4)。
7
7
  * 本档静态 import node:sqlite ⇒ 消费侧一律动态 import 本模块链(W8 契约②——KD-M2-3)。
8
8
  *
9
- * 项目根推导单源(`resolveLedgerRoot`)+ 同级枚举(`ledgerChildren`)居本档——键 / 库位 / 根同源
10
- * (设计档 `LEDGER.md` §2.1「根解析面」/ §3.8):读写同一判据(档在盘 ∨ 以目录自身路径为键的库在盘)。
9
+ * 项目根推导单源(`resolveLedgerRoot` = 判据本体 `owningProject` 的 `basis` 形状包装)+ 同级枚举
10
+ * (`ledgerChildren`)居本档——键 / 库位 / 根同源(设计档 `LEDGER.md` §2.1「根解析面」/ §3.8):
11
+ * 读写同一判据(档在盘——项目 = 带 manifest 的目录;库在盘不算项目,台账无第二定义)。
11
12
  *
12
13
  * 落点(2026-09-17 用户裁定收正):台账库**不在项目目录**——住用户数据目录
13
14
  * `~/.thincoder/ledger/<sha1(normalizeCwd(项目根绝对路径))[:16]>.db`(工作树外、天然不进 git;
@@ -17,11 +18,11 @@
17
18
  */
18
19
  import { createHash } from "node:crypto"
19
20
  import { existsSync, mkdirSync, opendirSync } from "node:fs"
20
- import { dirname, join, resolve } from "node:path"
21
+ import { join, resolve } from "node:path"
21
22
  import { DatabaseSync } from "node:sqlite"
22
23
 
23
24
  import { configDir } from "./config-io.mjs"
24
- import { MANIFEST_REL, projectRootView } from "./manifest.mjs"
25
+ import { MANIFEST_REL, owningProject, projectRootView } from "./manifest.mjs"
25
26
  // 归一步单源(KD-LN1——直引,不抽新 util:先例 = peer-domains.mjs / traces/trace-store.mjs)。
26
27
  import { normalizeCwd } from "./session-slots.mjs"
27
28
 
@@ -45,27 +46,24 @@ export function ledgerKey(root) {
45
46
  return createHash("sha1").update(normalizeCwd(root)).digest("hex").slice(0, 16)
46
47
  }
47
48
 
48
- /** 键控库文件路径(根 → 库位)——**内部形**:根推导内部判「库在盘」时用它,**不得经 `ledgerDbPath`**
49
- * (互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
49
+ /** 键控库文件路径(根 → 库位)——**内部形**:族枚举 / 库路径解析内部构造库位时用它,**不得经
50
+ * `ledgerDbPath`**(互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
50
51
  const keyedDbFile = (root) => join(ledgerDir, `${ledgerKey(root)}.db`)
51
52
 
52
- /** 「已注册」判据(设计档 §2.1——**唯一**事实,两臂取一即成立):
53
- * 档臂 = 该目录带项目声明档 `PROJECT-MANIFEST.json`(档在盘);
54
- * 库臂 = 以该目录自身路径为键的键控库在盘 ∧ 该目录**非工作区态**(键控库存在 ≠ 可作项目);
55
- * 工作区判据只在库在盘时才查(`projectRootView` 含 fs 扫描——档臂已中即短路)。 */
53
+ /** 「已注册」判据(**唯一**事实):该目录带项目声明档 `PROJECT-MANIFEST.json`。
54
+ * 项目 = 带 manifest 的目录(`docs/core/requirements/ENGINEERING-MODE-V2-SPEC-MANIFEST.md:68`)——
55
+ * 台账没有自己的「项目」定义:台账根 = 项目根,一个判据。 */
56
56
  function isRegistered(dir) {
57
- if (existsSync(join(dir, MANIFEST_REL))) return true
58
- if (!existsSync(keyedDbFile(dir))) return false
59
- return projectRootView(dir).state !== "workspace"
57
+ return existsSync(join(dir, MANIFEST_REL))
60
58
  }
61
59
 
62
60
  /** 同级枚举上限(成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集)。
63
- * 2026-10-16 批 ledger-root-derivation 自 `ledger.mjs` 随迁(根推导收敛序 ∥ 族发现同级枚举同源)。 */
61
+ * 2026-10-16 批 ledger-root-derivation 自 `ledger.mjs` 随迁(同级枚举:族发现 ∥ 轻通道闸候选枚举同源)。 */
64
62
  export const MAX_SIBLING_SCAN = 100
65
63
 
66
- /** 目录下**已注册**(档 ∨ 库——§2.1 单谓词)的直接子目录(目录名升序;不可读 ∥ 超限 ⇒ 空集;
67
- * **惰性枚举 + 上限**——大目录零全量扫描)。消费面 = 根推导收敛序 ∥ 族发现 ∥ 轻通道闸候选枚举。
68
- * `ledger` = 该子目录根的键控库路径(谓词含档臂 ⇒ 可能尚未在盘)。2026-10-16 批自 `ledger.mjs` 随迁。 */
64
+ /** 目录下**已注册**(档在盘——§2.1 单谓词)的直接子目录(目录名升序;不可读 ∥ 超限 ⇒ 空集;
65
+ * **惰性枚举 + 上限**——大目录零全量扫描)。消费面 = 族发现 ∥ 轻通道闸候选枚举。
66
+ * `ledger` = 该子目录根的键控库路径(推算值 ⇒ 可能尚未在盘)。2026-10-16 批自 `ledger.mjs` 随迁。 */
69
67
  export function ledgerChildren(dir) {
70
68
  const out = []
71
69
  let handle
@@ -86,48 +84,49 @@ export function ledgerChildren(dir) {
86
84
  return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
87
85
  }
88
86
 
89
- /** 项目根推导**单源**(设计档 §2.1「根解析面」/ §3.8——读写同一判据、同一实现):
90
- * 自 `target` 沿**祖先链**上溯取最近「已注册」目录——**以会话锚(`process.cwd()`)为界,越界即停**:
91
- * 界内上溯 = 子目录归到自己的项目根(应该);越过锚 = 挂到无关外层项目上(禁止)。
92
- * 命中 ⇒ `{ root, basis: 'self' | 'ancestor' }`(自身 vs 界内祖先);
93
- * 界内无命中 ∧ `descend: true` ⇒ **向下收敛**锚的直接子目录(`ledgerChildren` 同款惰性枚举 +
94
- * 上限、名序升序):恰一 ⇒ 取之;≥2 ⇒ ① 先取**有键控库**者(账已在盘 = 事实)② 该集名序首者 ⇒
95
- * `{ root, basis: 'descendant' }`(向下发现——**须报明**);零已注册子目录 ⇒ `null`(无根)。
96
- * `descend: false` ⇒ 只问界内上溯支(族发现 ∥ 标记面——容器 ∥ 工作区锚 ⇒ `null` ⇒ 族求和)。
97
- * **无自指**:内部「库在盘」判据只用 `ledgerKey` + `keyedDbFile`(不经 `ledgerDbPath`)。
98
- * @param {string} [target] 动作目标路径(目录)
99
- * @param {{descend?: boolean}} [opts] `descend` 缺省 `true`(动作面——`openLedger` / `ledgerDbPath` / 单范围读)
100
- * @returns {{root: string, basis: 'self'|'ancestor'|'descendant'}|null} 项目根 + 判据来源 / 无根(`null`) */
101
- export function resolveLedgerRoot(target, { descend = true } = {}) {
87
+ /** 项目根推导**单源**(设计档 §2.1「根解析面」/ §3.8——读写同一判据、同一实现):**判据本体的形状包装**
88
+ * = `owningProject(target, boundary)`(祖先链上溯的唯一实现——本档零自持上溯)+ `basis`。
89
+ * 起点 = `target`(动作目标路径:目录 = 自身,文件 = 其所在目录);界 = **会话锚**(越界即停——
90
+ * 不越出工作目录去挂外层项目,也不向下找、不猜);`basis` = `self`(目标自身即带档目录)∥
91
+ * `ancestor`(最近带档祖先)。「项目根报明 / 建档」由用点决定。
92
+ * **无自指**:本档不判库在盘(台账无第二定义——库在盘不算项目)。
93
+ * @param {string} [target] 动作目标路径(目录 | 文件则取其所在目录)
94
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`)
95
+ * @returns {{root: string, basis: 'self'|'ancestor'}|null} 项目根 + 判据来源 / 无根(`null`) */
96
+ export function resolveLedgerRoot(target, boundary) {
102
97
  const start = resolve(target ?? ".")
103
- let dir = start
104
- for (;;) {
105
- if (isRegistered(dir)) return { root: dir, basis: dir === start ? "self" : "ancestor" }
106
- const parent = dirname(dir)
107
- if (parent === dir) break
108
- dir = parent
109
- }
110
- if (!descend) return null
111
- const children = ledgerChildren(start)
112
- if (children.length === 0) return null
113
- if (children.length === 1) return { root: children[0].root, basis: "descendant" }
114
- const withDb = children.filter((c) => existsSync(c.ledger))
115
- return { root: (withDb.length > 0 ? withDb : children)[0].root, basis: "descendant" }
98
+ const edge = resolve(boundary ?? process.cwd())
99
+ const root = owningProject(start, edge)
100
+ return root ? { root, basis: root === start ? "self" : "ancestor" } : null
116
101
  }
117
102
 
118
- /** 无根文案(单源——设计档 §3.8「无根契约」逐字;`openLedger` 写面 ∥ `ledgerDbPath` 共用同一句)。 */
119
- const noRootMessage = (start) =>
120
- `台账:自 ${start} 未能推导项目根——该目录与本级子目录均无已注册项目(带 PROJECT-MANIFEST.json 或已有键控台账库)`
103
+ /** 无根文案(单源——`openLedger` 写面 ∥ `ledgerDbPath` ∥ 工具面读面(`ledger-tools.mjs`)共用同一句):
104
+ * **只陈述事实**——起点 + 上溯上界(界内无项目根),不写「祖先链无已注册项目」这类越界断言:
105
+ * 归属根可能就在界**之上**(界外)——那一格该句为假,读它会教人误判层级。
106
+ * 锚态 = 工作区时追报下辖候选(报明面读数 = `projectRootView` 的 `workspace` 态——
107
+ * **只供报明,不参与取根**:取根仍以会话锚为界)。
108
+ * @param {string} start 起点(动作目标路径)
109
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`)——与 `resolveLedgerRoot` 同一缺省 */
110
+ export const noRootMessage = (start, boundary) => {
111
+ const view = projectRootView(start)
112
+ const edge = resolve(boundary ?? process.cwd())
113
+ const tail = view.state === "workspace" ? `;下辖候选项目:${view.candidates.join(" ∥ ")}` : ""
114
+ return `台账:自 ${start} 未找到项目根(向上找到 ${edge} 为止)${tail}`
115
+ }
121
116
 
122
117
  /** 项目根 → 库路径(稳定键 = `ledgerKey` 前 16 位——路径唯一标识,不含分隔符)。
123
- * **归单源**(2026-10-16 批 ledger-root-derivation):根 = `resolveLedgerRoot(cwd)`——与读面 / 写面
118
+ * **归单源**(2026-10-16 批 ledger-root-derivation):根 = `resolveLedgerRoot(cwd, boundary)`——与读面 / 写面
124
119
  * 同一推导;无根 ⇒ 抛**同一**无根文案(键需根,不产第二答案)。
120
+ * `boundary` = 上溯上界(**会话锚**——设计档 §3.8「界(单源)」):调用方知道锚就传;
121
+ * 缺省 = `process.cwd()`(CLI / 进程锚 = 会话锚的调用面不必传)。
125
122
  * v2 键归一(2026-09-25 批):盘符本大写(CLI)⇒ 键恒等零变;小写盘符(VSC `uri.fsPath`)
126
- * 并入同键 ⇒ 同项目两端同库(收正前 = 同项目两键两库)。 */
127
- export function ledgerDbPath(cwd) {
123
+ * 并入同键 ⇒ 同项目两端同库(收正前 = 同项目两键两库)。
124
+ * @param {string} [cwd] 动作目标路径
125
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`) */
126
+ export function ledgerDbPath(cwd, boundary) {
128
127
  const start = resolve(cwd ?? ".")
129
- const derived = resolveLedgerRoot(start)
130
- if (!derived) throw new Error(noRootMessage(start))
128
+ const derived = resolveLedgerRoot(start, boundary)
129
+ if (!derived) throw new Error(noRootMessage(start, boundary))
131
130
  return keyedDbFile(derived.root)
132
131
  }
133
132
 
@@ -230,24 +229,26 @@ function markSchemaVersion(db) {
230
229
  if (v < LEDGER_SCHEMA_VERSION) db.exec(`PRAGMA user_version = ${LEDGER_SCHEMA_VERSION}`)
231
230
  }
232
231
 
233
- /** 开库(cwd = 「动作目标路径」)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
234
- * **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd)`——读写同一判据;嵌套取近者、
235
- * 容器锚向下收敛。**无根(`null`)**:写面(`create:true`)抛(fail-loud、零落盘零建库);读面空账
232
+ /** 开库(cwd = 「动作目标路径」;`boundary` = 上溯上界 / 会话锚)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
233
+ * **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd, boundary)`——读写同一判据;嵌套取近者、
234
+ * 容器锚不向下取根(无根 ⇒ 报明)。**无根(`null`)**:写面(`create:true`)抛(fail-loud、零落盘零建库);读面空账
236
235
  * (`null`——与「库不在 = 空账」同形)。
237
236
  * 读面(create=false):库文件不存在 → 返回 null(读面不建库、无副作用)。
238
237
  * 写面(create=true):库文件不存在自动建(台账目录随之创建)。
239
238
  * **只读态**(readOnly=true——读面专用):只读句柄 + 零 DDL / ALTER + items 探针(见 openReadOnly);
240
239
  * 如实副作用:旧库(缺 executor 列)**读**不再自动补列——补列归写面开库;读出行缺列按 null 归一。
241
- * 「目录不存在」的真错照抛(fail-loud——先于推导,文案逐字零改)。 */
242
- export function openLedger(cwd, { create = false, readOnly = false } = {}) {
240
+ * 「目录不存在」的真错照抛(fail-loud——先于推导,文案逐字零改)。
241
+ * `boundary` = 上溯上界(**会话锚**——设计档 §3.8「界(单源)」):工具面传 `ctx.agent.cwd`;
242
+ * 缺省 = `process.cwd()`(CLI / 进程锚 = 会话锚的调用面不必传)。 */
243
+ export function openLedger(cwd, { create = false, readOnly = false, boundary } = {}) {
243
244
  const start = resolve(cwd ?? ".")
244
245
  if (!existsSync(start)) {
245
246
  throw new Error(`项目目录不存在:${start}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
246
247
  }
247
- const derived = resolveLedgerRoot(start)
248
+ const derived = resolveLedgerRoot(start, boundary)
248
249
  if (!derived) {
249
- if (create) throw new Error(noRootMessage(start)) // 写面抛——零落盘零建库
250
- return null // 读面空账(未注册目录下旧行为亦为空 ⇒ 零回归)
250
+ if (create) throw new Error(noRootMessage(start, boundary)) // 写面抛——零落盘零建库
251
+ return null // 读面空账(两类同形:未注册目录 ∥ 目标落在界外的已注册项目——后者见 §3.8「界」;读面不抛)
251
252
  }
252
253
  const file = keyedDbFile(derived.root)
253
254
  if (!existsSync(file) && !create) return null
package/ledger-tools.mjs CHANGED
@@ -10,19 +10,50 @@
10
10
  * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
+ import { existsSync } from "node:fs"
14
+ import { isAbsolute, dirname, join, resolve } from "node:path"
13
15
  import { ledgerAdd, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerQuery, ledgerUpdate, ledgerUpdateBatch } from "./ledger-cmd.mjs"
14
- import { resolveLedgerRoot } from "./ledger-db.mjs"
16
+ import { noRootMessage, resolveLedgerRoot } from "./ledger-db.mjs"
15
17
  import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
16
18
 
17
- /** 工具 cwd 供值面(缺省 → 当前会话锚——「动作目标路径」;项目根由核 `resolveLedgerRoot` 推导)。 */
18
- const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
19
+ /** 指针值 → 文件路径(剥 `§N` 段号;空值跳过)。 */
20
+ const ptrPath = (v) => {
21
+ if (typeof v !== "string") return null
22
+ const p = v.split("§")[0].trim()
23
+ return p || null
24
+ }
25
+
26
+ /** 会话锚供值面(**单源**——`cwdOf` 的缺省起点与上溯上界(`boundary`)同一表达式):
27
+ * `ctx.agent.cwd` > `ctx.cwd` > 进程 cwd。界 = 会话锚(设计档 §3.8「界(单源)· 两面两界」)——
28
+ * 工具面传给 `resolveLedgerRoot` / `openLedger` 的上界恒取本值,不取 `process.cwd()`(desktop / VSC
29
+ * 宿主下两者不同:会话锚才是「不能跑到工作目录以外去」的那道界)。 */
30
+ const anchorOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
19
31
 
20
- /** 根推导**报明面**(设计档 §3.8「报明形态」):向下发现(`basis === 'descendant'`——容器锚收敛序)⇒
21
- * 对象形回执增 `root` 键;祖先支(自明路径)⇒ `null`(展开零键——回执逐字零改)。供值面 = 核同一
22
- * 单源 `resolveLedgerRoot`(工具层零自算根;单实现、多调用点)。 */
23
- const reportedRoot = (cwd) => {
24
- const derived = resolveLedgerRoot(cwd)
25
- return derived && derived.basis === "descendant" ? { root: derived.root } : null
32
+ /** 工具 cwd 供值面:**起点 = 动作目标路径**(`docs/core/requirements/ENGINEERING-MODE-V2-SPEC-MANIFEST.md:72`
33
+ * 「参数按用点解析——动作作用于哪个项目的路径,就取那个项目的档」)。
34
+ * 指针(`task_book` ∥ `req_doc` ∥ `batch`)在场 ⇒ 取该文件所在目录为起点(文件 = 其所在目录);
35
+ * 无指针的动作(`query` ∥ `count` ∥ 首次建行)才回退会话锚。项目根由核 `resolveLedgerRoot` 推导。 */
36
+ const cwdOf = (ctx, ...ptrs) => {
37
+ for (const raw of ptrs) {
38
+ const p = ptrPath(raw)
39
+ if (!p) continue
40
+ return dirname(isAbsolute(p) ? p : join(anchorOf(ctx), p))
41
+ }
42
+ return anchorOf(ctx)
43
+ }
44
+
45
+ /** 起点归一(**单源**——显式 `cwd` 与指针同基底):显式 `cwd` 在场 ⇒ 相对路径按**会话锚**解析、
46
+ * 绝对路径照用;不在场 ⇒ 按指针 / 会话锚推导(`cwdOf`)。两径同一基底,避免「界 = 会话锚、起点按进程 cwd」
47
+ * 的自相矛盾(锚 ≠ 进程目录的宿主下,相对 `cwd` 否则恒落界外 ⇒ 一律报无根)。 */
48
+ const startOf = (ctx, anchor, explicit, ...ptrs) => (explicit != null ? resolve(anchor, explicit) : cwdOf(ctx, ...ptrs))
49
+
50
+ /** 起点推导无根 ⇒ 抛(设计档 §3.8「无根契约」· **工具面**:空集与「记到别的项目」在回执上无法区分——
51
+ * 静默空集不成立)。判据 = 同一单源 `resolveLedgerRoot`(无第二套上溯);目录不存在不在此判
52
+ * (交 `openLedger` 的既有文案——两判据不混)。展示面(`buildScan` ∥ 族发现)不经本函数:
53
+ * 无根 ⇒ `ledger: null` + 零行,不抛。`boundary` = 会话锚(界与 `openLedger` 同一枚)。 */
54
+ function assertLedgerStart(start, boundary) {
55
+ if (!existsSync(start)) return
56
+ if (!resolveLedgerRoot(start, boundary)) throw new Error(noRootMessage(start, boundary))
26
57
  }
27
58
 
28
59
  // ── 工具层参数守卫(设计档 §3.2 · 2026-09-27 快车道修复 · Gitee #IKIQGK / 台账 #472) ──
@@ -105,7 +136,7 @@ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
105
136
  const READ_ACTIONS = ["query", "count"]
106
137
 
107
138
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
108
- const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
139
+ const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链取最近的项目目录(带 PROJECT-MANIFEST.json 的那一层;库在盘不算项目)。多根工作区 / 无项目目录 ⇒ 报明并给候选,不向下取子项目当根。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前会话锚解析;项目根按上溯界取——界外不挂项目。缺省 = 当前会话锚"
109
140
 
110
141
  /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。
111
142
  * `parameters` 内的键 = 交模型 / provider 的那一份声明;**组判据两键(`oneOfRequired` / `whenPresent`)
@@ -255,16 +286,18 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
255
286
  const spec = ACTION_SPECS[action]
256
287
  const rest = assertToolArgs({ name: `ledger(${action})`, ...spec }, rest0)
257
288
  // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
289
+ // 界 = 会话锚(`anchorOf` 单源)——逐 action 传 `boundary`:`openLedger` / `resolveLedgerRoot` 的上溯上界
290
+ const anchor = anchorOf(ctx)
258
291
  switch (action) {
259
292
  case "add": {
260
293
  const { cwd, ...row } = rest
261
- const target = cwd ?? cwdOf(ctx)
262
- const id = ledgerAdd({ cwd: target, row })
263
- return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
294
+ const target = startOf(ctx, anchor, cwd, row.task_book, row.req_doc)
295
+ const id = ledgerAdd({ cwd: target, row, boundary: anchor })
296
+ return JSON.stringify({ id, status: "待讨论" })
264
297
  }
265
298
  case "update": {
266
299
  const { cwd, id, batch, ...patch } = rest
267
- const target = cwd ?? cwdOf(ctx)
300
+ const target = startOf(ctx, anchor, cwd, batch, patch.task_book, patch.req_doc)
268
301
  let executorSessionId
269
302
  if (patch.executor == null) {
270
303
  // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
@@ -272,20 +305,24 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
272
305
  executorSessionId = getSessionId()
273
306
  }
274
307
  // 批级批量(§3.3)——`batch` 与 `id` 恰携其一(守卫 P7 已先裁)
275
- if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId }), ...reportedRoot(target) })
276
- return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId }), ...reportedRoot(target) })
308
+ if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId, boundary: anchor }) })
309
+ return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId, boundary: anchor }) })
277
310
  }
278
311
  case "close": {
279
- const target = rest.cwd ?? cwdOf(ctx)
312
+ const target = startOf(ctx, anchor, rest.cwd, rest.batch, rest.task_book)
280
313
  // 批级批量结算(§3.3——勾销径)
281
- if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
282
- return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
314
+ if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
315
+ return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
316
+ }
317
+ case "query": {
318
+ const target = startOf(ctx, anchor, rest.cwd, rest.task_book)
319
+ assertLedgerStart(target, anchor)
320
+ return JSON.stringify(ledgerQuery({ cwd: target, boundary: anchor, status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger, task_book: rest.task_book }), null, 2)
283
321
  }
284
- case "query":
285
- return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger, task_book: rest.task_book }), null, 2)
286
322
  case "count": {
287
- const target = rest.cwd ?? cwdOf(ctx)
288
- return JSON.stringify({ ...ledgerCount({ cwd: target, task_book: rest.task_book }), ...reportedRoot(target) })
323
+ const target = startOf(ctx, anchor, rest.cwd, rest.task_book)
324
+ assertLedgerStart(target, anchor)
325
+ return JSON.stringify({ ...ledgerCount({ cwd: target, boundary: anchor, task_book: rest.task_book }) })
289
326
  }
290
327
  default:
291
328
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)