@thincoder/core 0.10.7 → 0.10.9

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 (149) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/advisor/compaction.mjs +1 -1
  3. package/advisor/run.mjs +1 -1
  4. package/agent/chat-call.mjs +40 -27
  5. package/agent/child-marks.mjs +1 -1
  6. package/agent/dispatch-gates.mjs +7 -4
  7. package/agent/dispatch-run.mjs +1 -0
  8. package/agent/family-tools.mjs +13 -8
  9. package/agent/helpers.mjs +12 -10
  10. package/agent/image-heal.mjs +65 -0
  11. package/agent/light-round.mjs +4 -4
  12. package/agent/live-beat.mjs +4 -4
  13. package/agent/post-turn.mjs +34 -0
  14. package/agent/record-results.mjs +9 -1
  15. package/agent/run-stages.mjs +45 -10
  16. package/agent/setup-reminders.mjs +14 -13
  17. package/agent/setup.mjs +72 -48
  18. package/agent/spawn-child.mjs +1 -1
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +2 -1
  21. package/agent/timers.mjs +8 -8
  22. package/agent/turn-loop.mjs +7 -1
  23. package/agent-tools/advisor.mjs +1 -1
  24. package/agent-tools/async-discard.mjs +40 -5
  25. package/agent-tools/async-settle.mjs +3 -1
  26. package/agent-tools/audit-block.mjs +5 -3
  27. package/agent-tools/batch-gate.mjs +198 -0
  28. package/agent-tools/batch-lifecycle.mjs +216 -36
  29. package/agent-tools/batch-paths.mjs +20 -20
  30. package/agent-tools/batch-skeleton.mjs +87 -0
  31. package/agent-tools/batch.mjs +11 -3
  32. package/agent-tools/checkpoint.mjs +1 -1
  33. package/agent-tools/context.mjs +28 -5
  34. package/agent-tools/design-slots.mjs +20 -7
  35. package/agent-tools/doc-check.mjs +75 -0
  36. package/agent-tools/eng.mjs +5 -3
  37. package/agent-tools/manifest.mjs +18 -18
  38. package/agent-tools/parent-channel.mjs +3 -3
  39. package/agent-tools/review-facts.mjs +2 -2
  40. package/agent-tools/settings.mjs +1 -1
  41. package/agent-tools/spawn-gates.mjs +108 -47
  42. package/agent-tools/ssh-session.mjs +486 -0
  43. package/agent-tools/ssh-transport.mjs +324 -0
  44. package/agent-tools/ssh-vault.mjs +291 -0
  45. package/agent-tools/subagent-scheduler.mjs +6 -5
  46. package/agent-tools/subagent-spawn.mjs +18 -11
  47. package/agent-tools/subagent.mjs +17 -9
  48. package/agent-tools/task.mjs +1 -1
  49. package/agent-tools/timer.mjs +8 -2
  50. package/agent-tools/zero-write-watch.mjs +2 -2
  51. package/agent-tools.mjs +3 -2
  52. package/attachments.mjs +4 -4
  53. package/browser/cdp.mjs +2 -2
  54. package/browser/launch.mjs +50 -4
  55. package/browser/snapshot.mjs +1 -1
  56. package/config-defaults.mjs +92 -0
  57. package/config-io.mjs +7 -2
  58. package/config-normalize.mjs +109 -0
  59. package/config-watch.mjs +2 -2
  60. package/config.mjs +88 -190
  61. package/declaration.mjs +5 -1
  62. package/doc-check/anchors.mjs +142 -0
  63. package/doc-check/index.mjs +210 -0
  64. package/doc-check/version.mjs +84 -0
  65. package/doc-check/width.mjs +132 -0
  66. package/file-refs.mjs +6 -6
  67. package/generate-title.mjs +1 -1
  68. package/hooks.mjs +1 -1
  69. package/i18n.mjs +13 -1
  70. package/ledger-cmd.mjs +314 -62
  71. package/ledger-db.mjs +122 -31
  72. package/ledger-migrate.mjs +18 -12
  73. package/ledger-read.mjs +6 -7
  74. package/ledger-tools.mjs +138 -77
  75. package/ledger.mjs +34 -62
  76. package/log.mjs +17 -3
  77. package/manifest-discovery.mjs +21 -18
  78. package/manifest-schema.mjs +17 -14
  79. package/manifest.mjs +37 -22
  80. package/mcp/transport-stdio.mjs +1 -1
  81. package/mcp.mjs +2 -2
  82. package/memory/core.mjs +1 -1
  83. package/memory/scan.mjs +1 -1
  84. package/memory/sweep.mjs +1 -1
  85. package/model-specs.mjs +7 -7
  86. package/notify-policy.mjs +6 -6
  87. package/package.json +2 -1
  88. package/permission.mjs +21 -1
  89. package/prompts/advisor-design.md +53 -23
  90. package/prompts/advisor-round1.md +34 -14
  91. package/prompts/advisor-round2.md +32 -19
  92. package/prompts/advisor-round3.md +34 -21
  93. package/prompts/common.md +148 -79
  94. package/prompts/consult-base.md +14 -8
  95. package/prompts/discipline-engineering.md +70 -66
  96. package/prompts/discipline-normal.md +86 -77
  97. package/prompts/persona-coder.md +6 -6
  98. package/prompts/persona-eng-coder.md +19 -18
  99. package/prompts/persona-eng-designer.md +40 -31
  100. package/prompts/persona-engineering.md +124 -92
  101. package/prompts/persona-explore.md +2 -2
  102. package/prompts/persona-normal.md +20 -17
  103. package/prompts/persona-plan.md +7 -7
  104. package/prompts/subagent-base.md +4 -4
  105. package/provider/anthropic.mjs +4 -2
  106. package/provider/core.mjs +4 -2
  107. package/provider/errors.mjs +8 -0
  108. package/provider/google.mjs +8 -1
  109. package/provider/normalize.mjs +42 -12
  110. package/provider/responses-request.mjs +4 -3
  111. package/provider/responses.mjs +2 -2
  112. package/provider/wait-status.mjs +1 -1
  113. package/provider-flows.mjs +1 -1
  114. package/queued.mjs +2 -2
  115. package/rules.mjs +4 -4
  116. package/session-gc.mjs +1 -1
  117. package/session-lifecycle.mjs +7 -7
  118. package/session-slot-write.mjs +2 -2
  119. package/session-slots-manifest.mjs +1 -1
  120. package/session.mjs +1 -1
  121. package/shell-candidates.mjs +2 -2
  122. package/shell-identity.mjs +1 -1
  123. package/team.mjs +63 -5
  124. package/think-off.mjs +3 -3
  125. package/token-ttl.mjs +1 -1
  126. package/token-window.mjs +1 -1
  127. package/tool-docs/bash.md +1 -1
  128. package/tool-docs/batch.md +1 -1
  129. package/tool-docs/browser.md +1 -1
  130. package/tool-docs/doc_check.md +54 -0
  131. package/tool-docs/fetch.md +3 -2
  132. package/tool-docs/ledger.md +7 -5
  133. package/tool-docs/manifest.md +14 -2
  134. package/tool-docs/ssh.md +51 -0
  135. package/tool-docs/subagent.md +6 -9
  136. package/tool-docs/timer.md +2 -1
  137. package/tool-docs/websearch.md +3 -1
  138. package/tools/browser.mjs +1 -1
  139. package/tools/execute.mjs +1 -1
  140. package/tools/git-run.mjs +2 -2
  141. package/tools/git.mjs +8 -8
  142. package/tools/index.mjs +3 -0
  143. package/tools/ops.mjs +1 -1
  144. package/tools/process-tree.mjs +2 -2
  145. package/tools/ssh-askpass.mjs +35 -0
  146. package/tools/ssh.mjs +105 -0
  147. package/tools/web-deepseek.mjs +172 -0
  148. package/tools/web.mjs +29 -4
  149. package/vision-reader.mjs +4 -4
