@thincoder/core 0.10.3 → 0.10.5

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 (172) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +1 -1
  3. package/advisor/citations.mjs +29 -3
  4. package/advisor/compaction.mjs +31 -7
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +2 -1
  8. package/advisor/run.mjs +9 -5
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/assemble.mjs +1 -1
  12. package/agent/chat-call.mjs +1 -1
  13. package/agent/child-marks.mjs +2 -2
  14. package/agent/completion.mjs +3 -1
  15. package/agent/digest-account.mjs +189 -0
  16. package/agent/dispatch-gates.mjs +8 -0
  17. package/agent/dispatch.mjs +39 -18
  18. package/agent/family-tools.mjs +27 -16
  19. package/agent/helpers.mjs +12 -3
  20. package/agent/light-round.mjs +66 -0
  21. package/agent/post-turn.mjs +8 -4
  22. package/agent/record-results.mjs +4 -0
  23. package/agent/run-stages.mjs +32 -7
  24. package/agent/run-start.mjs +50 -3
  25. package/agent/setup-reminders.mjs +1 -1
  26. package/agent/setup.mjs +18 -3
  27. package/agent/subagent-event.mjs +1 -1
  28. package/agent/suspension.mjs +37 -12
  29. package/agent/timers.mjs +56 -5
  30. package/agent/turn-loop.mjs +30 -5
  31. package/agent/write-gate.mjs +20 -32
  32. package/agent-tools/advisor-async.mjs +15 -7
  33. package/agent-tools/advisor-settle.mjs +2 -2
  34. package/agent-tools/advisor.mjs +34 -33
  35. package/agent-tools/async-discard.mjs +34 -0
  36. package/agent-tools/async-settle.mjs +32 -7
  37. package/agent-tools/audit-block.mjs +1 -1
  38. package/agent-tools/bash-async.mjs +1 -1
  39. package/agent-tools/batch-lifecycle.mjs +59 -17
  40. package/agent-tools/batch-paths.mjs +24 -4
  41. package/agent-tools/batch-skeleton.mjs +59 -43
  42. package/agent-tools/batch.mjs +39 -18
  43. package/agent-tools/browser-async.mjs +198 -0
  44. package/agent-tools/checkpoint.mjs +1 -1
  45. package/agent-tools/design-slots.mjs +232 -0
  46. package/agent-tools/design-token.mjs +20 -0
  47. package/agent-tools/digest-budget.mjs +1 -1
  48. package/agent-tools/escalate-async.mjs +4 -2
  49. package/agent-tools/manifest.mjs +193 -0
  50. package/agent-tools/parent-channel.mjs +7 -4
  51. package/agent-tools/review-facts.mjs +143 -16
  52. package/agent-tools/settings.mjs +1 -1
  53. package/agent-tools/skill.mjs +1 -1
  54. package/agent-tools/subagent-actions-query.mjs +13 -6
  55. package/agent-tools/subagent-async.mjs +31 -15
  56. package/agent-tools/subagent-panel.mjs +2 -1
  57. package/agent-tools/subagent-scheduler.mjs +8 -5
  58. package/agent-tools/subagent-spawn.mjs +30 -56
  59. package/agent-tools/subagent.mjs +21 -8
  60. package/agent-tools/timer.mjs +186 -12
  61. package/agent-tools/zero-write-watch.mjs +77 -0
  62. package/agent-tools.mjs +2 -1
  63. package/agent.mjs +2 -2
  64. package/attachments.mjs +1 -1
  65. package/config-io.mjs +18 -13
  66. package/config-migrate.mjs +13 -29
  67. package/config-presets.mjs +33 -29
  68. package/config.mjs +68 -25
  69. package/context-degrade.mjs +1 -1
  70. package/context-echo.mjs +1 -1
  71. package/context-push.mjs +2 -2
  72. package/context.mjs +1 -1
  73. package/conventions.mjs +28 -8
  74. package/declaration.mjs +18 -2
  75. package/escape.mjs +1 -1
  76. package/file-links.mjs +1 -1
  77. package/file-refs.mjs +1 -1
  78. package/history-window.mjs +1 -1
  79. package/i18n.mjs +7 -0
  80. package/ledger-cmd.mjs +62 -16
  81. package/ledger-db.mjs +6 -0
  82. package/ledger-surface.mjs +49 -5
  83. package/ledger-tools.mjs +158 -87
  84. package/ledger-variant-notice.mjs +47 -0
  85. package/ledger.mjs +11 -18
  86. package/log.mjs +1 -1
  87. package/manifest-discovery.mjs +1 -1
  88. package/manifest-schema.mjs +7 -5
  89. package/manifest.mjs +5 -3
  90. package/mcp/transport-stdio.mjs +1 -1
  91. package/mcp.mjs +1 -1
  92. package/memory/code-index.mjs +1 -1
  93. package/memory/code-search.mjs +1 -1
  94. package/memory/code-sync.mjs +1 -1
  95. package/memory/docs.mjs +6 -2
  96. package/memory/file-list.mjs +1 -1
  97. package/memory/file-walk.mjs +17 -5
  98. package/memory/maintain.mjs +410 -0
  99. package/memory/memory-tool.mjs +21 -6
  100. package/memory/schema.mjs +19 -7
  101. package/memory/sweep.mjs +8 -5
  102. package/memory/sync-tail.mjs +16 -10
  103. package/model-ref.mjs +16 -13
  104. package/model-specs.mjs +21 -9
  105. package/package.json +1 -1
  106. package/peer-instances.mjs +1 -1
  107. package/permission.mjs +33 -0
  108. package/process-probe.mjs +2 -2
  109. package/prompts/advisor-design.md +4 -2
  110. package/prompts/advisor-round1.md +1 -1
  111. package/prompts/advisor-round2.md +1 -1
  112. package/prompts/advisor-round3.md +1 -1
  113. package/prompts/common.md +38 -4
  114. package/prompts/consult-base.md +1 -1
  115. package/prompts/discipline-engineering.md +32 -15
  116. package/prompts/discipline-normal.md +46 -2
  117. package/prompts/persona-eng-designer.md +6 -6
  118. package/prompts/persona-engineering.md +18 -9
  119. package/prompts/persona-normal.md +2 -0
  120. package/prompts/subagent-base.md +3 -0
  121. package/provider/anthropic.mjs +14 -2
  122. package/provider/core.mjs +6 -0
  123. package/provider/errors.mjs +28 -3
  124. package/provider/google.mjs +22 -10
  125. package/provider/list-models.mjs +3 -2
  126. package/provider/responses-request.mjs +3 -1
  127. package/provider/responses.mjs +38 -4
  128. package/provider/sse.mjs +3 -0
  129. package/provider-flows.mjs +14 -15
  130. package/proxy-chunked.mjs +102 -0
  131. package/proxy-target.mjs +54 -0
  132. package/proxy-transport.mjs +298 -0
  133. package/proxy.mjs +17 -248
  134. package/session-gc.mjs +2 -2
  135. package/session-lifecycle.mjs +18 -14
  136. package/session-slot-claims.mjs +2 -2
  137. package/session-slots-manifest.mjs +1 -1
  138. package/session-slots.mjs +3 -3
  139. package/shell-identity.mjs +107 -0
  140. package/team-ledger.mjs +282 -0
  141. package/team.mjs +223 -0
  142. package/tool-docs/advisor.md +1 -1
  143. package/tool-docs/bash.md +2 -1
  144. package/tool-docs/batch.md +1 -1
  145. package/tool-docs/browser.md +48 -0
  146. package/tool-docs/delete.md +7 -5
  147. package/tool-docs/ledger.md +6 -0
  148. package/tool-docs/manifest.md +16 -0
  149. package/tool-docs/memory.md +1 -0
  150. package/tool-docs/notify_parent.md +5 -0
  151. package/tool-docs/process.md +4 -4
  152. package/tool-docs/question.md +1 -0
  153. package/tool-docs/subagent.md +14 -3
  154. package/tool-docs/team_ledger.md +9 -0
  155. package/tool-docs/timer.md +6 -0
  156. package/tool-docs/wait_for.md +1 -0
  157. package/tools/bash.mjs +9 -4
  158. package/tools/browser.mjs +161 -0
  159. package/tools/git.mjs +1 -1
  160. package/tools/index.mjs +4 -4
  161. package/tools/ops.mjs +26 -10
  162. package/tools/patch.mjs +8 -2
  163. package/tools/repomap.mjs +1 -1
  164. package/tools/write-path.mjs +8 -6
  165. package/traces/trace-cleanup.mjs +1 -1
  166. package/traces/trace-store.mjs +1 -1
  167. package/vision-reader.mjs +10 -16
  168. package/tool-docs/ledger_add.md +0 -1
  169. package/tool-docs/ledger_close.md +0 -1
  170. package/tool-docs/ledger_count.md +0 -1
  171. package/tool-docs/ledger_query.md +0 -1
  172. package/tool-docs/ledger_update.md +0 -1
