@thincoder/core 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,188 @@
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 契约②)。
9
+ */
10
+ import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
11
+ import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger_*.md(DESC 单解析面)
12
+
13
+ /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
14
+ const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
15
+
16
+ // ── 工具层参数守卫(设计档 §3.2 · 2026-09-27 快车道修复 · Gitee #IKIQGK / 台账 #472) ──
17
+
18
+ /** 值预览(P1–P5 文案的 `<预览>`——设计档 §3.2):`JSON.stringify`,超 80 字符截断加 `…`
19
+ * (`undefined` 无 JSON 形 ⇒ 取文本形)。 */
20
+ const argPreview = (v) => {
21
+ const s = JSON.stringify(v) ?? String(v)
22
+ return s.length > 80 ? `${s.slice(0, 80)}…` : s
23
+ }
24
+
25
+ /** 枚举取值域文本(P3 / P5 文案同源)。 */
26
+ const enumText = (values) => `{${values.join(", ")}}`
27
+
28
+ /** P3 后缀(声明派生):枚举字段带取值域 / 非枚举必填串带「非空字符串」/ 数字字段仅「必填」。 */
29
+ const requiredSuffix = (spec) => (spec.enum ? `;取值 ∈ ${enumText(spec.enum)}` : spec.type === "string" ? ";非空字符串" : "")
30
+
31
+ /**
32
+ * 工具层参数守卫(设计档 §3.2 · Gitee #IKIQGK / 台账 #472):判序 P1–P6,文案逐字 = §3.2 模板;
33
+ * 消费工具自身 `parameters` 声明派生(`required` / `type` / `enum`——枚举 / 必填 / 类型不在守卫内复写);
34
+ * 只判不改(零缺省填充 / 零类型转换 / 零文本改写),返回入参对象(唯一归一 = 判序① 的缺省 / `null`
35
+ * ⇒ `{}`)。非法 ⇒ throw——判于核函数之前(零写、零库动作)。
36
+ */
37
+ function assertToolArgs(tool, args) {
38
+ const name = tool.name
39
+ const decl = tool.parameters ?? {}
40
+ const props = decl.properties ?? {}
41
+ const fields = Object.keys(props)
42
+ // P1:入参缺省 / `null` ⇒ 视作 `{}`;非对象(数组 / 标量)⇒ 拒
43
+ if (args == null) args = {}
44
+ else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`${name}:参数须为对象(收到 ${argPreview(args)})`)
45
+ // P2:未知键 ⇒ 拒(判序先于缺参——typo 键先给可用参数集)
46
+ const unknown = Object.keys(args).filter((k) => !Object.hasOwn(props, k))
47
+ if (unknown.length > 0) throw new Error(`${name}:未知参数:${unknown.join(" / ")}(可用参数 = ${fields.join(" / ")})`)
48
+ // P3–P6:逐字段(声明序)——缺失(必填;显式 `null` 同判)→ 类型 / 枚举 → `title` 空串 / 全空白
49
+ for (const field of fields) {
50
+ const spec = props[field]
51
+ const value = args[field]
52
+ const required = (decl.required ?? []).includes(field)
53
+ if (value === undefined || value === null) {
54
+ if (required) throw new Error(`${name}:${field} 缺失(必填${requiredSuffix(spec)})`)
55
+ continue // 可空字段 `null` ⇒ 放行(≡ 略去——与 `patch.x ?? 行值` 既有语义同源)
56
+ }
57
+ if (spec.enum) {
58
+ if (!spec.enum.includes(value)) throw new Error(`${name}:${field} 非法:${argPreview(value)}(取值 ∈ ${enumText(spec.enum)})`)
59
+ } else if (spec.type === "string" && typeof value !== "string") {
60
+ throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为字符串)`)
61
+ } else if (spec.type === "number" && typeof value !== "number") {
62
+ throw new Error(`${name}:${field} 非法:${argPreview(value)}(应为数字)`)
63
+ }
64
+ if (field === "title" && typeof value === "string" && value.trim() === "") {
65
+ throw new Error(`${name}:${field} 为空(${required ? "必填;" : ""}非空字符串)`)
66
+ }
67
+ }
68
+ return args
69
+ }
70
+
71
+ // ── 工具定义(接线:查询 → tools/index.mjs 全角色面;写 → family-tools.mjs depthOnly 分支) ──
72
+
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
+ }
92
+
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
+ }
107
+
108
+ export const ledgerAddTool = {
109
+ name: "ledger_add",
110
+ description: DESC("ledger_add"), // #15 外置:文本单点 = tool-docs/ledger_add.md(两池词表逐字保留)
111
+ parameters: {
112
+ type: "object",
113
+ required: ["kind", "title"],
114
+ properties: {
115
+ cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
116
+ kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
117
+ title: { type: "string", description: "条目标题" },
118
+ board: { type: "string", description: "归属板块(需求档文档名,可空)" },
119
+ req_doc: { type: "string", description: "需求档指针(可空)" },
120
+ task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
121
+ evidence: { type: "string", description: "最小证据行(可空)" },
122
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
123
+ },
124
+ additionalProperties: false,
125
+ },
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: {
139
+ type: "object",
140
+ required: ["id"],
141
+ properties: {
142
+ cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
143
+ id: { type: "number", description: "条目 id" },
144
+ status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
145
+ title: { type: "string", description: "标题(缺省 = 不变)" },
146
+ board: { type: "string", description: "归属板块(缺省 = 不变)" },
147
+ req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
148
+ task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
149
+ evidence: { type: "string", description: "证据(缺省 = 不变)" },
150
+ trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
151
+ executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进在途自动写本会话 / 出在途自动清空 / 其余不变)" },
152
+ },
153
+ additionalProperties: false,
154
+ },
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: {
174
+ type: "object",
175
+ required: ["id", "status"],
176
+ properties: {
177
+ cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
178
+ id: { type: "number", description: "条目 id" },
179
+ status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
180
+ },
181
+ additionalProperties: false,
182
+ },
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 }))
187
+ },
188
+ }
package/ledger.mjs CHANGED
@@ -5,7 +5,8 @@
5
5
  * v1 md 台账(docs/TODO.md 扫描器)→ SQLite 单表(ledger.db,项目级、不进 git):六态 CHECK 机械
6
6
  * 锁死、COUNT 单源计数、事务保证收口原子、归档 = 软删除。行龄源 = SQLite 时间戳(无 git blame)。
7
7
  *
8
- * 面:① 命令族(查询 = 只读全角色 / 写 = 仅主 agent——接线见 tools/index.mjs 与 agent/family-tools.mjs)
8
+ * 面:① 命令族(查询 = 只读全角色 / 写 = 仅主 agent——核函数 `ledger-cmd.mjs` + 工具定义 `ledger-tools.mjs`
9
+ * 两源,接线见 tools/index.mjs 与 agent/family-tools.mjs)
9
10
  * ② 族发现(findProject / discoverFamily——标记 = ledger.db)③ scan 组装(buildScan——行集 + ledgerCount
10
11
  * → scan 对象,形状契约 = pool/tech/aged/thresholdReached/actionable/root/name/ledger)
11
12
  * ④ 通知去重(送达门 + 一次性去重——去重档跨会话、跨端共享)⑤ 格式 helper(行文本逐字契约)。
@@ -20,6 +21,8 @@ import { configDir } from "./config.mjs"
20
21
  import { ledgerDbPath, PENDING_STATUSES } from "./ledger-db.mjs"
21
22
  import { ledgerQuery } from "./ledger-cmd.mjs"
22
23
  import { executorTail } from "./ledger-executors.mjs"
24
+ // 归一步单源(#585——盘符大写契约直引 `session-slots.mjs`;先例 = `ledger-db.mjs:23`)。
25
+ import { normalizeCwd } from "./session-slots.mjs"
23
26
 
24
27
  /** 老化阈值(天——口径 = 需求档;`days` 参数可覆盖)。 */
25
28
  export const AGING_DAYS = 30
@@ -182,9 +185,10 @@ export function planChangeLines(prev, scan) {
182
185
 
183
186
  /** 去重档键 = 台账库绝对路径·正斜杠 + **盘符大写**(跨端同规则——CLI 的 `process.cwd()` 盘符大写、
184
187
  * VSC 的 `uri.fsPath` 小写(`session-slots.mjs` `normalizeCwd` 同一契约——session / checkpoint /
185
- * trace 跨端共享即赖此);CLI 写的键 VSC 须逐字认得)。 */
188
+ * trace 跨端共享即赖此);盘符步 = `normalizeCwd` 直调(单源——#585;先例 = `ledger-db.mjs:23`)、
189
+ * 分隔符折叠自持;CLI 写的键 VSC 须逐字认得)。 */
186
190
  export function notifyKey(ledger) {
187
- return resolve(ledger).replace(/\\/g, "/").replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
191
+ return normalizeCwd(resolve(ledger).replace(/\\/g, "/"))
188
192
  }
189
193
 
190
194
  /** 去重档读(缺失 / 坏 JSON → 空态——N1 降级不崩)。 */
@@ -209,6 +213,7 @@ export function saveNotifyState(file, state) {
209
213
 
210
214
  // ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3) ──
211
215
  export { ALLOWED_MIGRATIONS, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
212
- export { ledgerAdd, ledgerAddTool, ledgerClose, ledgerCloseTool, ledgerCount, ledgerCountTool, ledgerQuery, ledgerQueryTool, ledgerUpdate, ledgerUpdateTool } from "./ledger-cmd.mjs"
216
+ export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
217
+ export { ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
213
218
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
214
219
  export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
package/log.mjs CHANGED
@@ -21,8 +21,8 @@
21
21
  * 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
22
22
  * - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
23
23
  * - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
24
- * 诊断日志(两端测试套件都会跑真实 agent 管线);显式设置 THINCODER_LOG_DIR 强制
25
- * 写入该目录(log.test.mjs 用它隔离临时目录——refinement #6)。
24
+ * 诊断日志(两端测试套件都会跑真实 agent 管线);显式测试缝(_setLogsDirForTest)
25
+ * 强制写入该目录(log.test.mjs 用它隔离临时目录——refinement #6)。
26
26
  * - 轮转清理(refinement #3——长驻进程覆盖):非仅启动时——每进程每日**首次写事件**
27
27
  * 时顺带清理 >1 天的 agent-*.log(extension host 可长驻数月,启动清理覆盖不到)。
28
28
  * - seq:每进程单调计数器。双端同写一个文件时 seq 会各自重复——定位同文件时序以
@@ -46,9 +46,14 @@ let _seq = 0
46
46
  let _dead = false // 进程内写失败即死(NF-L1 静默降级——不逐事件重复空转)
47
47
  let _cleanupDate = null // 本进程已执行过清理的日期(每日首次写时清一次)
48
48
 
49
- /** 日志目录:THINCODER_LOG_DIR(测试隔离/override)> ~/.thincoder/logs(与 sessions/ 同域) */
49
+ /** Test seam: override the logs directory (process-internal seam — CONFIG.md §6.2 ③ 通道). */
50
+ let _logsDir = null
51
+ export function _setLogsDirForTest(p) { _logsDir = p }
52
+ export function _resetLogsDirForTest() { _logsDir = null }
53
+
54
+ /** 日志目录:显式测试缝 > ~/.thincoder/logs(与 sessions/ 同域) */
50
55
  export function logsDir() {
51
- return process.env.THINCODER_LOG_DIR ?? join(homedir(), ".thincoder", "logs")
56
+ return _logsDir ?? join(homedir(), ".thincoder", "logs")
52
57
  }
53
58
 
54
59
  /** 今日日志文件路径(agent-YYYY-MM-DD.log) */
@@ -58,10 +63,10 @@ export function todayLogPath(now = new Date()) {
58
63
  }
59
64
 
60
65
  /** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
61
- * THINCODER_LOG_DIR override(log.test.mjs 隔离临时目录——refinement #6)。 */
66
+ * 测试缝 override(log.test.mjs 隔离临时目录——refinement #6)。 */
62
67
  function writeEnabled() {
63
68
  if (_dead) return false
64
- if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_LOG_DIR) return false
69
+ if (process.env.NODE_TEST_CONTEXT && _logsDir === null) return false
65
70
  return true
66
71
  }
67
72
 
@@ -138,7 +143,7 @@ export function logEvent(kind, fields = {}) {
138
143
  }
139
144
  }
140
145
 
141
- /** 共享脱敏(§18.6 D-TR2——trace-store 复用;logEvent 自身保持"丢弃字段"语义,
146
+ /** 共享脱敏(TRACES.md §6.1 D-TR2——trace-store 复用;logEvent 自身保持"丢弃字段"语义,
142
147
  * 本函数提供"遮蔽标记"语义——同一字段名黑名单 + 同一 SECRET_FORM 形态扫描,
143
148
  * 不发明新遮蔽模式)。字段名命中黑名单 → "[REDACTED](整个字段遮蔽);内容命中
144
149
  * 密钥形态 → 截断到形态前 + "[redacted]" 标记(宁可丢信息不漏密钥——§2.4)。 */
@@ -0,0 +1,131 @@
1
+ /**
2
+ * manifest-discovery.mjs — **发现 ∕ 归属族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
+ * 全量迁出〔原 `:39-143`〕——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
4
+ * `scanChildren`(两条梯表共用 walk 内核)· `discoverProjects`(项目梯)· `discoverRepos`(仓梯)·
5
+ * `owningProject`(归属形)· `resolveProjectRoot`(归属 ∨ 发现);**结构拆分零语义**(面不变 ∕ 判据不变,
6
+ * 只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
7
+ *
8
+ * 缝 = 同名再出口:宿主 `manifest.mjs` 转口本档七名既有导出(消费者 import 面零改);宿主自用四名
9
+ * (`MANIFEST_REL` ∥ `discoverProjects` ∥ `owningProject` ∥ `resolveProjectRoot`)随 import。
10
+ * 零环:本档不引宿主(方向单行);档外依赖 = `node:fs` ∥ `node:path`;机制详述 = `docs/core/design/MANIFEST.md` §2.2 / §2.9。
11
+ */
12
+ import { existsSync, readdirSync } from "node:fs"
13
+ import { dirname, join, resolve } from "node:path"
14
+
15
+ /** 数据档文件名(**每个项目一份**——项目根 = 带档目录;**git 非前提**。2026-09-17 / 2026-09-21 用户裁定)。 */
16
+ export const MANIFEST_REL = "PROJECT-MANIFEST.json"
17
+ /** 测试注入:强制项目根(测试 tmp 非 git 仓——同 ledger `_setLedgerDirForTest` 先例)。 */
18
+ let _projectRootOverride = null
19
+ export function _setProjectRootForTest(dir) { _projectRootOverride = dir }
20
+ export function _resetProjectRootForTest() { _projectRootOverride = null }
21
+
22
+ /**
23
+ * 一层扫描内核(**模块私有**——两条梯表共用;KD-M1-23 / 设计 §2.2「单源结构」):枚举锚的
24
+ * **直接子目录**一层(不递归)+ 按名排序 + 谓词过滤 ⇒ 命中表(绝对路径)。
25
+ * 全档唯一 `readdirSync` 落点(结构机判 T54);锚不可读 ⇒ `[]`(不抛——调用方按「零命中」处置)。
26
+ * @param {string} anchor 锚绝对路径
27
+ * @param {(dir:string)=>boolean} isHit 子目录谓词
28
+ * @returns {string[]} 命中子目录绝对路径(按名排序)
29
+ */
30
+ function scanChildren(anchor, isHit) {
31
+ try {
32
+ return readdirSync(anchor, { withFileTypes: true })
33
+ .filter((e) => e.isDirectory())
34
+ .map((e) => e.name)
35
+ .sort()
36
+ .map((e) => join(anchor, e))
37
+ .filter(isHit)
38
+ } catch { return [] }
39
+ }
40
+
41
+ /**
42
+ * **项目发现**(项目梯五级——KD-M1-23 / 设计 `docs/core/design/MANIFEST.md` §2.2;**git 非前提**,
43
+ * 2026-09-21 用户裁定):① 锚自身带 `MANIFEST_REL` ⇒ 锚;② 锚含 `.git` ⇒ 锚(缺档 = 建档机会);
44
+ * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `ambiguous`);
45
+ * ④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,落点 = 会话锚)。
46
+ * `candidates` 一律**按名排序**(`self` / `none` ⇒ `[]`);`matched` ∈ `manifest` / `git` / `null`
47
+ * 记命中(或歧义)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
48
+ * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**(测试注入面——先于真判据,同 `discoverRepos`)。
49
+ * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
50
+ * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
51
+ */
52
+ export function discoverProjects(cwd) {
53
+ if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
54
+ const anchor = resolve(cwd ?? ".")
55
+ if (existsSync(join(anchor, MANIFEST_REL))) return { kind: "self", root: anchor, candidates: [], matched: "manifest" }
56
+ if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
57
+ const withManifest = scanChildren(anchor, (d) => existsSync(join(d, MANIFEST_REL)))
58
+ if (withManifest.length > 1) return { kind: "ambiguous", root: null, candidates: withManifest, matched: "manifest" }
59
+ if (withManifest.length === 1) return { kind: "unique", root: withManifest[0], candidates: [...withManifest], matched: "manifest" }
60
+ const bareRepos = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
61
+ if (bareRepos.length > 1) return { kind: "ambiguous", root: null, candidates: bareRepos, matched: "git" }
62
+ if (bareRepos.length === 1) return { kind: "unique", root: bareRepos[0], candidates: [...bareRepos], matched: "git" }
63
+ return { kind: "none", root: null, candidates: [], matched: null }
64
+ }
65
+
66
+ /**
67
+ * **仓发现**(仓梯——KD-M1-23 / `docs/core/design/TOOLS.md` §6.13;**单源** KD-M1-22——`git` 工具
68
+ * 经此符号接线,禁第二份实现):① 锚含 `.git` ⇒ 锚;② 直接子目录中 `.git` **∧** 带档者恰一 ⇒ 命中
69
+ * (≥2 ⇒ 歧义);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 / ≥2 ⇒ 歧义);④ 均无 ⇒ `none`。
70
+ * 与 `discoverProjects` = **同一 walk 内核**(`scanChildren`)的两种梯表——差异只在谓词与级序。
71
+ * `candidates` 按名排序;`matched` 记档位;纯 fs / 不抛 / 不递归 / 不向上;覆盖位 ⇒ 头部短路。
72
+ * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
73
+ * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
74
+ */
75
+ export function discoverRepos(cwd) {
76
+ if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
77
+ const anchor = resolve(cwd ?? ".")
78
+ if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
79
+ const scoped = scanChildren(anchor, (d) => existsSync(join(d, ".git")) && existsSync(join(d, MANIFEST_REL)))
80
+ if (scoped.length > 1) return { kind: "ambiguous", root: null, candidates: scoped, matched: "manifest" }
81
+ if (scoped.length === 1) return { kind: "unique", root: scoped[0], candidates: [...scoped], matched: "manifest" }
82
+ const bare = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
83
+ if (bare.length > 1) return { kind: "ambiguous", root: null, candidates: bare, matched: "git" }
84
+ if (bare.length === 1) return { kind: "unique", root: bare[0], candidates: [...bare], matched: "git" }
85
+ return { kind: "none", root: null, candidates: [], matched: null }
86
+ }
87
+
88
+ /**
89
+ * **归属形单点**(KD-M1-30 / 设计 §2.9 A——2026-09-21 用户裁定「最近者优先」):自 `target`
90
+ * (目录含自身;文件路径自其父目录起)沿**祖先链**逐级上溯至盘根,取**最近**带 `MANIFEST_REL`
91
+ * 的目录;无 ⇒ `null`(⇒ 调用方走发现兜底)。**嵌套合法**:子内归子、根其余归根。
92
+ * **纯 fs**(只判存在性——不解析档内容 / 不问模式)· **不跨兄弟** · **无全局优先级**。
93
+ * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**:直接返回覆盖值(不查档存在性——
94
+ * 保「覆盖即覆盖值」语义,回归守卫 = 本批批内件基座腿(`docs/batches/2026-10-02-manifest-resolution-fix.test.mjs`——
95
+ * 基座三态 + 覆盖位 + 薄委托等价逐格)+ `docs/batches/2026-09-29-structure-split-2.test.mjs` 导出枚举探针(`Object.keys` 名面核))。
96
+ * @param {string} [target] 目标路径(目录 / 文件)
97
+ * @returns {string|null} 最近带档祖先目录绝对路径 / null
98
+ */
99
+ export function owningProject(target) {
100
+ if (_projectRootOverride) return resolve(_projectRootOverride)
101
+ let dir = resolve(target ?? ".")
102
+ for (;;) {
103
+ if (existsSync(join(dir, MANIFEST_REL))) return dir
104
+ const parent = dirname(dir)
105
+ if (parent === dir) return null // 盘根 → 祖先链无档
106
+ dir = parent
107
+ }
108
+ }
109
+
110
+ /** **项目根解析的歧义直读形**(判定单点——设计 `MANIFEST.md` §2.2 `projectRootView` 契约行 / §2.5
111
+ * 「多档解析消费面收口」条):`ok`(唯一解析:归属命中 ∥ 发现 `self` / `unique`)/ `ambiguous`
112
+ * (≥2 候选 ⇒ `root` null + 候选全列**按名排序**)/ `none`(无项目 ⇒ 双空);覆盖位在场 ⇒
113
+ * 头部短路(`ok` + 覆盖值)。**非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数(行为逐字零变)。
114
+ * @returns {{state:'ok'|'ambiguous'|'none', root:string|null, candidates:string[]}} */
115
+ export function projectRootView(cwd) {
116
+ const owning = owningProject(cwd)
117
+ if (owning) return { state: "ok", root: owning, candidates: [] }
118
+ const d = discoverProjects(cwd)
119
+ if (d.root) return { state: "ok", root: d.root, candidates: [] }
120
+ if (d.kind === "ambiguous") return { state: "ambiguous", root: null, candidates: d.candidates }
121
+ return { state: "none", root: null, candidates: [] }
122
+ }
123
+
124
+ /** 项目根解析(**归属 ∨ 发现**——KD-M1-30 / M1-24):**薄委托** `projectRootView(cwd).root`(判定
125
+ * 单点见上——#828)。带档路径与批前**逐字同**;变更面两条(设计 §2.2 同条):① 项目树内路径
126
+ * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `ambiguous` ⇒
127
+ * `null`。调用方零改(行为随语义变更——设计 §2.5 键面条 / 错层条)。
128
+ * @returns {string|null} 项目根绝对路径 / null */
129
+ export function resolveProjectRoot(cwd) {
130
+ return projectRootView(cwd).root
131
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * manifest-schema.mjs — **校验 ∕ 默认值族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
+ * 全量迁出〔原 `:194-205` + `:240-369` 两段非连续〕——`isValidDocRootValue`(docRoot 子键值形态判据)·
4
+ * `DEFAULT_MANIFEST`(默认八键)· `MANIFEST_SCHEMA`(判据单源)· 元素层谓词两件 · `validateManifest` ·
5
+ * `fillDefaults`;**结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 =
6
+ * 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
7
+ *
8
+ * 缝 = 同名再出口:宿主 `manifest.mjs` 转口四名既有导出(`DEFAULT_MANIFEST` ∥ `MANIFEST_SCHEMA` ∥
9
+ * `isValidDocRootValue` ∥ `validateManifest`);`fillDefaults` 被宿主 `readManifest` 消费 ⇒ 随迁导出(宿主 import)。
10
+ * 零环:本档零 import(纯谓词 ∥ 冻结常量——不引宿主 ∥ 不引它档);机制详述 = `docs/core/design/MANIFEST.md` §2.2。
11
+ */
12
+
13
+ /**
14
+ * `docRoot` 子键**值形态判据**(F7 / KD-M1-6 / KD-M1-8——判据单源):非空字符串(`trim` 后
15
+ * 非空),或非空数组且元素皆非空字符串(**同款口径**:元素 `trim` 后非空);其余(空串 /
16
+ * 空白串 / 空数组 / 数组含非串 / 空串 / 空白串元素 / 非串非数组)为非法。
17
+ * @param {unknown} value docRoot 某子键的值
18
+ * @returns {boolean}
19
+ */
20
+ export function isValidDocRootValue(value) {
21
+ if (typeof value === "string") return value.trim() !== ""
22
+ if (!Array.isArray(value) || value.length === 0) return false
23
+ return value.every((p) => typeof p === "string" && p.trim() !== "")
24
+ }
25
+
26
+ /** 默认八键(架构 §2.3 E1 五键 + 三族声明键——KD-M1-31)——整档初始化的写源与缺键 fallback 的补源。 */
27
+ export const DEFAULT_MANIFEST = Object.freeze({
28
+ version: 1,
29
+ phase: "initial-dev",
30
+ docRoot: Object.freeze({
31
+ requirements: "docs/requirements",
32
+ specs: "docs/requirements/specs",
33
+ design: "docs/design",
34
+ modules: "docs/design/modules",
35
+ batches: "docs/batches",
36
+ }),
37
+ promptsLanding: "thincoder-core/prompts",
38
+ checkConfig: Object.freeze({
39
+ scanDirs: Object.freeze(["docs"]),
40
+ lineWidth: 300,
41
+ widthExemptZones: Object.freeze([]),
42
+ anchors: Object.freeze({ domain: "docs", exclude: Object.freeze(["_archive", "batches"]) }),
43
+ exemptions: Object.freeze([]),
44
+ lineCounts: Object.freeze([]), // 行数面机检声明(#546):[] = 未载惰性(doc-check 行数族)
45
+ }),
46
+ // 项目声明三族(2026-09-27 声明载体唯一化批并入——键名 / 形态逐字承前;来源档已退役)。
47
+ codePaths: Object.freeze(["src"]),
48
+ index: Object.freeze({ codeExtensions: Object.freeze([]), docExtensions: Object.freeze([]), excludePaths: Object.freeze([]), publicRepos: Object.freeze([]) }),
49
+ advisor: Object.freeze({ docMap: "", standardsDoc: "" }),
50
+ })
51
+
52
+ /** 校验判据(模块设计 §2.2)——枚举 / 键存在(fallback 用);判据单源(KD-M1-4)。 */
53
+ export const MANIFEST_SCHEMA = Object.freeze({
54
+ $anchor: "docs/core/design/MANIFEST.md",
55
+ enum: Object.freeze({
56
+ phase: Object.freeze(["initial-dev", "production"]),
57
+ }),
58
+ keys: Object.freeze(Object.keys(DEFAULT_MANIFEST)),
59
+ nestedKeys: Object.freeze({
60
+ docRoot: Object.freeze(Object.keys(DEFAULT_MANIFEST.docRoot)),
61
+ checkConfig: Object.freeze(Object.keys(DEFAULT_MANIFEST.checkConfig)),
62
+ index: Object.freeze(Object.keys(DEFAULT_MANIFEST.index)),
63
+ advisor: Object.freeze(Object.keys(DEFAULT_MANIFEST.advisor)),
64
+ }),
65
+ })
66
+
67
+ /** 三族声明键**元素层**判据(KD-M1-32):数组且各元素为非空字符串(`trim` 后非空);
68
+ * **数组层**自身可空(`[]` 合法——`codePaths:[]` = 无段名代码面 / `index.*:[]` = 追加零项)。 */
69
+ function isNonEmptyStringArray(v) {
70
+ return Array.isArray(v) && v.every((e) => typeof e === "string" && e.trim() !== "")
71
+ }
72
+
73
+ /** `checkConfig.lineCounts` 元素层判据(#546):数组且各元素为非数组对象、`doc` / `section` 皆非空串(`trim` 后非空);`[]` = 未载惰性。 */
74
+ function isLineCountsValue(v) {
75
+ return Array.isArray(v) && v.every((e) => e !== null && typeof e === "object" && !Array.isArray(e)
76
+ && typeof e.doc === "string" && e.doc.trim() !== "" && typeof e.section === "string" && e.section.trim() !== "")
77
+ }
78
+
79
+ /**
80
+ * 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
81
+ * 指针腿已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
82
+ * 缺键 = 便利 fallback(补默认值),不是错(与整档缺失两分——KD-M1-2)。
83
+ * @param {object} obj 待校验 manifest 对象
84
+ * @returns {{ok:boolean, errors:string[], missingKeys:string[]}}
85
+ */
86
+ export function validateManifest(obj) {
87
+ const errors = []
88
+ const missingKeys = []
89
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
90
+ return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys }
91
+ }
92
+ for (const key of MANIFEST_SCHEMA.keys) {
93
+ if (!(key in obj)) missingKeys.push(key)
94
+ }
95
+ for (const [nested, subkeys] of Object.entries(MANIFEST_SCHEMA.nestedKeys)) {
96
+ const value = obj[nested]
97
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
98
+ missingKeys.push(nested) // 整键缺失/非对象 → 与整键缺同语义(整键补默认)
99
+ continue
100
+ }
101
+ for (const sub of subkeys) {
102
+ if (!(sub in value)) { missingKeys.push(`${nested}.${sub}`); continue } // 子键路径(AC-3 / T3b)
103
+ // docRoot 子键值形态(F7 / KD-M1-7):串 | 非空串数组;非法 → 拒(不静默跳过)。
104
+ if (nested === "docRoot" && !isValidDocRootValue(value[sub])) {
105
+ errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串或非空字符串数组——KD-M1-6 / KD-M1-7)`)
106
+ }
107
+ // checkConfig.lineCounts 元素层形态(#546):数组,元素 = { doc, section } 非空串;非法 → 拒(不静默跳过)。
108
+ if (nested === "checkConfig" && sub === "lineCounts" && !isLineCountsValue(value[sub])) {
109
+ errors.push(`checkConfig.lineCounts 值形态非法:${JSON.stringify(value[sub])}(须为数组,元素 = { doc: 非空串, section: 非空串 }——#546)`)
110
+ }
111
+ }
112
+ }
113
+ // 三族声明键形态(KD-M1-32——fail-closed:非法即 errors,不静默跳过;缺键仍在 missingKeys)。
114
+ // `index.excludePaths`(§6.14 面①):**数组层** fail-closed(非数组 ⇒ 拒);元素层宽容——空 ∕ 非串
115
+ // 元素由归一剔除(L-①-1 夹具 `["./openclaw/","refs",""]` ⇒ `["openclaw","refs"]`);`index.publicRepos`(§6.15)同款。
116
+ const strArrErr = (key, v) => { if (!isNonEmptyStringArray(v)) errors.push(`${key} 值形态非法:${JSON.stringify(v)}(须为非空字符串数组——KD-M1-32)`) }
117
+ const strErr = (key, v) => { if (typeof v !== "string") errors.push(`${key} 值形态非法:${JSON.stringify(v)}(须为字符串——KD-M1-32)`) }
118
+ const objErr = (key, v) => (v === null || typeof v !== "object" || Array.isArray(v)
119
+ ? `${key} 值形态非法:${JSON.stringify(v)}(须为对象——KD-M1-32)` : null)
120
+ if (obj.codePaths !== undefined) strArrErr("codePaths", obj.codePaths)
121
+ const idxBad = obj.index === undefined ? null : objErr("index", obj.index)
122
+ if (idxBad) errors.push(idxBad)
123
+ else if (obj.index !== undefined) {
124
+ for (const k of ["codeExtensions", "docExtensions"]) if (obj.index[k] !== undefined) strArrErr(`index.${k}`, obj.index[k])
125
+ if (obj.index.excludePaths !== undefined && !Array.isArray(obj.index.excludePaths)) {
126
+ errors.push(`index.excludePaths 值形态非法:${JSON.stringify(obj.index.excludePaths)}(须为数组——非数组 fail-closed;空 ∕ 非串元素由归一剔除)`)
127
+ }
128
+ if (obj.index.publicRepos !== undefined && !Array.isArray(obj.index.publicRepos)) {
129
+ errors.push(`index.publicRepos 值形态非法:${JSON.stringify(obj.index.publicRepos)}(须为数组——非数组 fail-closed;空 ∕ 非串元素由归一剔除)`)
130
+ }
131
+ }
132
+ const advBad = obj.advisor === undefined ? null : objErr("advisor", obj.advisor)
133
+ if (advBad) errors.push(advBad)
134
+ else if (obj.advisor !== undefined) for (const k of ["docMap", "standardsDoc"]) if (obj.advisor[k] !== undefined) strErr(`advisor.${k}`, obj.advisor[k])
135
+ if (obj.phase !== undefined && !MANIFEST_SCHEMA.enum.phase.includes(obj.phase)) {
136
+ errors.push(`phase 取值非法:"${obj.phase}"(允许:${MANIFEST_SCHEMA.enum.phase.join(" | ")})`)
137
+ }
138
+ if (obj.version !== undefined && (typeof obj.version !== "number" || !Number.isFinite(obj.version))) {
139
+ errors.push(`version 非法:${JSON.stringify(obj.version)}(须为数值)`)
140
+ }
141
+ return { ok: errors.length === 0, errors, missingKeys }
142
+ }
143
+
144
+ /** 缺键补默认值(module 设计 §2.2 管线):顶层缺键补默认、嵌套键(docRoot/checkConfig/index/advisor)
145
+ * 子键补默认(与整键缺同语义);不覆写既有值。深拷贝默认源(structuredClone)——冻结常量永不外泄引用。
146
+ * 三族键的非对象值**原样透传**(不洗白——再校验拒,KD-M1-32)。 */
147
+ export function fillDefaults(obj) {
148
+ const out = structuredClone(DEFAULT_MANIFEST)
149
+ for (const key of MANIFEST_SCHEMA.keys) {
150
+ if (!(key in obj)) continue
151
+ const def = out[key]
152
+ if (def !== null && typeof def === "object" && !Array.isArray(def)) {
153
+ const src = obj[key]
154
+ if (src === null || typeof src !== "object" || Array.isArray(src)) {
155
+ if (key === "index" || key === "advisor") out[key] = src // 形态错 → 透传(再校验拒);docRoot/checkConfig 保持默认
156
+ continue
157
+ }
158
+ for (const sub of Object.keys(def)) {
159
+ if (sub in src) out[key][sub] = src[sub]
160
+ }
161
+ } else {
162
+ out[key] = obj[key]
163
+ }
164
+ }
165
+ return out
166
+ }