package/ledger-tools.mjs CHANGED
@@ -10,12 +10,21 @@
10
10
  * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
- import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
13
+ import { ledgerAdd, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerQuery, ledgerUpdate, ledgerUpdateBatch } from "./ledger-cmd.mjs"
14
+ import { resolveLedgerRoot } from "./ledger-db.mjs"
14
15
  import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
15
16
 
16
- /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
17
+ /** 工具 cwd 供值面(缺省 → 当前会话锚——「动作目标路径」;项目根由核 `resolveLedgerRoot` 推导)。 */
17
18
  const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
18
19
 
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
26
+ }
27
+
19
28
  // ── 工具层参数守卫(设计档 §3.2 · 2026-09-27 快车道修复 · Gitee #IKIQGK / 台账 #472) ──
20
29
 
21
30
  /** 值预览(P1–P5 文案的 `<预览>`——设计档 §3.2):`JSON.stringify`,超 80 字符截断加 `…`
@@ -28,37 +37,47 @@ const argPreview = (v) => {
28
37
  /** 枚举取值域文本(P3 / P5 文案同源)。 */
29
38
  const enumText = (values) => `{${values.join(", ")}}`
30
39
 
31
- /** P3 后缀(声明派生):枚举字段带取值域 / 非枚举必填串带「非空字符串」/ 数字字段仅「必填」。 */
32
- const requiredSuffix = (spec) => (spec.enum ? `;取值 ∈ ${enumText(spec.enum)}` : spec.type === "string" ? ";非空字符串" : "")
40
+ /** P3 后缀(声明派生):枚举字段带取值域 / 非枚举必填串带「非空字符串」/ 数字字段仅「必填」
41
+ * (`domain` = 按形条件判据给出的取值域——`whenPresent.enum` 优先于基本形)。 */
42
+ const requiredSuffix = (spec, domain) => (domain ? `;取值 ∈ ${enumText(domain)}` : spec.type === "string" ? ";非空字符串" : "")
43
+
44
+ /** 值非空字段(P6 字段级:空串 / 全空白 ⇒ 拒——声明面不表达值级判据,与 `title` 同源一处)。 */
45
+ const NON_EMPTY_FIELDS = ["title", "resume_when"]
33
46
 