package/ledger-cmd.mjs CHANGED
@@ -11,13 +11,26 @@
11
11
  * 消费侧静态 import 会破 W8 契约②)。
12
12
  */
13
13
  import { resolve } from "node:path"
14
- import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES } from "./ledger-db.mjs"
14
+ import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES, AGING_DAYS } from "./ledger-db.mjs"
15
15
  import { resolveProjectRoot } from "./manifest.mjs"
16
16
  import { resolveDeclaredRef } from "./declaration.mjs"
17
17
 
18
- /** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。
19
- * 只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批 FR2③);库档非台账库 ⇒ 空账。 */
20
- export function ledgerQuery({ cwd, status = null, kind = null, board = null } = {}) {
18
+ /** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
19
+ * 行龄 > `AGING_DAYS` 天;行龄源 = `updated_at ?? created_at`(§5 同源);时间戳缺 / 不可解析
20
+ * ⇒ `false`(零假阳降级——与 `buildScan` 行龄口径同源)。 */
21
+ function isRowAged(row, now) {
22
+ if (!PENDING_STATUSES.includes(row.status)) return false
23
+ const t = row.updated_at ?? row.created_at
24
+ if (t == null) return false
25
+ const ms = Date.parse(String(t))
26
+ return Number.isFinite(ms) && (now - ms) / 86400000 > AGING_DAYS
27
+ }
28
+
29
+ /** 查询(只读,全角色):SELECT 行集(status / kind / board / trigger 等值过滤并取,缺省 = 全部行;id 升序)。
30
+ * 库不在 = 空账。只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批 FR2③);库档非台账库 ⇒ 空账。
31
+ * 逐行携计算字段 `aged: boolean`(§13.4——不入库、不入导出白名单 DATA_COLUMNS);`trigger` 过滤
32
+ * 仅三枚举等值(`NULL` 不过滤——批面未列,§13.2);`now` = 注入缝(确定性用例)。 */
33
+ export function ledgerQuery({ cwd, status = null, kind = null, board = null, trigger = null, now = Date.now() } = {}) {
21
34
  const db = openLedger(cwd, { readOnly: true })
22
35
  if (!db) return []
23
36
  try {
@@ -25,7 +38,9 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null } =
25
38
  if (status) { where.push("status = ?"); args.push(status) }
26
39
  if (kind) { where.push("kind = ?"); args.push(kind) }
27
40
  if (board) { where.push("board = ?"); args.push(board) }
28
- return db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
41
+ if (trigger) { where.push("trigger = ?"); args.push(trigger) }
42
+ const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
43
+ return rows.map((r) => ({ ...r, aged: isRowAged(r, now) }))
29
44
  } finally { db.close() }
30
45
  }