34
47
  /**
35
- * 工具层参数守卫(设计档 §3.2 · Gitee #IKIQGK / 台账 #472):判序 P1–P6,文案逐字 = §3.2 模板;
36
- * 消费工具自身 `parameters` 声明派生(`required` / `type` / `enum`——枚举 / 必填 / 类型不在守卫内复写);
37
- * 只判不改(零缺省填充 / 零类型转换 / 零文本改写),返回入参对象(唯一归一 = 判序① 的缺省 / `null`
38
- * ⇒ `{}`)。非法 ⇒ throw——判于核函数之前(零写、零库动作)。
48
+ * 工具层参数守卫(设计档 §3.2 · Gitee #IKIQGK / 台账 #472):判序 P1–P7,文案逐字 = §3.2 模板;
49
+ * 消费工具自身声明派生(`required` / `type` / `enum` / `oneOfRequired` / `whenPresent`——枚举 / 必填 /
50
+ * 类型 / 组判据不在守卫内复写);只判不改(零缺省填充 / 零类型转换 / 零文本改写),返回入参对象
51
+ * (唯一归一 = 判序① 的缺省 / `null` ⇒ `{}`)。非法 ⇒ throw——判于核函数之前(零写、零库动作)。
52
+ * 按形条件判据(`whenPresent`):`when` 字段在本笔入参**在场**(非 `null`)⇒ 该条生效(`required` 按
53
+ * P3 判、`enum` 按 P5 判并取代基本形取值域);不在场 ⇒ 本条不生效、按基本形判。
39
54
  */
40
55
  function assertToolArgs(tool, args) {
41
56
  const name = tool.name
42
57
  const decl = tool.parameters ?? {}
43
58
  const props = decl.properties ?? {}
44
59
  const fields = Object.keys(props)
60
+ // 组判据两键居声明条目顶层(不入 `parameters`——自定义键不随声明交模型 / provider)
61
+ const conditional = tool.whenPresent ?? []
45
62
  // P1:入参缺省 / `null` ⇒ 视作 `{}`;非对象(数组 / 标量)⇒ 拒
46
63
  if (args == null) args = {}
47
64
  else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`${name}:参数须为对象(收到 ${argPreview(args)})`)
48
65
  // P2:未知键 ⇒ 拒(判序先于缺参——typo 键先给可用参数集)
49
66
  const unknown = Object.keys(args).filter((k) => !Object.hasOwn(props, k))
50
67
  if (unknown.length > 0) throw new Error(`${name}:未知参数:${unknown.join(" / ")}(可用参数 = ${fields.join(" / ")})`)
51
- // P3–P6:逐字段(声明序)——缺失(必填;显式 `null` 同判)→ 类型 / 枚举 → `title` 空串 / 全空白
68
+ // P3–P6:逐字段(声明序)——缺失(必填;显式 `null` 同判)→ 类型 / 枚举 → 值非空
52
69
  for (const field of fields) {
53
70
  const spec = props[field]
54
71
  const value = args[field]
55
- const required = (decl.required ?? []).includes(field)
72
+ const rule = conditional.find((c) => c.field === field && args[c.when] != null)
73
+ const required = rule?.required ?? (decl.required ?? []).includes(field)
74
+ const domain = rule?.enum ?? spec.enum
56
75
  if (value === undefined || value === null) {
57
- if (required) throw new Error(`${name}:${field} 缺失(必填${requiredSuffix(spec)})`)
76
+ if (required) throw new Error(`${name}:${field} 缺失(必填${requiredSuffix(spec, domain)})`)
58
77
  continue // 可空字段 `null` ⇒ 放行(≡ 略去——与 `patch.x ?? 行值` 既有语义同源)
59
78
  }
60
- if (spec.enum) {
61
- if (!spec.enum.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(spec.enum)})`)
79
+ if (domain) {
80
+ if (!domain.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(domain)})`)
62
81
  } else if (spec.type === "string" && typeof value !== "string") {
63
82
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为字符串)`)
64
83
  } else if (spec.type === "number" && typeof value !== "number") {
@@ -66,10 +85,16 @@ function assertToolArgs(tool, args) {
66
85
  } else if (spec.type === "boolean" && typeof value !== "boolean") {
67
86
  throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为布尔)`)
68
87
  }
69
- if (field === "title" && typeof value === "string" && value.trim() === "") {
88
+ if (NON_EMPTY_FIELDS.includes(field) && typeof value === "string" && value.trim() === "") {
70
89
  throw new Error(`${name}:${field} 为空(${required ? "必填;" : ""}非空字符串)`)
71
90
  }
72
91
  }
92
+ // P7:组判据(声明派生 `oneOfRequired`)——组内恰携其一 ⇒ 过;同携 / 双缺 ⇒ 拒
93
+ for (const group of tool.oneOfRequired ?? []) {
94
+ if (group.filter((f) => args[f] != null).length === 1) continue
95
+ const received = Object.fromEntries(group.map((f) => [f, args[f] ?? null]))
96
+ throw new Error(`${name}:${group.join(" ∥ ")} 须恰携其一(收到 ${argPreview(received)})`)
97
+ }
73
98
  return args
74
99
  }
75
100
 
@@ -80,70 +105,92 @@ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
80
105
  const READ_ACTIONS = ["query", "count"]
81
106
 
82
107
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
83
- const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
108
+ const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
84
109
 
85
- /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
110
+ /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。
111
+ * `parameters` 内的键 = 交模型 / provider 的那一份声明;**组判据两键(`oneOfRequired` / `whenPresent`)
112
+ * 居本条条目顶层、不入 `parameters`**——自定义键不随声明外泄(provider 面零风险),守卫仍只读声明。 */
86
113
  const ACTION_SPECS = {
87
114
  add: {
88
- type: "object",
89
- required: ["kind", "title"],
90
- properties: {
91
- cwd: { type: "string", description: CWD_DESC },
92
- kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
93
- title: { type: "string", description: "条目标题" },
94
- board: { type: "string", description: "归属板块(需求档文档名,可空)" },
95
- req_doc: { type: "string", description: "需求档指针(可空)" },
96
- task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
97
- evidence: { type: "string", description: "最小证据行(可空)" },
98
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
115
+ parameters: {
116
+ type: "object",
117
+ required: ["kind", "title"],
118
+ properties: {
119
+ cwd: { type: "string", description: CWD_DESC },
120
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
121
+ title: { type: "string", description: "条目标题" },
122
+ board: { type: "string", description: "归属板块(需求档文档名,可空)" },
123
+ req_doc: { type: "string", description: "需求档指针(可空)" },
124
+ task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
125
+ evidence: { type: "string", description: "最小证据行(可空)" },
126
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
127
+ resume_when: { type: "string", description: "复启条件句(可空;`trigger=条件` 行的一句话条件 + 可选锚 `@YYYY-MM-DD` ∥ `@事件件名`)——值非空:空串 / 全空白 ⇒ 拒" },
128
+ },
129
+ additionalProperties: false,
99
130
  },
100
- additionalProperties: false,
101
131
  },
102
132
  update: {
103
- type: "object",
104
- required: ["id"],
105
- properties: {
106
- cwd: { type: "string", description: CWD_DESC },
107
- id: { type: "number", description: "条目 id" },
108
- status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
109
- title: { type: "string", description: "标题(缺省 = 不变)" },
110
- board: { type: "string", description: "归属板块(缺省 = 不变)" },
111
- req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
112
- task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
113
- evidence: { type: "string", description: "证据(缺省 = 不变;传入 = 追加——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传)" },
114
- evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
115
- trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
116
- executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
133
+ parameters: {
134
+ type: "object",
135
+ properties: {
136
+ cwd: { type: "string", description: CWD_DESC },
137
+ id: { type: "number", description: "条目 id(与 batch 恰携其一)" },
138
+ batch: { type: "string", description: "批档指针(与 id 恰携其一)——批级批量:一次结算该批的行;携 status ⇒ 逐行按迁移表重判,不携 status ⇒ 纯字段补丁(status 零写)" },
139
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
140
+ title: { type: "string", description: "标题(缺省 = 不变)" },
141
+ board: { type: "string", description: "归属板块(缺省 = 不变)" },
142
+ req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
143
+ task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
144
+ evidence: { type: "string", description: "证据(缺省 = 不变;传入 = 追加——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传)" },
145
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
146
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
147
+ resume_when: { type: "string", description: "复启条件句(缺省 = 不变;值非空——空串 / 全空白 ⇒ 拒)" },
148
+ executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
149
+ },
150
+ additionalProperties: false,
117
151
  },
118
- additionalProperties: false,
152
+ oneOfRequired: [["id", "batch"]],
119
153
  },
120
154
  close: {
121
- type: "object",
122
- required: ["id", "status"],
123
- properties: {
124
- cwd: { type: "string", description: CWD_DESC },
125
- id: { type: "number", description: "条目 id" },
126
- status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
127
- evidence: { type: "string", description: "最小证据行(可选;传入 = 追加缺省——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传。追认核销面判结果值须非空:缺 / 全空白 ⇒ 拒)" },
128
- evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
155
+ parameters: {
156
+ type: "object",
157
+ required: ["status"],
158
+ properties: {
159
+ cwd: { type: "string", description: CWD_DESC },
160
+ id: { type: "number", description: "条目 id(与 batch 恰携其一)" },
161
+ batch: { type: "string", description: "批档指针(与 id 恰携其一)——批级批量结算:只销已到 待核销 的行;待讨论 / 待设计 点名跳过(追认核销不入批量面)" },
162
+ status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)——携 batch 时可不给(缺省 = 勾销单值),给了只能是 已核销" },
163
+ evidence: { type: "string", description: "最小证据行(可选;传入 = 追加缺省——旧值非空 ⇒ 拼 `旧|新`(「|」零空格)、旧空直写;新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传。追认核销面判结果值须非空:缺 / 全空白 ⇒ 拒)" },
164
+ evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换(旧文零保留);缺省 / false = 追加;旗独传(无新文)⇒ 拒" },
165
+ },
166
+ additionalProperties: false,
129
167
  },
130
- additionalProperties: false,
168
+ oneOfRequired: [["id", "batch"]],
169
+ whenPresent: [{ when: "batch", field: "status", required: false, enum: ["已核销"] }],
131
170
  },
132
171
  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)不过滤)" },
172
+ parameters: {
173
+ type: "object",
174
+ properties: {
175
+ cwd: { type: "string", description: CWD_DESC },
176
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
177
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
178
+ board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
179
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "按技术待办触发过滤(三枚举,缺省 = 不过滤——无触发(NULL)不过滤)" },
180
+ task_book: { type: "string", description: "按任务书指针过滤(原值等值——批→行反查;缺省 = 不过滤)" },
181
+ },
182
+ additionalProperties: false,
140
183
  },