31
46
 
@@ -75,9 +90,11 @@ export function ledgerAdd({ cwd, row }) {
75
90
  }
76
91
 
77
92
  /** executor 目标值计算(设计档 docs/core/design/LEDGER.md §3.1——判位在 ledgerUpdate 体内:迁移表判 → 本语义 → 写门 → UPDATE)。
78
- * 优先级 = patch 显式 > 自动语义(进在途 = executorSessionId / 出在途 = NULL)> 行现值兜底 > NULL。 */
93
+ * 优先级 = patch 显式 > 自动语义(进边 = executorSessionId / 出边 = NULL)> 行现值兜底 > NULL。
94
+ * 进边集(2026-10-07 批 ledger-tool 扩面——§13.3 / KD-LT1):`待讨论 → 待设计`(批点火边)∥
95
+ * `待设计 → 在途`(实施入边)——两枚同式。 */
79
96
  function resolveExecutorTarget(row, to, patch, executorSessionId) {
80
- if (row.status === "待设计" && to === "在途") {
97
+ if ((row.status === "待讨论" && to === "待设计") || (row.status === "待设计" && to === "在途")) {
81
98
  return patch.executor ?? executorSessionId ?? row.executor ?? null
82
99
  }
83
100
  if (row.status === "在途" && (to === "待核销" || to === "已废弃")) {
@@ -86,9 +103,29 @@ function resolveExecutorTarget(row, to, patch, executorSessionId) {
86
103
  return patch.executor ?? row.executor ?? null
87
104
  }
88
105
 
106
+ /** `evidence` 值计算(设计档 LEDGER.md §13.9——update / close 两函数同源一处落):追加缺省(旧值非空 ⇒
107
+ * `旧 + "|" + 新`——「|」两侧零空格直接相接;旧空(`null` ∥ 空串 ∥ 全空白)⇒ 直写,零分隔符残留);
108
+ * `evidenceReplace === true` 与值同传 ⇒ 整段替换(旧文零保留);`evidence` 缺 / `null` ≡ 略去(零变)。
109
+ * 拒面①(旗独传——无新文可算)判于本阶段(close 侧先于追认门);核内旗判只认 `=== true`(类型严判在
110
+ * 工具层)。两值逐字落行(不裁 / 不判内容)。 */
111
+ function evidenceValue(fnName, oldValue, evidence, evidenceReplace) {
112
+ if (evidenceReplace === true && evidence == null) throw new Error(`${fnName}:evidenceReplace 须与 evidence 同传(缺新文)`)
113
+ if (evidence == null) return oldValue
114
+ const old = oldValue == null || String(oldValue).trim() === "" ? "" : String(oldValue)
115
+ return evidenceReplace === true || old === "" ? evidence : `${old}|${evidence}`
116
+ }
117
+
118
+ /** 拒面②(新值全空白——追加 ∥ 覆盖两径同判)判点(§13.9 判序):update 随值计算同拍 ∥ close 落
119
+ * 追认门之后;`evidence` 未传(`null` / 略去)⇒ 不判。 */
120
+ function assertEvidenceValue(fnName, evidence) {
121
+ if (evidence != null && String(evidence).trim() === "") throw new Error(`${fnName}:evidence 为空(非空字符串)`)
122
+ }
123
+
89
124
  /** 更新(写命令,仅主 agent):UPDATE——状态迁移前判允许迁移表(不在表内 → 拒,行不变)。
90
125
  * executor 目标值随同一 UPDATE 落列(LEDGER.md §3.1);executorSessionId = 调用会话(函数参数注入——K-LX1
91
- * 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。 */
126
+ * 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
127
+ * `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
128
+ * / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。 */
92
129
  export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
93
130
  const db = openLedger(cwd, { create: true })
94
131
  try {
@@ -98,20 +135,27 @@ export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
98
135
  if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
99
136
  throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
100
137
  }
101
- const nextTaskBook = patch.task_book ?? row.task_book
102
- assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // 判序:迁移表判 → 本门 → UPDATE
138
+ // 判序(§13.9):迁移表判 → 本语义(executor 目标值 + evidence 值计算——含两拒面)→ 写门 → UPDATE
103
139
  const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
140
+ const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
141
+ assertEvidenceValue("ledgerUpdate", patch.evidence)
142
+ const nextTaskBook = patch.task_book ?? row.task_book
143
+ assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // → 写门 → UPDATE
104
144
  const now = nowIso()
105
145
  db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, executor = ?, updated_at = ? WHERE id = ?`)
106
- .run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, patch.evidence ?? row.evidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
146
+ .run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, nextEvidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
107
147
  return { id }
108
148
  } finally { db.close() }
109
149
  }
110
150
 