141
- additionalProperties: false,
142
184
  },
143
185
  count: {
144
- type: "object",
145
- properties: { cwd: { type: "string", description: CWD_DESC } },
146
- additionalProperties: false,
186
+ parameters: {
187
+ type: "object",
188
+ properties: {
189
+ cwd: { type: "string", description: CWD_DESC },
190
+ task_book: { type: "string", description: "按任务书指针过滤(原值等值)" },
191
+ },
192
+ additionalProperties: false,
193
+ },
147
194
  },
148
195
  }
149
196
 
@@ -162,8 +209,10 @@ const UNION_PARAMETERS = {
162
209
  evidence: { type: "string", description: "最小证据行(add 可空;update / close 传入 = 追加缺省——旧空直写、旧非空拼 `旧|新`(「|」零空格)、新值全空白 ⇒ 拒;覆盖 = 与 `evidenceReplace: true` 同传;update 缺省 = 不变 / close 缺省 = 判行值;追认核销须非空)" },
163
210
  evidenceReplace: { type: "boolean", description: "覆盖旗(布尔):与 evidence 同传 = 整段替换;缺省 / false = 追加;旗独传(无新文)⇒ 拒(update / close)" },
164
211
  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 过滤——六态之一)" },
212
+ resume_when: { type: "string", description: "复启条件句(add / update 可空;值非空——空串 / 全空白 ⇒ 拒;`trigger=条件` 行的一句话条件 + 可选锚 `@YYYY-MM-DD` ∥ `@事件件名`)" },
213
+ id: { type: "number", description: "条目 id(update / close:与 batch 恰携其一)" },
214
+ batch: { type: "string", description: "批档指针(update / close:与 id 恰携其一)——批级批量:一次结算该批的行(update 携 status ⇒ 逐行迁移表重判;不携 status ⇒ 纯字段补丁;close 只销 待核销 行、status 可空)" },
215
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(单行 close 必填 ∈ {已核销, 已废弃};close 携 batch 时可空、给了只能是 已核销;update 迁移 / query 过滤——六态之一)" },
167
216
  executor: { type: "string", description: "执行者 sessionId(update 可选——接手改写归属;缺省 = 按状态迁移语义:进入待设计 / 在途自动写本会话;出在途 / 撤回清空)" },
168
217
  },
169
218
  additionalProperties: false,
@@ -201,31 +250,43 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
201
250
  // ② 入口级 action 判(P3 缺失 / P5 非法——消费 action;取值域 = 本装配面枚举)
202
251
  const { action, ...rest0 } = args
203
252
  assertToolArgs(entryTool, { action })
204
- // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域)
205
- const rest = assertToolArgs({ name: `ledger(${action})`, parameters: ACTION_SPECS[action] }, rest0)
253
+ // ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域;
254
+ // 组判据两键在条目顶层,与 `parameters` 同送守卫)
255
+ const spec = ACTION_SPECS[action]
256
+ const rest = assertToolArgs({ name: `ledger(${action})`, ...spec }, rest0)
206
257
  // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
207
258
  switch (action) {
208
259
  case "add": {
209
260
  const { cwd, ...row } = rest
210
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
211
- return JSON.stringify({ id, status: "待讨论" })
261
+ const target = cwd ?? cwdOf(ctx)
262
+ const id = ledgerAdd({ cwd: target, row })
263
+ return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
212
264
  }
213
265
  case "update": {
214
- const { cwd, id, ...patch } = rest
266
+ const { cwd, id, batch, ...patch } = rest
267
+ const target = cwd ?? cwdOf(ctx)
215
268
  let executorSessionId
216
269
  if (patch.executor == null) {
217
270
  // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
218
271
  const { getSessionId } = await import("./session-slots.mjs")
219
272
  executorSessionId = getSessionId()
220
273
  }
221
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
274
+ // 批级批量(§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) })
277
+ }
278
+ case "close": {
279
+ const target = rest.cwd ?? cwdOf(ctx)
280
+ // 批级批量结算(§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) })
222
283
  }
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
284
  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) }) })
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
+ case "count": {
287
+ const target = rest.cwd ?? cwdOf(ctx)
288
+ return JSON.stringify({ ...ledgerCount({ cwd: target, task_book: rest.task_book }), ...reportedRoot(target) })
289
+ }
229
290
  default:
230
291
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
231
292
  throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
@@ -247,7 +308,7 @@ const deprecatedLine = (old, action) => `${old} 已退役(本名不再装配
247
308
  const deprecatedShell = (old, action, readonly) => ({
248
309
  name: old,
249
310
  description: deprecatedLine(old, action),
250
- parameters: ACTION_SPECS[action],
311
+ parameters: ACTION_SPECS[action].parameters,
251
312
  readonly,
252
313
  async execute() { throw new Error(deprecatedLine(old, action)) },
253
314
  })
package/ledger.mjs CHANGED
@@ -15,10 +15,8 @@
15
15
  *
16
16
  * W8 契约②:ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧一律**动态 import** 本档(禁止静态 import)。
17
17
  */
18
- import { opendirSync, statSync } from "node:fs"
19
- import { basename, dirname, join, resolve } from "node:path"
20
- import { projectRootView } from "./manifest.mjs"
21
- import { ledgerDbPath, PENDING_STATUSES, AGING_DAYS } from "./ledger-db.mjs"
18
+ import { basename, dirname, resolve } from "node:path"
19
+ import { AGING_DAYS, PENDING_STATUSES, ledgerChildren, ledgerDbPath, resolveLedgerRoot } from "./ledger-db.mjs"
22
20
  import { ledgerQuery } from "./ledger-cmd.mjs"
23
21
  import { executorTail } from "./ledger-executors.mjs"
24
22
 
@@ -45,55 +43,20 @@ export function entryTitle(text) {
45
43
  return norm.length > 20 ? norm.slice(0, 20) + "…" : norm
46
44
  }
47
45
 
48
- const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
49
-
50
- /** 向上(含自身)最近的已注册台账项目根 → {root, ledger} / null(F4——CLI 锚 = cwd;
51
- * 标记 = 用户数据目录里以该项目路径为键的台账库——项目目录内不落任何标记文件)。 */
46
+ /** 向上(含自身)最近的**已注册**项目根 → {root, ledger} / null(F4——CLI 锚 = cwd)。
47
+ * 薄委托 `resolveLedgerRoot(anchor, { descend: false })`(设计档 §3.8——**取祖先支**):`root` = 推导所
48
+ * 得项目根(非锚);容器 ∥ 工作区锚 ⇒ `null` ⇒ 族发现按容器形(族求和——§7.2 / AC-M2-18 / AC-M2-19)。 */
52
49
  export function findProject(anchor) {
53
- let dir = resolve(anchor)
54
- for (;;) {
55
- const ledger = ledgerDbPath(dir)
56
- if (isFile(ledger)) return { root: dir, ledger }
57
- const parent = dirname(dir)
58
- if (parent === dir) return null
59
- dir = parent
60
- }
61
- }
62
-
63
- /** 同级枚举上限(N2 成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集,只显当前项目)。 */
64
- export const MAX_SIBLING_SCAN = 100
65
-
66
- /** 目录下已注册台账的直接子目录(目录名升序;不可读 → [];**惰性枚举 + 上限**——大目录零全量扫描)。
67
- * 导出面 = 族发现(本档)∥ 轻通道闸候选枚举(`agent/light-round.mjs`——同级判据单源)。 */
68
- export function ledgerChildren(dir) {
69
- const out = []
70
- let handle
71
- try { handle = opendirSync(dir) } catch { return out }
72
- try {
73
- let n = 0
74
- for (;;) {
75
- const ent = handle.readSync()
76
- if (!ent) break
77
- if (++n > MAX_SIBLING_SCAN) return []
78
- if (!ent.isDirectory()) continue
79
- const root = join(dir, ent.name)
80
- const ledger = ledgerDbPath(root)
81
- if (isFile(ledger)) out.push({ root, ledger })
82
- }
83
- } catch { /* 读中断 → 已得子集(尽力而为) */ }
84
- finally { try { handle.closeSync() } catch { /* 已关 */ } }
85
- return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
50
+ const derived = resolveLedgerRoot(anchor, { descend: false })
51
+ return derived ? { root: derived.root, ledger: ledgerDbPath(derived.root) } : null
86
52
  }
87
53
 
88
- /** 项目族发现:{current, projects}——projects = current + 其同级含台账库目录(**发现序:current 在前**,
89
- * 其余按目录名升序);current 缺(容器目录,K6)→ 锚本地:取锚自身含台账库的直接子目录(不沿祖先链上找)。
90
- * **歧义根不充当项目**(轻通道轮 · 2026-10-04):命中锚若项目解析 `ambiguous`(≥2 候选——`projectRootView`)
91
- * ⇒ 该锚不可扫描(`openLedger` 歧义拒)——其键控库(先于 #828 写门的存量空壳 / 幽灵)不得遮蔽族发现;
92
- * 按容器形落子目录族(需求:打开容器根 ⇒ 族内合计——标记范围批 §1 原话)。 */
54
+ /** 项目族发现:{current, projects}——current = `findProject`(**祖先支**:容器 ∥ 工作区锚 ⇒ `null` ⇒ 按
55
+ * 容器形落子目录族);projects = current + 其**同级已注册**目录(**发现序:current 在前**,其余按目录
56
+ * 名升序);current 缺(容器目录,K6)→ **锚本地**:取锚自身**已注册**的直接子目录(不沿祖先链上找)。
57
+ * 「工作区根不充当项目」的判据单源 = `resolveLedgerRoot` 的库臂排除(§2.1)——本函数零自持判据。 */
93
58
  export function discoverFamily(anchor) {
94
- const hit = findProject(anchor)
95
- // 歧义根跳过(键控库存在 ≠ 可作项目——扫描面 `buildScan` 对歧义锚必拒;遮蔽则标记静默缺席)。
96
- const current = hit && projectRootView(hit.root).state === "ambiguous" ? null : hit
59
+ const current = findProject(anchor)
97
60
  if (current) {
98
61
  const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
99
62
  return { current, projects: [current, ...siblings] }
@@ -102,13 +65,21 @@ export function discoverFamily(anchor) {
102
65
  }
103
66
 
104
67
  /** scan 组装(SQLite 行集 + 计数单源 → scan 对象)——形状契约(设计档 §2.2):pool/tech/aged/
105
- * agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
68
+ * agedKeys/agedTitles/overdue/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
106
69
  * (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
107
70
  * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
71
+ * **`days` 只作用于展示面 `aged`**(本函数局部阈值);`overdue`(出口态)恒按核常量 `AGING_DAYS`
72
+ * (`overdueOf` 单源)——故 `days` 调小时 `aged` 会先于 `overdue` 涨(两数口径不同,非矛盾)。
73
+ * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor)` 推导所得项目根(非锚
74
+ * ——容器 ∥ 工作区锚取收敛序之根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`(不落兜底根——
75
+ * 该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 容器支只列已注册子目录))。
108
76
  * `inflightExecutors`(F-LX1 · sync 零变——K-LX2):在途且 executor 非空行集原样携带
109
77
  * {executor, updated_at, created_at}——判活解析由 `resolveExecutorStates` async 单源负责,本函数零探测。 */
110
78
  export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
111
- const rows = ledgerQuery({ cwd })
79
+ const anchor = resolve(cwd)
80
+ const derived = resolveLedgerRoot(anchor)
81
+ const root = derived ? derived.root : anchor
82
+ const rows = ledgerQuery({ cwd: anchor, now }) // `now` 透传(两读数同时钟——`overdue` 的行键由核同一 `now` 算)
112
83
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
113
84
  const poolEntries = pending.filter((r) => r.kind === "requirement")
114
85
  const techEntries = pending.filter((r) => r.kind === "tech_todo")
@@ -123,13 +94,13 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
123
94
  if (key) boards.set(key, (boards.get(key) ?? 0) + 1)
124
95
  }
125
96
  const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
126
- const root = resolve(cwd)
127
97
  const inflightExecutors = pending
128
98
  .filter((r) => r.status === "在途" && r.executor)
129
99
  .map((r) => ({ executor: r.executor, updated_at: r.updated_at ?? null, created_at: r.created_at ?? null }))
130
100
  return {
131
- name: basename(root), root, ledger: ledgerDbPath(root),
101
+ name: basename(root), root, ledger: derived ? ledgerDbPath(root) : null,
132
102
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
103
+ overdue: pending.filter((r) => r.overdue != null).length, // 待处置(§3.7——机判单源 = 行集 `overdue` 键,行值与 `now` 均同源)
133
104
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
134
105
  thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
135
106
  inflightExecutors,
@@ -145,8 +116,8 @@ export function formatMarker(scan) {
145
116
  * `root` = `family.current.root` 的 scan(命中但不在 `scans`——不可读 ⇒ 空范围,不掺兄弟);
146
117
  * `current` 缺席(容器根锚)⇒ 范围 = `scans` 全体(族内已读项目——构建期不可读已跳过)。
147
118
  * 空范围 ⇒ `{ marker: null, warn: false }`(不落 `0·0`);范围非空 ⇒ `marker` = `formatMarker`
148
- * 逐字(两池分列求和——F1);`warn` = 范围内任一项 `aged>0 ∨ deadExecutors>0`(端零重算)。
149
- * 消费面 = 核拍面(`ledger-surface.mjs`)∥ VSC item——端零自算单源。 */
119
+ * 逐字(两池分列求和——F1);`warn` = 范围内任一项 `overdue>0 ∨ deadExecutors>0`(§3.7——`aged ⊂
120
+ * overdue` ⇒ 警示色不缩;端零重算)。消费面 = 核拍面(`ledger-surface.mjs`)∥ VSC item。 */
150
121
  export function scopeMarkerOf(scans, family) {
151
122
  const list = Array.isArray(scans) ? scans : []
152
123
  const currentRoot = family?.current?.root ?? null
@@ -158,15 +129,15 @@ export function scopeMarkerOf(scans, family) {
158
129
  for (const s of range) {
159
130
  pool += s.pool
160
131
  tech += s.tech
161
- if (s.aged > 0 || s.deadExecutors > 0) warn = true
132
+ if (s.overdue > 0 || s.deadExecutors > 0) warn = true
162
133
  }
163
134
  return { marker: formatMarker({ pool, tech }), warn }
164
135
  }
165
136
 
166
- /** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——
167
- * 在途 executor 缺席时尾段空串,既有逐字断言零破)。 */
137
+ /** L2 明细行(逐字——`(老化 <n> · 待处置 <m>)` 两数恒显(含 0——§3.7 出口面恒显位扩);
138
+ * 阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——在途 executor 缺席时尾段空串)。 */
168
139
  export function formatDetailLine(scan) {
169
- return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
140
+ return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged} · 待处置 ${scan.overdue})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
170
141
  }
171
142
 
172
143
  /** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
@@ -178,9 +149,10 @@ export function detailScans(scans, current) {
178
149
  }
179
150
 
180
151
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3;`AGING_DAYS` 常量单源 = `ledger-db.mjs`
181
- // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变) ──
182
- export { AGING_DAYS, ALLOWED_MIGRATIONS, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
183
- export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
152
+ // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变;`ledgerChildren` / `MAX_SIBLING_SCAN` / `resolveLedgerRoot`
153
+ // ——2026-10-16 批 ledger-root-derivation 随迁 / 新增,公共面形零改) ──
154
+ export { AGING_DAYS, ALLOWED_MIGRATIONS, EVIDENCE_SOFT_LIMIT, MAX_SIBLING_SCAN, ledgerChildren, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, resolveLedgerRoot, _setLedgerDirForTest, _resetLedgerDirForTest, ensureColumn, ensureExecutorColumn } from "./ledger-db.mjs"
155
+ export { ledgerAdd, ledgerBatchRows, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerEvidenceOversize, ledgerQuery, ledgerReanchorBatch, ledgerUpdate, ledgerUpdateBatch, overdueOf } from "./ledger-cmd.mjs"
184
156
  export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
185
157
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
186
158
  export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
package/log.mjs CHANGED
@@ -16,8 +16,10 @@
16
16
  * ① 字段名精确匹配黑名单(apiKey/designToken/password/secret/token,大小写不敏感;
17
17
  * 另加防御性名称 authorization/proxy/proxyUri——防凭据型 URL/头字段泄漏——§2.5)
18
18
  * → 整个字段丢弃;
19
- * ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… 等)→ 截断到形态之前
19
+ * ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… / token 值形等)→ 截断到形态之前
20
20
  * ——宁可丢信息不漏密钥。
21
+ * token 值形(`uuid:epoch`——2026-10-14 批并入):工具结果经 `tool:done` head 落盘
22
+ * 时在形态前截断、不落值(与 batch.mjs 凭据第三形同判据)。
21
23
  * 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
22
24
  * - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
23
25
  * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
@@ -37,8 +39,8 @@ import { join } from "node:path"
37
39
  export const MAX_LINE = 512
38
40
  /** 字段名精确黑名单(NF-L3/§2.4——2026-09-03 评审 refinement #7:与 §2.4 词表一致含 token) */
39
41
  const BLACKLIST_FIELDS = new Set(["apikey", "designtoken", "password", "secret", "token", "authorization", "proxyuri", "proxy"])
40
- /** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前 */
41
- const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,})/i
42
+ /** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前(含 token 值形 `uuid:epoch`——同 batch.mjs 凭据第三形判据) */
43
+ const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:\d{10,16})/i
42
44
  /** 字符串字段长度兜底:head ≤300 / err ≤200 / 其余 ≤120 */
43
45
  const FIELD_CAPS = { head: 300, err: 200 }
44
46
 
@@ -154,6 +156,18 @@ export function redactSecret(fieldKey, value) {
154
156
  return hit ? s.slice(0, hit.index) + "[redacted]" : s
155
157
  }
156
158
 
159
+ /** 结构面遮蔽(#1432——tool-errors args 落痕前过同一链):键级 ∥ 内容级复用 `redactSecret`;嵌套对象 ∥ 数组递归;
160
+ * 非串值仅在键名命中黑名单时整键遮蔽(否则原样 ∥ 递归)——「宁可丢信息不漏密钥」同判据。 */
161
+ export function redactArgs(value) {
162
+ if (Array.isArray(value)) return value.map(redactArgs)
163
+ if (value && typeof value === "object") {
164
+ const out = {}
165
+ for (const [k, v] of Object.entries(value)) out[k] = typeof v === "string" || BLACKLIST_FIELDS.has(String(k).toLowerCase()) ? redactSecret(k, v) : redactArgs(v)
166
+ return out
167
+ }
168
+ return typeof value === "string" ? redactSecret("", value) : value
169
+ }
170
+
157
171
  /** 内容级净化:密钥形态截断到形态前(②);超长截断 + "…" 标记。 */
158
172
  export function sanitizeString(key, value) {
159
173
  let s = String(value)