111
151
  /** 收口(写命令,仅主 agent):核销两源(勾销:待核销 → 已核销;追认核销:待讨论 / 待设计 → 已核销,
112
152
  * 行 `evidence` 非空,缺 / 全空白 ⇒ 拒)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。
113
- * 判序(同函数体同层):目标集判 → 行取 → 源态判 → `evidence` 门 → UPDATE(LEDGER.md §3 收口两源)。 */
114
- export function ledgerClose({ cwd, id, status }) {
153
+ * 可选 `evidence` 参:传值 = 追加缺省(§13.9——旧空直写 / 旧非空拼 `旧|新`)、`evidenceReplace: true`
154
+ * 同传 = 整段替换;勾销 / 追认 / 撤回三径同携(写值落行,内容不判);两拒面(旗独传 @值计算 ∥
155
+ * 新值空白 @追认门后)见 `evidenceValue` / `assertEvidenceValue`。
156
+ * 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 → 追认门(原样——判**结果值**缺 /
157
+ * 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3 收口两源)。 */
158
+ export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
115
159
  if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
116
160
  const db = openLedger(cwd, { create: true })
117
161
  try {
@@ -119,12 +163,14 @@ export function ledgerClose({ cwd, id, status }) {
119
163
  try {
120
164
  const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
121
165
  if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
122
- // 判序:源态判(核销仅三源——在途不可跳)→ 追认口 `evidence` 门(行字段非空,缺 / 全空白 ⇒ 拒)
166
+ // 判序(§13.9):源态判(核销仅三源——在途不可跳)→ 值计算(拒面① 旗独传判于此)→ 追认口 `evidence` 门
123
167
  if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
124
- if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (row.evidence == null || String(row.evidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
168
+ const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
169
+ if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
170
+ assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
125
171
  const now = nowIso()
126
172
  // 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);核销两源(勾销 / 追认)executor 零触碰(非在途出边)
127
- db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, id)
173
+ db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ?, evidence = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, nextEvidence, id)
128
174
  db.exec("COMMIT")
129
175
  return { id, status }
130
176
  } catch (e) { db.exec("ROLLBACK"); throw e }
package/ledger-db.mjs CHANGED
@@ -55,6 +55,12 @@ export function ledgerDbPath(cwd) {
55
55
  /** 未决四态(「活条目」口径 = 计数单源 WHERE 集;已核销 / 已废弃 = 归档态)。 */
56
56
  export const PENDING_STATUSES = ["待讨论", "待设计", "在途", "待核销"]
57
57
 
58
+ /** 老化阈值(天——口径 = 需求档;消费面 = `buildScan`(展示,`days` 可覆盖)+ `ledgerQuery` 逐行
59
+ * `aged`(查询面,`now` 可注入))。2026-10-07 批 ledger-tool:自 `ledger.mjs` 迁入(枚举常量档单源
60
+ * ——`ledgerQuery` 计算面直引本档,消 `ledger-cmd ↔ ledger.mjs` 新环;`ledger.mjs` 同名 re-export
61
+ * 保公共面零变——LEDGER.md §13.4)。 */
62
+ export const AGING_DAYS = 30
63
+
58
64
  /** 台账库结构版本(N2 标记**单源**——`PRAGMA user_version` 落标:写面开库 `v < 本值 ⇒ 置`;
59
65
  * 读面如实回读(旧库未标 = 0);结构变更(新列 / 新表)同步升号——2026-10-03 read-data-interface 批)。 */
60
66
  export const LEDGER_SCHEMA_VERSION = 1
@@ -3,8 +3,13 @@
3
3
  *
4
4
  * 面:族扫描 → 判活批量解析 → 状态位(`state.ledger`——L1 常驻标记,范围归约 `scopeMarkerOf`
5
5
  * 单源 §7.2,渲染端消费);只读台账,本面零写。
6
+ *
7
+ * **写后即时**(2026-10-13 缺陷修复——用户原话「写了以后就要立即更新」):台账库目录
8
+ * `fs.watch` ⇒ 去抖立即重扫(同进程 ∥ 跨进程写皆覆盖);`REFRESH_MS` 周期降为兜底。
6
9
  */
10
+ import { watch } from "node:fs"
7
11
  import { buildScan, discoverFamily, REFRESH_MS, resolveExecutorStates, scopeMarkerOf } from "./ledger.mjs"
12
+ import { ledgerDirPath } from "./ledger-db.mjs"
8
13
 
9
14
  /** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。**async**(F-LX1:await 判活解析——
10
15
  * LEDGER.md §7.3.1 ④ 读面不阻塞事件循环)。 */
@@ -23,18 +28,50 @@ export async function runLedgerScan({ state, agent = null, anchor = null, render
23
28
  render()
24
29
  }
25
30
 
26
- /** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。
31
+ /** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期兜底(`REFRESH_MS`)
32
+ * + **写后即时**(台账库目录变更 ⇒ 去抖 400ms 重扫——同进程 ∥ 跨进程写皆覆盖)。
27
33
  * `tick` 异步化(F-LX1——runLedgerScan async):await 且 catch 不崩(N1)。重叠防衛:
28
- * 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。 */
34
+ * 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。
35
+ * `state.processing` 跳过仅限**定时轮**(不打断回合帧);库变更轮放行(写后立即可见 ——
36
+ * 用户 2026-10-13「写了以后就要立即更新」)。
37
+ * 评审 #5 修复(2026-10-13):① 变更轮遇在途扫描 ⇒ 记待办、落定补跑(不丢);② watcher 缺席
38
+ * (目录未建)⇒ 2s 短重试补挂(闭「首写才建」窗口)。 */
29
39
  export function startLedgerSurface(ctx = {}) {
30
40
  let disposed = false
31
41
  let timer = null
32
42
  let inflight = false
33
- const tick = async () => {
34
- if (disposed || inflight) return
35
- if (ctx.state?.processing) return // 扫描不打断回合帧
43
+ let watcher = null
44
+ let schedTimer = null
45
+ let armTimer = null
46
+ let pendingForce = false
47
+ const tick = async (force = false) => {
48
+ if (disposed) return
49
+ if (inflight) { if (force) pendingForce = true; return } // 在途扫描:变更轮记待办(落定补跑——不丢)
50
+ if (!force && ctx.state?.processing) return // 定时轮不打断回合帧
36
51
  inflight = true
37
52
  try { await runLedgerScan(ctx) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
53
+ armWatcher() // 目录可能首写才建——每轮后补挂一次(零成本:已挂则直接返)
54
+ if (pendingForce && !disposed) { pendingForce = false; void tick(true) } // 补跑被在途挤掉的变更轮
55
+ }
56
+ const onFsEvent = (_evt, name) => {
57
+ if (name && !/\.db($|-(wal|shm|journal)$)/.test(String(name))) return
58
+ if (schedTimer) clearTimeout(schedTimer)
59
+ schedTimer = setTimeout(() => { schedTimer = null; tick(true) }, 400)
60
+ schedTimer.unref?.()
61
+ }
62
+ function armWatcher() {
63
+ if (disposed || watcher) return
64
+ try {
65
+ watcher = watch(ledgerDirPath(), { persistent: false }, onFsEvent)
66
+ watcher.unref?.()
67
+ if (armTimer) { clearTimeout(armTimer); armTimer = null }
68
+ } catch {
69
+ // 目录未建(首写才建)——2s 短重试补挂(评审 #5 发现②:闭「首写不被观测」窗口)
70
+ if (!armTimer) {
71
+ armTimer = setTimeout(() => { armTimer = null; armWatcher() }, 2000)
72
+ armTimer.unref?.()
73
+ }
74
+ }
38
75
  }
39
76
  setImmediate(() => {
40
77
  if (disposed) return
@@ -42,11 +79,18 @@ export function startLedgerSurface(ctx = {}) {
42
79
  timer = setInterval(() => tick(), REFRESH_MS)
43
80
  timer.unref?.()
44
81
  })
82
+ armWatcher()
45
83
  return {
46
84
  dispose() {
47
85
  disposed = true
48
86
  if (timer) clearInterval(timer)
87
+ if (schedTimer) clearTimeout(schedTimer)
88
+ if (armTimer) clearTimeout(armTimer)
89
+ if (watcher) watcher.close()
49
90
  timer = null
91
+ schedTimer = null
92
+ armTimer = null
93
+ watcher = null
50
94
  },
51
95
  }
52
96
  }
package/ledger-tools.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  /**
2
- * ledger-tools.mjs — 台账五工具定义(查询二 = 只读全角色 / 写三 = 仅主 agent 装配)+ 工具层参数守卫。
3
- * 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473——纯搬移、语义零变;核函数留原档)。
4
- * 守卫 = **声明派生**(设计档 §3.2:枚举 / 必填 / 类型取自工具自身 `parameters`,不在守卫内复写——
5
- * D2「声明即校验」);判于核函数之前 ⇒ 非法入参零写、零库动作、零 SQLite 原文外泄。读面二具同拍
6
- * (#473:非法过滤参数 ⇒ 拒——不再「静默空集 / 绑原文」)。
7
- * 接线:族出口 = `ledger.mjs`(re-export——消费面 tools/index.mjs · agent/family-tools.mjs 零改);
8
- * 消费侧一律动态 import `ledger.mjs`(ledger-db.mjs 静态 import node:sqlite ⇒ W8 契约②)。
2
+ * ledger-tools.mjs — 台账**统一入口** `ledger`(action 分派)+ 五旧名弃用壳 + 工具层参数守卫。
3
+ * 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473);2026-10-05 统一入口批
4
+ * (台账 #923)合五工具为一:模型面恰一名 `ledger`(两变体同名单对象、schema 窄化——
5
+ * 全量 = depth-0 五 action ∥ 只读 = depth>0 二 action);旧五名 = code 面弃用壳(不入任何装配面)。
6
+ * 守卫 = **声明派生**(设计档 §3.2 / §11.3:判序① 入参归一 → ② 入口级 action 判(消费 action)→
7
+ * ③ 余键喂该 action 伪工具对象走 P2–P6——声明源 = 前身旧工具 `parameters` 逐字;helper `assertToolArgs`
8
+ * 机制沿用,「零改」面破例 = #1006 批增布尔分支(§13.9));判于核函数之前 ⇒ 非法入参零写、零库动作、
9
+ * 零 SQLite 原文外泄。
10
+ * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
11
+ * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
9
12
  */
10
13
  import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
11
- import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger_*.md(DESC 单解析面)
14
+ import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
12
15
 
13
16
  /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
14
17
  const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
@@ -60,6 +63,8 @@ function assertToolArgs(tool, args) {
60
63
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为字符串)`)
61
64
  } else if (spec.type === "number" && typeof value !== "number") {
62
65
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为数字)`)
66
+ } else if (spec.type === "boolean" && typeof value !== "boolean") {
67
+ throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为布尔)`)
63
68
  }
64
69
  if (field === "title" && typeof value === "string" && value.trim() === "") {
65
70
  throw new Error(`${name}:${field} 为空(${required ? "必填;" : ""}非空字符串)`)
@@ -68,51 +73,22 @@ function assertToolArgs(tool, args) {
68
73
  return args
69
74
  }
70
75
 
71
- // ── 工具定义(接线:查询 → tools/index.mjs 全角色面;写 → family-tools.mjs depthOnly 分支) ──
76
+ // ── 统一入口(设计档 §11.2 / §11.3) ─────────────────────────────────────────
72
77
 
73
- export const ledgerQueryTool = {
74
- name: "ledger_query",
75
- description: DESC("ledger_query"), // #15 外置:文本单点 = tool-docs/ledger_query.md(两池词表逐字保留)
76
- parameters: {
77
- type: "object",
78
- properties: {
79
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
80
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
81
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
82
- board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
83
- },
84
- additionalProperties: false,
85
- },
86
- readonly: true,
87
- async execute(args, ctx) {
88
- args = assertToolArgs(ledgerQueryTool, args)
89
- return JSON.stringify(ledgerQuery({ cwd: args.cwd ?? cwdOf(ctx), status: args.status, kind: args.kind, board: args.board }), null, 2)
90
- },
91
- }
78
+ /** action 枚举(全量 / 只读两变体;顺序 = 文档与按钮序)。 */
79
+ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
80
+ const READ_ACTIONS = ["query", "count"]
92
81
 
93
- export const ledgerCountTool = {
94
- name: "ledger_count",
95
- description: DESC("ledger_count"), // #15 外置:文本单点 = tool-docs/ledger_count.md
96
- parameters: {
97
- type: "object",
98
- properties: { cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" } },
99
- additionalProperties: false,
100
- },
101
- readonly: true,
102
- async execute(args, ctx) {
103
- args = assertToolArgs(ledgerCountTool, args)
104
- return JSON.stringify({ count: ledgerCount({ cwd: args.cwd ?? cwdOf(ctx) }) })
105
- },
106
- }
82
+ /** cwd 参数描述(五前身声明同文——文本单点)。 */
83
+ const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
107
84
 
108
- export const ledgerAddTool = {
109
- name: "ledger_add",
110
- description: DESC("ledger_add"), // #15 外置:文本单点 = tool-docs/ledger_add.md(两池词表逐字保留)
111
- parameters: {
85
+ /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
86
+ const ACTION_SPECS = {
87
+ add: {
112
88
  type: "object",
113
89
  required: ["kind", "title"],
114
90
  properties: {
115
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
91
+ cwd: { type: "string", description: CWD_DESC },
116
92
  kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
117
93
  title: { type: "string", description: "条目标题" },
118
94
  board: { type: "string", description: "归属板块(需求档文档名,可空)" },
@@ -123,66 +99,161 @@ export const ledgerAddTool = {
123
99
  },
124
100
  additionalProperties: false,
125
101
  },
126
- readonly: false,
127
- async execute(args, ctx) {
128
- args = assertToolArgs(ledgerAddTool, args)
129
- const { cwd, ...row } = args
130
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
131
- return JSON.stringify({ id, status: "待讨论" })
132
- },
133
- }
134
-
135
- export const ledgerUpdateTool = {
136
- name: "ledger_update",
137
- description: DESC("ledger_update"), // #15 外置:文本单点 = tool-docs/ledger_update.md
138
- parameters: {
102
+ update: {
139
103
  type: "object",
140
104
  required: ["id"],
141
105
  properties: {
142
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
106
+ cwd: { type: "string", description: CWD_DESC },
143
107
  id: { type: "number", description: "条目 id" },
144
108
  status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
145
109
  title: { type: "string", description: "标题(缺省 = 不变)" },
146
110
  board: { type: "string", description: "归属板块(缺省 = 不变)" },
147
111
  req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
148
112
  task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
149
- evidence: { type: "string", description: "证据(缺省 = 不变)" },
113
+ evidence: { type: "string", description: "证据(缺省 = 不变;传入 = 追加——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传)" },
114
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
150
115
  trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
151
- executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进在途自动写本会话 / 出在途自动清空 / 其余不变)" },
116
+ executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
152
117
  },
153
118
  additionalProperties: false,
154
119
  },
155
- readonly: false,
156
- async execute(args, ctx) {
157
- args = assertToolArgs(ledgerUpdateTool, args)
158
- const { cwd, id, ...patch } = args
159
- let executorSessionId
160
- if (patch.executor == null) {
161
- // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1);
162
- // 工具层供值,核心函数保持参数注入纯函数
163
- const { getSessionId } = await import("./session-slots.mjs")
164
- executorSessionId = getSessionId()
165
- }
166
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
167
- },
168
- }
169
-
170
- export const ledgerCloseTool = {
171
- name: "ledger_close",
172
- description: DESC("ledger_close"), // #15 外置:文本单点 = tool-docs/ledger_close.md
173
- parameters: {
120
+ close: {
174
121
  type: "object",
175
122
  required: ["id", "status"],
176
123
  properties: {
177
- cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
124
+ cwd: { type: "string", description: CWD_DESC },
178
125
  id: { type: "number", description: "条目 id" },
179
126
  status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
127
+ evidence: { type: "string", description: "最小证据行(可选;传入 = 追加缺省——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传。追认核销面判结果值须非空:缺 / 全空白 ⇒ 拒)" },
128
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
180
129
  },
181
130
  additionalProperties: false,
182
131
  },
183
- readonly: false,
184
- async execute(args, ctx) {
185
- args = assertToolArgs(ledgerCloseTool, args)
186
- return JSON.stringify(ledgerClose({ cwd: args.cwd ?? cwdOf(ctx), id: args.id, status: args.status }))
132
+ query: {
133
+ type: "object",
134
+ properties: {
135
+ cwd: { type: "string", description: CWD_DESC },
136
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
137
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
138
+ board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
139
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "按技术待办触发过滤(三枚举,缺省 = 不过滤——无触发(NULL)不过滤)" },
140
+ },
141
+ additionalProperties: false,
142
+ },
143
+ count: {
144
+ type: "object",
145
+ properties: { cwd: { type: "string", description: CWD_DESC } },
146
+ additionalProperties: false,
147
+ },
148
+ }
149
+
150
+ /** 并集 schema(§11.2——`action` 枚举 + 五 action 参数并集;per-action 必填由运行时守卫裁)。 */
151
+ const UNION_PARAMETERS = {
152
+ type: "object",
153
+ required: ["action"],
154
+ properties: {
155
+ action: { type: "string", enum: FULL_ACTIONS, description: "" }, // 变体构造时填描述
156
+ cwd: { type: "string", description: CWD_DESC },
157
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别(add 必填 / query 过滤;缺省 = 不过滤)" },
158
+ title: { type: "string", description: "条目标题(add 必填;update 可改——空串 / 全空白 ⇒ 拒)" },
159
+ board: { type: "string", description: "归属板块(需求档文档名;add 可空 / update 缺省 = 不变 / query 过滤)" },
160
+ req_doc: { type: "string", description: "需求档指针(add 可空 / update 缺省 = 不变)" },
161
+ task_book: { type: "string", description: "任务书指针(add 可空 / update 缺省 = 不变;在途 / 待核销必填——咬合 CHECK)" },
162
+ evidence: { type: "string", description: "最小证据行(add 可空;update / close 传入 = 追加缺省——旧空直写、旧非空拼 `旧|新`(「|」零空格)、新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传;update 缺省 = 不变 / close 缺省 = 判行值;追认核销须非空)" },
163
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换;缺省 / false = 追加;旗独传(无新文)⇒ 拒(update / close)" },
164
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(add / update 可空 / query 过滤)" },
165
+ id: { type: "number", description: "条目 id(update / close 必填)" },
166
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(close 必填 ∈ {已核销, 已废弃};update 迁移 / query 过滤——六态之一)" },
167
+ executor: { type: "string", description: "执行者 sessionId(update 可选——接手改写归属;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
187
168
  },
169
+ additionalProperties: false,
170
+ }
171
+
172
+ /** 变体构造(同名单对象、schema 窄化、两形永不同装配——先例 = `subagent` 工具 depth 面变体):
173
+ * 全量(depth-0 五 action;`writeFace:true` —— `readonly:false` + 动作级分类钩子,写面保持侧效门,
174
+ * query / count 归只读类 = 旧读二 `readonly:true` 同行为:planMode 放行 / 免审批)∥ 只读(depth>0
175
+ * 二 action;`readonly:true`,写三 schema 级不可达)。形态显式声明(不按 actions 长度推导——增量
176
+ * 变体不改错标文案)。 */
177
+ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
178
+ const entryTool = { name: "ledger", parameters: { type: "object", required: ["action"], properties: { action: { type: "string", enum: actions } } } }
179
+ return {
180
+ name: "ledger",
181
+ description: DESC("ledger"), // 文本单点 = tool-docs/ledger.md(五档要点合一)
182
+ parameters: {
183
+ ...UNION_PARAMETERS,
184
+ properties: {
185
+ ...UNION_PARAMETERS.properties,
186
+ action: {
187
+ ...UNION_PARAMETERS.properties.action,
188
+ enum: actions,
189
+ description: writeFace
190
+ ? "操作(五值):add 新增 / update 更新 / close 收口 / query 查询 / count 计数——写三仅主 agent 装配面"
191
+ : "操作(只读二值):query 查询 / count 计数",
192
+ },
193
+ },
194
+ },
195
+ readonly,
196
+ isReadonlyAction(args) { return READ_ACTIONS.includes(args?.action) },
197
+ async execute(args, ctx) {
198
+ // ① 入参归一(判序①——P1 同式;缺省 / null ⇒ {})
199
+ if (args == null) args = {}
200
+ else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`ledger:参数须为对象(收到 ${argPreview(args)})`)
201
+ // ② 入口级 action 判(P3 缺失 / P5 非法——消费 action;取值域 = 本装配面枚举)
202
+ const { action, ...rest0 } = args
203
+ assertToolArgs(entryTool, { action })
204
+ // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域)
205
+ const rest = assertToolArgs({ name: `ledger(${action})`, parameters: ACTION_SPECS[action] }, rest0)
206
+ // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
207
+ switch (action) {
208
+ case "add": {
209
+ const { cwd, ...row } = rest
210
+ const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
211
+ return JSON.stringify({ id, status: "待讨论" })
212
+ }
213
+ case "update": {
214
+ const { cwd, id, ...patch } = rest
215
+ let executorSessionId
216
+ if (patch.executor == null) {
217
+ // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
218
+ const { getSessionId } = await import("./session-slots.mjs")
219
+ executorSessionId = getSessionId()
220
+ }
221
+ return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
222
+ }
223
+ case "close":
224
+ return JSON.stringify(ledgerClose({ cwd: rest.cwd ?? cwdOf(ctx), id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }))
225
+ case "query":
226
+ return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger }), null, 2)
227
+ case "count":
228
+ return JSON.stringify({ count: ledgerCount({ cwd: rest.cwd ?? cwdOf(ctx) }) })
229
+ default:
230
+ // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
231
+ throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
232
+ }
233
+ },
234
+ }
188
235
  }
236
+
237
+ /** 统一入口两变体(同名 `ledger`——depth-0 装配全量 ∥ depth>0 各角色段装配只读)。 */
238
+ export const ledgerTool = makeLedgerTool(FULL_ACTIONS, { writeFace: true })
239
+ export const ledgerReadTool = makeLedgerTool(READ_ACTIONS, { readonly: true })
240
+
241
+ // ── 旧五名弃用壳(设计档 §11.4——code 面保留、不入任何装配面) ────────────────
242
+
243
+ /** 弃用文案(含旧名 + 统一入口 + 对应 action 指引;execute 抛它 ⇒ 零库动作)。
244
+ * description 同文静态化——不经 `DESC`(旧五档退场后 DESC 加载必炸;壳面规避)。 */
245
+ const deprecatedLine = (old, action) => `${old} 已退役(本名不再装配)——请改用统一入口 ledger:action="${action}"(参数面与原工具相同)。`
246
+
247
+ const deprecatedShell = (old, action, readonly) => ({
248
+ name: old,
249
+ description: deprecatedLine(old, action),
250
+ parameters: ACTION_SPECS[action],
251
+ readonly,
252
+ async execute() { throw new Error(deprecatedLine(old, action)) },
253
+ })
254
+
255
+ export const ledgerQueryTool = deprecatedShell("ledger_query", "query", true)
256
+ export const ledgerCountTool = deprecatedShell("ledger_count", "count", true)
257
+ export const ledgerAddTool = deprecatedShell("ledger_add", "add", false)
258
+ export const ledgerUpdateTool = deprecatedShell("ledger_update", "update", false)
259
+ export const ledgerCloseTool = deprecatedShell("ledger_close", "close", false)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ledger-variant-notice.mjs — 变体键库首跑检测提示(F-LX3 · 设计档 `docs/core/design/LEDGER.md` §12 ·
3
+ * 批 `docs/batches/2026-10-05-ledger-variant-db-notice.md` · 台账 #935)。
4
+ *
5
+ * 升级到含键归一的版本后,存量变体键库(旧产物写入的盘符翻转拼写键——`legacyKeyVariants`)不再无声搁浅:
6
+ * CLI 主入口启动时对**当前项目根**检测,命中 ⇒ 单行 i18n 提示(`thincoder ledger audit` 查看 ∥
7
+ * `thincoder ledger migrate` 收正);**只提示,零自动动作**(不迁移 / 不删档 / 不建库 / 零写)。
8
+ * 判据 = 变体键档**文件存在**(KD-VN1——零开库:不触 SQLite / 不建 WAL;坏档 / 空库不漏报)。
9
+ * 每进程至多一次 = 模块级闩(首唤检测 / 后唤零动作——重置缝 `_resetLedgerVariantNoticeForTest`,
10
+ * 先例 = `_resetLedgerDirForTest`)。降级 = 全径 try ⇒ 返回 `null`(零抛零阻塞——启动链不受累)。
11
+ * 本档静态链入 `ledger-migrate.mjs`(node:sqlite)⇒ 消费侧一律动态 import(W8 契约②)。
12
+ * 检测面零新建哈希式:键式直引 `ledgerKey` ∥ 变体枚举直引 `legacyKeyVariants`(单源不二写)。
13
+ */
14
+ import { statSync } from "node:fs"
15
+ import { join, resolve } from "node:path"
16
+
17
+ import { t } from "./i18n.mjs"
18
+ import { ledgerDirPath, ledgerKey } from "./ledger-db.mjs"
19
+ import { legacyKeyVariants } from "./ledger-migrate.mjs"
20
+ import { resolveProjectRoot } from "./manifest.mjs"
21
+
22
+ /** 默认探针(吞错形——与 `migrate` 源枚举过滤同判据):`statSync(p).isFile()`,任何 stat 失败 ⇒ false。 */
23
+ const defaultExists = (p) => { try { return statSync(p).isFile() } catch { return false } }
24
+
25
+ /** 一次为限闩(「每进程」= ESM 模块实例的天然粒度——首唤检测 / 后唤零动作)。 */
26
+ let noticeChecked = false
27
+
28
+ /** 测试重置缝(同 `_resetLedgerDirForTest` 先例——闩为进程级事实,直测需复位)。 */
29
+ export function _resetLedgerVariantNoticeForTest() { noticeChecked = false }
30
+
31
+ /**
32
+ * 检测当前项目根的变体键库:命中 ⇒ 单行提示文案(i18n 键 `ledger.variantDbNotice`——zh ∥ en 单源),
33
+ * 无 ∥ 已查过 ∥ 任何异常(根解析 / 探针 / 文案解析)⇒ `null`(静默降级——零抛)。
34
+ * 命中判据 = 变体键(`k !== 主键`)中有档在盘(文件存在——零开库);主库在否不影响判定。
35
+ * @param {{cwd?: string, dir?: string, locale?: string, exists?: (p: string) => boolean}} [opts]
36
+ * @returns {string|null} 单行文案 / null
37
+ */
38
+ export function ledgerVariantNotice({ cwd = process.cwd(), dir = ledgerDirPath(), locale = "en", exists = defaultExists } = {}) {
39
+ if (noticeChecked) return null
40
+ noticeChecked = true
41
+ try {
42
+ const root = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
43
+ const targetKey = ledgerKey(root)
44
+ const hit = legacyKeyVariants(root).filter((k) => k !== targetKey).some((k) => exists(join(dir, `${k}.db`)))
45
+ return hit ? t("ledger.variantDbNotice", {}, locale) : null
46
+ } catch { return null }
47
+ }