@thincoder/core 0.10.12 → 0.11.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 (174) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/advisor/messages.mjs +4 -2
  3. package/advisor/notice.mjs +1 -1
  4. package/advisor/project-context.mjs +121 -11
  5. package/agent/completion.mjs +13 -1
  6. package/agent/digest-account.mjs +73 -46
  7. package/agent/dispatch-run.mjs +1 -1
  8. package/agent/dispatch.mjs +29 -21
  9. package/agent/family-tools.mjs +9 -5
  10. package/agent/helpers.mjs +1 -94
  11. package/agent/offload.mjs +88 -0
  12. package/agent/record-results.mjs +1 -6
  13. package/agent/run-stages.mjs +74 -49
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +6 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +35 -3
  18. package/agent/write-face.mjs +99 -0
  19. package/agent/write-gate.mjs +354 -5
  20. package/agent-tools/advisor.mjs +127 -11
  21. package/agent-tools/async-settle.mjs +2 -2
  22. package/agent-tools/bash-async.mjs +8 -2
  23. package/agent-tools/batch-closeout.mjs +206 -0
  24. package/agent-tools/batch-gate.mjs +287 -73
  25. package/agent-tools/batch-ledger-bind.mjs +183 -0
  26. package/agent-tools/batch-ledger-link.mjs +73 -0
  27. package/agent-tools/batch-lifecycle.mjs +113 -354
  28. package/agent-tools/batch-paths.mjs +45 -40
  29. package/agent-tools/batch-read.mjs +1 -1
  30. package/agent-tools/batch-reopen.mjs +142 -0
  31. package/agent-tools/batch-skeleton.mjs +283 -80
  32. package/agent-tools/batch-unit-test-gate.mjs +107 -0
  33. package/agent-tools/batch.mjs +243 -50
  34. package/agent-tools/context.mjs +6 -3
  35. package/agent-tools/design-slots.mjs +24 -24
  36. package/agent-tools/design-token.mjs +96 -15
  37. package/agent-tools/doc-check.mjs +18 -2
  38. package/agent-tools/eng.mjs +1 -1
  39. package/agent-tools/escalate-async.mjs +3 -3
  40. package/agent-tools/manifest.mjs +8 -5
  41. package/agent-tools/read-history.mjs +5 -5
  42. package/agent-tools/review-facts.mjs +31 -13
  43. package/agent-tools/spawn-gates.mjs +10 -14
  44. package/agent-tools/ssh-session.mjs +120 -111
  45. package/agent-tools/ssh-transfer.mjs +210 -0
  46. package/agent-tools/ssh-transport.mjs +277 -46
  47. package/agent-tools/subagent-actions-query.mjs +1 -1
  48. package/agent-tools/subagent-actions.mjs +1 -1
  49. package/agent-tools/subagent-async.mjs +3 -2
  50. package/agent-tools/subagent-scheduler.mjs +43 -9
  51. package/agent-tools/subagent-spawn.mjs +68 -9
  52. package/agent-tools/subagent.mjs +16 -14
  53. package/agent.mjs +1 -0
  54. package/client-identity.mjs +70 -0
  55. package/config-presets.mjs +5 -3
  56. package/config.mjs +19 -13
  57. package/context.mjs +39 -8
  58. package/declaration.mjs +2 -2
  59. package/doc-check/anchors.mjs +128 -28
  60. package/doc-check/index.mjs +93 -27
  61. package/doc-check/line-refs.mjs +112 -0
  62. package/doc-check/paragraph.mjs +24 -8
  63. package/embedding.mjs +2 -0
  64. package/explore-distill.mjs +1 -1
  65. package/generate-title.mjs +3 -2
  66. package/i18n.mjs +1 -1
  67. package/index-discover.mjs +4 -1
  68. package/ledger-close-proof.mjs +62 -0
  69. package/ledger-cmd.mjs +79 -194
  70. package/ledger-db.mjs +29 -28
  71. package/ledger-migrate.mjs +4 -4
  72. package/ledger-read.mjs +10 -8
  73. package/ledger-surface.mjs +1 -1
  74. package/ledger-tools.mjs +10 -10
  75. package/ledger-variant-notice.mjs +1 -1
  76. package/ledger-write-ops.mjs +373 -0
  77. package/ledger.mjs +2 -2
  78. package/manifest-discovery.mjs +2 -2
  79. package/manifest-schema.mjs +4 -0
  80. package/memory/memory-tool.mjs +5 -2
  81. package/memory/schema.mjs +5 -26
  82. package/model-specs.mjs +6 -5
  83. package/package.json +1 -1
  84. package/path-resolve.mjs +217 -0
  85. package/permission.mjs +4 -0
  86. package/prompt-overlays.mjs +5 -0
  87. package/prompts/advisor-design.md +9 -6
  88. package/prompts/advisor-round1.md +4 -3
  89. package/prompts/advisor-round2.md +6 -5
  90. package/prompts/advisor-round3.md +6 -5
  91. package/prompts/common.md +23 -10
  92. package/prompts/consult-base.md +2 -1
  93. package/prompts/discipline-engineering.md +29 -48
  94. package/prompts/discipline-normal.md +10 -5
  95. package/prompts/persona-eng-coder.md +8 -0
  96. package/prompts/persona-eng-designer.md +22 -12
  97. package/prompts/persona-engineering.md +81 -66
  98. package/prompts/persona-plan.md +2 -1
  99. package/prompts/persona-tester.md +55 -0
  100. package/provider/anthropic.mjs +12 -7
  101. package/provider/core.mjs +21 -5
  102. package/provider/errors.mjs +41 -0
  103. package/provider/google.mjs +25 -9
  104. package/provider/list-models.mjs +4 -3
  105. package/provider/rate.mjs +11 -3
  106. package/provider/responses.mjs +17 -7
  107. package/provider/sse.mjs +15 -4
  108. package/session-identity.mjs +21 -0
  109. package/session-lifecycle.mjs +12 -4
  110. package/session-slots.mjs +9 -14
  111. package/session.mjs +5 -0
  112. package/skip-dirs.mjs +48 -0
  113. package/team-ledger.mjs +4 -4
  114. package/text-budget.mjs +4 -0
  115. package/token-window.mjs +107 -10
  116. package/tool-docs/advisor.md +2 -2
  117. package/tool-docs/apply_patch.md +2 -0
  118. package/tool-docs/bash.md +4 -0
  119. package/tool-docs/batch.md +85 -1
  120. package/tool-docs/browser.md +3 -1
  121. package/tool-docs/code_search.md +3 -1
  122. package/tool-docs/consult_start.md +6 -2
  123. package/tool-docs/consult_stop.md +3 -1
  124. package/tool-docs/delete.md +3 -0
  125. package/tool-docs/doc_check.md +43 -8
  126. package/tool-docs/doc_search.md +5 -1
  127. package/tool-docs/edit.md +22 -1
  128. package/tool-docs/execute.md +7 -1
  129. package/tool-docs/fetch.md +3 -1
  130. package/tool-docs/file_ops.md +3 -1
  131. package/tool-docs/git.md +9 -1
  132. package/tool-docs/goal.md +2 -0
  133. package/tool-docs/grep.md +1 -1
  134. package/tool-docs/hashline_edit.md +2 -0
  135. package/tool-docs/insert_after.md +3 -1
  136. package/tool-docs/ledger.md +5 -5
  137. package/tool-docs/lint.md +1 -0
  138. package/tool-docs/manifest-read.md +2 -1
  139. package/tool-docs/manifest.md +9 -2
  140. package/tool-docs/memory.md +11 -1
  141. package/tool-docs/notify_parent.md +1 -1
  142. package/tool-docs/plan.md +2 -0
  143. package/tool-docs/question.md +1 -1
  144. package/tool-docs/read_history.md +15 -3
  145. package/tool-docs/read_image.md +3 -1
  146. package/tool-docs/recent_changes.md +3 -1
  147. package/tool-docs/repo_outline.md +3 -1
  148. package/tool-docs/settings.md +10 -2
  149. package/tool-docs/skill.md +2 -0
  150. package/tool-docs/ssh.md +30 -17
  151. package/tool-docs/subagent.md +50 -13
  152. package/tool-docs/task.md +2 -0
  153. package/tool-docs/team_ledger.md +1 -1
  154. package/tool-docs/timer.md +7 -0
  155. package/tool-docs/verify.md +13 -1
  156. package/tool-docs/websearch.md +3 -1
  157. package/tool-docs/write.md +2 -0
  158. package/tools/bash.mjs +12 -2
  159. package/tools/edit-batch.mjs +40 -33
  160. package/tools/edit-diff.mjs +69 -1
  161. package/tools/execute.mjs +10 -4
  162. package/tools/file.mjs +17 -27
  163. package/tools/git.mjs +27 -1
  164. package/tools/ops.mjs +2 -1
  165. package/tools/patch.mjs +12 -3
  166. package/tools/process-doc-mark.mjs +139 -0
  167. package/tools/search.mjs +15 -6
  168. package/tools/shared.mjs +24 -1
  169. package/tools/ssh.mjs +48 -7
  170. package/tools/tree.mjs +4 -1
  171. package/tools/write-path.mjs +64 -3
  172. package/traces/trace-store.mjs +4 -3
  173. package/agent/light-round.mjs +0 -63
  174. package/agent/unbooked-pen.mjs +0 -62
@@ -0,0 +1,99 @@
1
+ /**
2
+ * agent/write-face.mjs — 面判定单源(写锁与设计门共用的判据底座)。
3
+ *
4
+ * 权威规格 = `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.1 / §2.10;面分类的原文正源 =
5
+ * `docs/core/requirements/ENGINEERING-MODE-V2.md` §13.5。本档只做一件事:给一个绝对路径判「产品代码面
6
+ * / 文档面」,判据逐步骤对应 §2.10 的两值表。
7
+ *
8
+ * 面**只有两值**:文档面(放行)∥ 产品面(拦)——设计门按它拦;写锁 2026-11-03 起不分面(设计档 §2.3),
9
+ * 面在那里只选拒文变体。四格先命中先定:
10
+ * 1 出辖(祖先链无 manifest)⇒ 放行(`out-of-jurisdiction`)——辖界,不是面
11
+ * 2 manifest `docRoot` 前缀命中 ⇒ 放行(`doc-root`,D1 写权矩阵在写面上的机判投影)
12
+ * 3 文件 = `PROJECT-MANIFEST.json`(项目状态档)⇒ 放行(`manifest-state`)
13
+ * 4 其余一切 ⇒ 产品面(`product`,fail-closed——判不出面就按产品面走全链)
14
+ *
15
+ * 为什么不再复用 `classifyPath`:旧形把「构建产物 / 仓根 `scripts` ∥ `test` ∥ `.thincoder` ∥ `.github`
16
+ * / 锁文件 / `classifyPath` 的 temp·aux·state」当放行桶——每多一个放行桶,就多一条「这一步要不要走流程」
17
+ * 的自判口子(用户 2026-11-02 裁定退役)。两值之后没有第三桶、没有豁免、没有待裁格;`classifyPath` /
18
+ * `isCodePath` **本体零改**,它们仍是索引 / 检索 / verify 那一族的判据(七个生产消费面)。
19
+ *
20
+ * `test` 标记与面值**分离**:路径含 `test` / `tests` 段 ⇒ `test: true`,只决定写锁用测试变体拒文,
21
+ * 不改 `face`。
22
+ */
23
+ import { isAbsolute, relative, resolve } from "node:path"
24
+
25
+ import { declarationForTarget } from "../conventions.mjs"
26
+ import { docRootPaths, manifestFilePath, readManifest } from "../manifest.mjs"
27
+ // 文档面键集**不另持字面**(单源 = `agent-tools/review-facts.mjs` 的 `REVIEW_ROOT_KEYS`——同一组 docRoot 子键,
28
+ // 两份清单漂移就是「评审面放的 / 写面拦的」各说各话)。
29
+ import { REVIEW_ROOT_KEYS } from "../agent-tools/review-facts.mjs"
30
+
31
+ /** 比较键(绝对化 + 分隔符归一 `/` + win32 大小写归一——同 `write-gate.mjs` 的 `gateKey` 口径)。 */
32
+ const faceKey = (p) => {
33
+ const s = resolve(String(p)).replace(/\\/g, "/")
34
+ return process.platform === "win32" ? s.toLowerCase() : s
35
+ }
36
+
37
+ /** 该路径所属项目根的文档面根集(绝对比较键)。manifest 读不到 ⇒ 空集(兜底 = 产品代码面)。 */
38
+ function docRootKeys(root, manifest) {
39
+ let man = manifest
40
+ if (!man || typeof man !== "object" || !man.docRoot) {
41
+ try { man = readManifest(root).manifest } catch { man = null }
42
+ }
43
+ if (!man || typeof man !== "object") return []
44
+ const out = []
45
+ for (const key of REVIEW_ROOT_KEYS) {
46
+ for (const abs of docRootPaths(man.docRoot?.[key], root)) out.push(faceKey(abs))
47
+ }
48
+ return out
49
+ }
50
+
51
+ /** 路径段里有没有 `test` / `tests`(拒文变体句的判据,与面值无关)。 */
52
+ function hasTestSegment(relOrAbs) {
53
+ return String(relOrAbs).split(/[\\/]+/).filter(Boolean).some((s) => {
54
+ const low = s.toLowerCase()
55
+ return low === "test" || low === "tests"
56
+ })
57
+ }
58
+
59
+ /**
60
+ * 面判定(判据本体)。返回 `{face, reason, test}`:
61
+ * - `face` = `"product"`(产品面 ⇒ 设计门拦;写锁里只选拒文变体)∥ `"pass"`(放行面);
62
+ * - `reason` = 命中的那一格——**只有四值**:`out-of-jurisdiction` / `doc-root` / `manifest-state` /
63
+ * `product`(回执与用例的定位面);
64
+ * - `test` = 路径含 `test` / `tests` 段(产品测试面 ⇒ 写锁换测试变体那句拒文;不改 `face`)。
65
+ * @param {string} absPath — 目标绝对路径(相对形由调用方先 resolve;本档不猜会话锚)
66
+ * @param {object|null} [manifest] — 所属项目的 manifest(缺省 ⇒ 按项目根读;读不到 ⇒ 文档面空集)
67
+ * @returns {{face: "product"|"pass", reason: "out-of-jurisdiction"|"doc-root"|"manifest-state"|"product", test: boolean}}
68
+ */
69
+ export function faceDetail(absPath, manifest = null) {
70
+ const abs = resolve(String(absPath ?? ""))
71
+ const conv = declarationForTarget(abs) // 出辖 ⇒ null(祖先链无 manifest)
72
+ if (!conv) return { face: "pass", reason: "out-of-jurisdiction", test: hasTestSegment(abs) }
73
+ const root = conv.root
74
+ const rel = relative(root, abs)
75
+ const inRoot = rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))
76
+ const test = hasTestSegment(inRoot ? rel : abs)
77
+ if (inRoot) {
78
+ const key = faceKey(abs)
79
+ // 2 文档面(manifest docRoot)
80
+ if (docRootKeys(root, manifest).some((d) => key === d || key.startsWith(`${d}/`))) {
81
+ return { face: "pass", reason: "doc-root", test }
82
+ }
83
+ // 3 项目状态档(主 agent 的声明面——D1 写权矩阵同源)
84
+ if (key === faceKey(manifestFilePath(root))) return { face: "pass", reason: "manifest-state", test }
85
+ }
86
+ // 4 其余一切 = 产品面(fail-closed)
87
+ return { face: "product", reason: "product", test }
88
+ }
89
+
90
+ /**
91
+ * 面判定(§2.1 契约形):`"product"` = 产品面(设计门按它拦;写锁里只选拒文变体——台账在途行才是判据),
92
+ * `"pass"` = 放行面(文档面 / 出辖)。
93
+ * @param {string} absPath — 目标绝对路径
94
+ * @param {object|null} [manifest] — 所属项目 manifest(缺省 ⇒ 按项目根读)
95
+ * @returns {"product"|"pass"}
96
+ */
97
+ export function faceOf(absPath, manifest = null) {
98
+ return faceDetail(absPath, manifest).face
99
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * agent/write-gate.mjs — M4 写权门禁(评审目标解析 + D5 冻结窗口判据组装)。
2
+ * agent/write-gate.mjs — M4 写权门禁(评审目标解析 + D5 冻结窗口判据组装 + 写锁)。
3
3
  *
4
4
  * v2 唯一增量(模块设计 §2.1):评审对象 / 被审文件路径来源改读 manifest `docRoot`
5
5
  * (去硬编码 `docs/`),token 门(`anyLiveDesignSlot`)与冻结窗口谓词本体继承 v1 零改
6
- * (KD-M4-3)——本档承载三个单点:
6
+ * (KD-M4-3)——本档承载四个单点:
7
7
  *
8
8
  * 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
9
9
  * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组)。
@@ -25,17 +25,36 @@
25
25
  * `AGENT-LOOP-SUBAGENT.md` §6.29.1):判据与拒绝文案同址(文案字面只出本档)。
26
26
  * 2026-09-29 收窄(台账 #545):自身批次伴随件(非 `.md` · 同目录 · 词干族 = 三合取)放行——
27
27
  * 批次档本体 ∕ 他批伴随件 ∕ 异目录伴随件照拒;两端取用 = 核 `agent/dispatch.mjs` Phase 1(两端同径)。
28
+ * 4. `ledgerBindWriteConflict(agent, depth, tool, toolName, args)` —— 写锁(第三道门 · 设计档 §2.3):
29
+ * 判据 = 「**本笔报的** `#N` 是目标所属项目台账里一条 `status = 在途` 的行」(笔-账绑定——用户
30
+ * 2026-11-03 02:02 裁「甲 · 逐笔带号」)。判据函数 `ledgerRowAt` 住本档模块内(不导出)。
31
+ * 2026-11-03 不分面批:文档面 / 项目状态档 / 产品面 / 产品测面读同一条判据;面在判据里退场。
32
+ * 2026-11-03 笔-账绑定批:入口集扩到十二把(写盘七件 + `bash` ∥ `execute` + `git` + `manifest` ∥ `memory`),
33
+ * `git` 只对写型 action 要号(枚举可判,不猜命令字面),拒文成为唯一教学面(`ledger` 不进 `required`)。
34
+ * 2026-11-03 项目声明面批:**判据点从会话锚换成调用方声明的项目**(决策 40 ∥ 41)——定位参数优先,
35
+ * `project` 补位;会话锚永不充当判据点(`no project on this call` ∥ `declared project is not a project`)。
36
+ * 2026-11-04 ssh 传输面批:入口集扩到十三把(+ `ssh`——只有 `get` 往本地落盘,按 action 判要号:
37
+ * `connect` / `send` / `read` / `list` 不写项目内档,不得被本门问到)。
28
38
  *
29
39
  * 继承零改面(本档不碰):token 门资格判据(anyLiveDesignSlot / validateDesignToken)、
30
40
  * 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
31
41
  */
32
- import { basename, dirname, resolve } from "node:path"
42
+ import { existsSync } from "node:fs"
43
+ import { basename, dirname, join, resolve } from "node:path"
33
44
  // advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
34
45
  // 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
35
46
  // (指针链非副本),M6 经本档一行 import 取齐。
36
47
  import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-settle.mjs"
37
48
  // 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
38
49
  import { batchDocBases } from "../agent-tools/batch-paths.mjs"
50
+ // 项目根推导 + manifest 读面(写锁判据 · 2026-11-03 笔-账绑定批):判据按**调用方声明的项目**问该根台账(§2.3)。
51
+ // `MANIFEST_REL` = 「自身即带 manifest」那一支的判据(决策 41 的 (a) 支)。
52
+ import { MANIFEST_REL, readManifest, resolveProjectRoot } from "../manifest.mjs"
53
+ // 动作级只读谓词(`git` 写型 ∥ 只读的判法单源 = 既有 `readonlyActionOf`,决策 37——不新写一套分类)。
54
+ // 钩子优先 ∥ 核名面谓词回落:`git` 的名单落 `tools/git.mjs` 的 `isReadonlyAction`(单源,端覆写同笔删)。
55
+ import { readonlyActionOf } from "./dispatch-gates.mjs"
56
+ // 工具自报触达面单源谓词(写锁的路径集由判据函数自己算——§2.3;与门禁 / 变更记账同一枚谓词)。
57
+ import { toolTouchPaths } from "./helpers.mjs"
39
58
  export { normAbs }
40
59
  // 2026-10-04(工具路径基面根治批 · #921 · KD-2 同名再出口):REVIEW_ROOT_KEYS +
41
60
  // resolveReviewRootsFor 判定本体迁 `review-facts.mjs`(评审事实面——advisor-async 既有
@@ -56,7 +75,7 @@ import { resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
56
75
  * @returns {string[]} 评审目标目录的绝对路径(去重)
57
76
  */
58
77
  export function resolveReviewTargetPaths(agent) {
59
- return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
78
+ return resolveReviewRootsFor(agent?.cwd)
60
79
  }
61
80
 
62
81
  /**
@@ -151,7 +170,7 @@ export function batchRecordWriteConflict(agent, depth, absPaths) {
151
170
  if (typeof bound !== "string" || bound.length === 0) return null
152
171
  const wanted = (absPaths ?? []).filter((p) => typeof p === "string" && p.length > 0)
153
172
  if (wanted.length === 0) return null
154
- const cwd = typeof agent?.cwd === "string" && agent.cwd.length > 0 ? agent.cwd : process.cwd()
173
+ const cwd = agent.cwd
155
174
  const bases = memoizedBatchBases(agent, cwd)
156
175
  const boundAbs = normAbs(bound, cwd)
157
176
  const boundKey = gateKey(boundAbs)
@@ -170,3 +189,333 @@ export function batchRecordWriteConflict(agent, depth, absPaths) {
170
189
  return null
171
190
  }
172
191
 
192
+ // ─── 写锁(第三道门 · 2026-11-03 笔-账绑定批 · 设计档 §2.3)───────────────────────────
193
+
194
+ /** 写门入口集(设计档 §2.3):判据 = 「凡能把字节落到项目内的工具」。十三把 = 写盘七件 +
195
+ * 三把命令工具 + 两把结构化 action 工具(`manifest` ∥ `memory` 按各自的枚举判——决策 39)+ `ssh`
196
+ * (只有 `get` 落盘,按 action 判)。
197
+ * `settings`(写 `~/.thincoder/config.json`)∥ `browser`(自有 profile 与截图目录)不写项目内档 ⇒ 不进。
198
+ * `ledger` ∥ `batch` 也不进——挂行与开档不经过写面,「没号就挂不了账」的锁死因此不存在。 */
199
+ const LEDGER_ENTRY_TOOLS = new Set([
200
+ "write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit", "file_ops",
201
+ "bash", "execute", "git", "manifest", "memory", "ssh",
202
+ ])
203
+ /** 结构化 action 工具的写型枚举(按值判,不解析内容——决策 37 ∥ 39 同法)。 */
204
+ const MANIFEST_WRITE_ACTIONS = new Set(["init", "write"])
205
+ /** `memory` 里**能落 project 层档**的写型 action:`put` 按 `layer`、`delete` 按 `layer` ∥ id 前缀。
206
+ * `clear` 只对 personal 开放(该 action 自己拒 shared 层)∥ `maintain` 无 `layer` 参数(库级维护——MEMORY.md §6.6.1)
207
+ * ⇒ 两者都落不到 `<项目>/.thincoder/memory/**`,故不进枚举(枚举只收真能落项目内档的值——空的覆盖项比没有更坏)。 */
208
+ const MEMORY_WRITE_ACTIONS = new Set(["put", "delete"])
209
+ /** `memory` 的落盘目标按层分:只有 `project` 层写 `<项目>/.thincoder/memory/**`(`personal` ∥ `team` 落用户数据目录)。 */
210
+ const MEMORY_PROJECT_LAYER = "project"
211
+ /** 单条删除的层由 id 前缀定(`layer` 可省——`thincoder-core/memory/memory-tool.mjs` 的 `execDeleteSingle`
212
+ * 逐字「layer is OPTIONAL … else the id prefix routes the delete」)⇒ 前缀与 `layer` 同判:
213
+ * 只看 `layer` 会漏掉「不带 layer、按 `project:` 前缀删 project 层档」这条写项目内字节的路。 */
214
+ const MEMORY_PROJECT_UID_PREFIX = "project:"
215
+ /** 在途行的状态词(台账六态之一):只有它携带写权。 */
216
+ const IN_FLIGHT_STATUS = "在途"
217
+
218
+ /** 拒文(逐字 = 设计档 §2.3——机检可断言):一条基串 + 按情形选的 `<case>` + 用法句 + 本项目在途清单 + 挂号两步。
219
+ * `ledger` ∥ `project` 都不进 `required`(决策 35 ∥ 42)⇒ 模型不知道有这两个参数,第一次撞的就是这条拒文,
220
+ * 它是这条设计唯一的教学面(决策 38):缺什么 / 要带什么(**两个输入一次点出**)/ 当下有哪些号可带,缺一不可。 */
221
+ const LEDGER_PARAM_SENTENCE = 'Pass project:<the project root that carries PROJECT-MANIFEST.json> and ledger:"#N" on this call — engineering mode binds every write to a project and to the in-flight ledger row it serves; a call that names neither is refused.'
222
+ const LEDGER_HANG_STEPS = 'If none is in flight, hang one first — neither step is a write: ① ledger({action:"add", title, kind, cwd:<project root>}) hangs the row; ② batch({action:"create", ledger:"#N", topic, source}) opens the record over it (row → 在途).'
223
+ /** 清单上限(设计档 §2.3):在途行多时逐条列全会把教学面变成台账导出。 */
224
+ const LEDGER_LIST_LIMIT = 10
225
+
226
+ const CASE_NO_NUMBER = "no ledger number on this call"
227
+ const CASE_NO_PROJECT = "no project on this call"
228
+ /** 无项目时清单位的三种取值(见 `ledgerNoProjectRefusal`):缺声明 ∥ 声明无效 ∥ 写盘目标不在任何项目里。
229
+ * 三种各用自己的一句:一句里两个结论会让读拒文的人分不清该改哪一边。 */
230
+ const LIST_UNKNOWN_NO_PROJECT = "unknown — no project on this call"
231
+ const LIST_UNKNOWN_NOT_A_PROJECT = "unknown — the declared project is not a project"
232
+ const LIST_UNKNOWN_TARGET_NOT_A_PROJECT = "unknown — no project owns this write's target"
233
+ const CASE_UNREADABLE = "no ledger can be read for this project"
234
+ const caseNotRowNumber = (raw) => `ledger number ${JSON.stringify(String(raw))} is not a row number — pass "#N" or "N"`
235
+ const caseNoSuchRow = (no) => `row #${no} does not exist in this project's ledger`
236
+ const caseNotInFlight = (no, status) => `row #${no} is ${JSON.stringify(String(status))} — only a ${IN_FLIGHT_STATUS} row carries writes`
237
+ /** 声明无效(设计档 §2.3 拒文表逐字:`declared project is not a project (no PROJECT-MANIFEST.json at this path or
238
+ * within the session anchor): "<path>"`)—— 声明与写盘目标两种申报形共用这一句:两者都没在每个被搜过的位置
239
+ * 找到 manifest(写盘目标沿祖先链搜得更远,那句括注仍然为真)。 */
240
+ const caseNotAProject = (p) => `declared project is not a project (no PROJECT-MANIFEST.json at this path or within the session anchor): ${JSON.stringify(String(p))}`
241
+
242
+ /** 当下在途行清单(决策 38 的第三件):`#N, #M …`;无 ⇒ `none`;超上限折 `… +K more`。 */
243
+ function ledgerInFlightList(rows) {
244
+ const nums = (Array.isArray(rows) ? rows : [])
245
+ .filter((r) => r?.status === IN_FLIGHT_STATUS)
246
+ .map((r) => `#${r.id}`)
247
+ if (nums.length === 0) return "none"
248
+ if (nums.length <= LEDGER_LIST_LIMIT) return nums.join(", ")
249
+ return `${nums.slice(0, LEDGER_LIST_LIMIT).join(", ")}, … +${nums.length - LEDGER_LIST_LIMIT} more`
250
+ }
251
+
252
+ /** 拒文组装(可读台账形):基串 + `<case>` + 用法句 + 在途清单 + 挂号两步。
253
+ * 清单按「**本项目的**账」说(判据点换成声明后,号只在那一个项目的账上成立)。 */
254
+ function ledgerRefusal(caseText, rows) {
255
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} In-flight rows in this project's ledger right now: ${ledgerInFlightList(rows)}. ${LEDGER_HANG_STEPS}`
256
+ }
257
+
258
+ /** 拒文组装(无项目形):没有可判的项目就没有「本项目的账」可问——清单位写 `unknown` 并点名哪一种无效,
259
+ * **不写 `none`**(那会把「不知道判哪个项目」说成「本项目一条在途行都没有」,模型会照它去挂号而忘了声明项目)。
260
+ * 两种无效的项目各用自己的清单句:声明缺失 ≠ 声明了个不是项目的东西,一句里两个结论会让读拒文的人分不清。
261
+ * 用法句仍把两个输入一起点出(R33:三件在每一支都在位)。 */
262
+ function ledgerNoProjectRefusal(caseText, listText) {
263
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} In-flight rows in this project's ledger right now: ${listText}. ${LEDGER_HANG_STEPS}`
264
+ }
265
+
266
+ /** 拒文组装(台账读不到形):基串 + `<case>` + 用法句 + 挂号两步,另起一行带原始错误;
267
+ * **不列在途清单**——读不到就没有清单,写个 `none` 是把「读不到」说成「一条都没有」。 */
268
+ function ledgerUnreadableRefusal(caseText, error) {
269
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} ${LEDGER_HANG_STEPS}\n(ledger unreadable — ${error})`
270
+ }
271
+
272
+ /** 台账号解析(`#` 可省——`"#2016"` 与 `"2016"` 同判,设计档 §2.3):三种结果
273
+ * `{ case: "missing" }`(未传 / 空串 / 全空白)· `{ case: "form", raw }`(剥前导 `#` 后非纯数字)· `{ no }`。 */
274
+ function parseLedgerNo(raw) {
275
+ if (raw === undefined || raw === null) return { case: "missing" }
276
+ const text = String(raw).trim()
277
+ if (text === "") return { case: "missing" }
278
+ const m = /^#?(\d+)$/.exec(text)
279
+ if (!m) return { case: "form", raw: text }
280
+ return { no: Number(m[1]) }
281
+ }
282
+
283
+ /** 该根台账的行集(判据的第二个输入)。两种「根」不是一回事(§2.3 的钉子):根**不在**(声明无效)
284
+ * 由调用方在取根那一步拒(`declared project is not a project`);manifest **在而不可用**(读不动 / 坏 JSON)
285
+ * 与库开不了 ⇒ 返回 `error`,一律落拒——判的是「是不是项目」,不是「项目好不好用」。 */
286
+ async function ledgerRowsOf(root) {
287
+ try {
288
+ const man = readManifest(root)
289
+ if (!man?.ok) {
290
+ const detail = (man?.errors ?? []).filter((s) => typeof s === "string" && s).join(";")
291
+ return { rows: [], error: detail || String(man?.reason ?? "manifest unusable") }
292
+ }
293
+ } catch (e) {
294
+ return { rows: [], error: String(e?.message ?? e) }
295
+ }
296
+ try {
297
+ // W8 契约②:node:sqlite 链只经动态 import 进入(勿升静态)。
298
+ // `boundary` 必传、传的就是刚算出的那个根:`openLedger` 首行即 `resolve(boundary)`,
299
+ // 缺省直接抛 ⇒ 每一笔写都被判成「台账读不到」(§2.3 读法)。
300
+ const { ledgerQuery } = await import("../ledger.mjs")
301
+ const rows = ledgerQuery({ cwd: root, boundary: root })
302
+ return { rows: Array.isArray(rows) ? rows : [], error: null }
303
+ } catch (e) {
304
+ return { rows: [], error: String(e?.message ?? e) } // fail-closed:读不到不等于放行
305
+ }
306
+ }
307
+
308
+ /**
309
+ * 判据函数(**模块内,不导出**——唯一消费方是本门的判据本体;导出去就是下一个零消费符号):
310
+ * 已算出的项目根 → 该根台账的行集 → 按 `id` 找本笔报的那一行(设计档 §2.3「读法」)。
311
+ *
312
+ * **判据点不再是「目标自身推根」**(2026-11-03 项目声明面批 · 决策 40):项目由调用方声明,本函数只负责
313
+ * 「拿一个已成立的项目根 + 一个号」这一半;「根从哪来」由 `ledgerGateTargets` + `resolveDeclaredRoot` 回答。
314
+ * 判据点不存在(不在任何项目里)现在由调用方落 `declared project is not a project`,不再是「门不管」。
315
+ *
316
+ * **不为门新增按 id 的列过滤**(§2.3):`ledgerQuery` 现有过滤键只有 `status` / `kind` / `board` /
317
+ * `trigger` / `task_book`,行集取全量后按 `id` 找——同一笔写的多根按 `root` 缓存,一次写只读一遍账。
318
+ *
319
+ * @param {string} root — 已成立的判据点(项目根绝对路径)
320
+ * @param {number|null} ledgerNo — 本笔报的行号(缺号 / 非行号形时传 null)
321
+ * @param {Map<string, object>|null} [cache] — 同一笔写的根 → 行集(多目标同项目时只问一次台账)
322
+ * @returns {Promise<{row: object|null, rows: object[], error: string|null}>}
323
+ * `row` = 找到的那一行(查不到 ⇒ null);`error` = 读不到时的原始错误(fail-closed 拒文点名它)
324
+ */
325
+ async function ledgerRowAt(root, ledgerNo, cache = null) {
326
+ let base = cache instanceof Map ? cache.get(root) : null
327
+ if (!base) {
328
+ base = await ledgerRowsOf(root)
329
+ if (cache instanceof Map) cache.set(root, base)
330
+ }
331
+ if (base.error) return { row: null, rows: [], error: base.error }
332
+ const row = ledgerNo === null ? null : base.rows.find((r) => Number(r?.id) === ledgerNo) ?? null
333
+ return { row, rows: base.rows, error: null }
334
+ }
335
+
336
+ /** 声明项目的取根(设计档 §2.3「两个分支的落法」/ §5 R31)——**界 = 相关性判据,不是单向的「不许越界」**:
337
+ * (b) 支 = 既有单源 `resolveLedgerRoot(声明, agent.cwd)`:起点在会话锚之内 ⇒ 沿祖先链上溯到最近带 manifest 的目录;
338
+ * (b) 取不到再走 (a) 支 = 声明路径**自身即带 manifest**且会话锚在它之下 ⇒ 根 = 声明路径(`basis = self`,不上溯)。
339
+ * **为什么 (a) 支要门自己判一次**:`resolveLedgerRoot` 的上溯界就是会话锚,起点落在界之上时它取不到根;
340
+ * 而「会话 cwd 在项目子目录」正是项目指南记录的启动形(`cd thincoder/thincoder-cli && node bin/thincoder.cjs`)——
341
+ * 不认这一支,那种会话里 `bash` ∥ `memory` 声明根也被拒、声明子目录也被拒 = 无解。
342
+ * 两支都取不到(不存在 ∥ 平级目录 ∥ 不含锚的更远祖先 ∥ 界内上溯无 manifest)⇒ null(拒,点名该路径)。
343
+ * **会话锚在这两支里都不是「判哪个项目」的答案**(决策 41):它只用来定界与判相关性。
344
+ * @param {string} declared — 调用方给的项目声明(绝对 ∥ 相对;相对按会话 cwd 解析)
345
+ * @param {string} cwd — 会话锚
346
+ * @returns {Promise<{root: string, basis: "self"|"ancestor"}|null>} */
347
+ async function resolveDeclaredRoot(declared, cwd) {
348
+ const start = resolve(cwd, String(declared))
349
+ // 声明路径不存在 ⇒ 拒(§5 R31 四态第一态):不加这一行,`owningProject` 会从一条不存在的路径上溯,
350
+ // 把拼错的声明归到会话自己那个项目上——那正是本批要关掉的「会话锚当判据点」(决策 41)。
351
+ if (!existsSync(start)) return null
352
+ // W8 契约②:node:sqlite 链只经动态 import 进入(`resolveLedgerRoot` 与 `ledgerQuery` 同住该链;模块已缓存)。
353
+ const { resolveLedgerRoot } = await import("../ledger.mjs")
354
+ const owned = resolveLedgerRoot(start, cwd)
355
+ if (owned) return { root: owned.root, basis: owned.basis }
356
+ if (existsSync(join(start, MANIFEST_REL)) && underKey(start, cwd)) return { root: start, basis: "self" }
357
+ return null
358
+ }
359
+
360
+ /** `child` 是否等于 `root` 或在其子树内(比较键 = `gateKey`:绝对化 + `/` 归一 + win32 大小写归一)。 */
361
+ function underKey(root, child) {
362
+ const r = gateKey(root)
363
+ const c = gateKey(child)
364
+ return c === r || c.startsWith(r.endsWith("/") ? r : r + "/")
365
+ }
366
+
367
+ /** 本笔的判据点(设计档 §2.3「判据点怎么来」· 决策 40):**只有两个来源,都来自调用方**——
368
+ * ① 该工具自带的定位参数:写盘七件的目标路径(`toolTouchPaths`)· `execute.workdir` · `git.path` ∥ `workdir`
369
+ * (`path` 相对 `workdir`——`thincoder-core/tools/git.mjs` 的 `resolveBaseDir` 同源;设计档要求「`toolTouchPaths`
370
+ * 再补 `path` ∥ `workdir`」,而 `git` 无 `touchedPaths` 钩子 ⇒ `toolTouchPaths` 的回落形正是 `[args.path]`,
371
+ * 不用它是因为它按会话 cwd 归一,而 `git` 的 `path` 相对 `workdir`)· `manifest.target`;
372
+ * ② 新参数 `project`(项目根绝对路径,纯声明)。**定位参数优先**,`project` 只在定位参数缺位时被读。
373
+ * **会话锚永不充当判据点**(决策 41):`bash` ∥ `memory` 没有定位参数,缺 `project` 即拒;
374
+ * `execute` ∥ `git` ∥ `manifest` 缺定位参数时同样不回落到 cwd。
375
+ * 三种结果:`{ paths }`(按路径所属项目判)· `{ declared }`(按声明取根)· `{ none: true }`(无判据点 ⇒ 拒);
376
+ * 写盘工具连路径都没有 ⇒ null(工具自己拒,本门不问)。 */
377
+ function ledgerGateTargets(agent, toolName, tool, args) {
378
+ const cwd = resolve(agent?.cwd ?? ".")
379
+ const asAbs = (p) => (typeof p === "string" && p.trim() ? resolve(cwd, p) : null)
380
+ const declared = typeof args?.project === "string" && args.project.trim() ? args.project.trim() : null
381
+ const onlyDeclared = () => (declared ? { declared } : { none: true })
382
+ if (toolName === "bash" || toolName === "memory") return onlyDeclared()
383
+ if (toolName === "execute") {
384
+ const wd = asAbs(args?.workdir)
385
+ return wd ? { paths: [wd] } : onlyDeclared()
386
+ }
387
+ if (toolName === "git") {
388
+ const wd = asAbs(args?.workdir)
389
+ const p = typeof args?.path === "string" && args.path.trim() ? resolve(wd ?? cwd, args.path) : null
390
+ if (p) return { paths: wd ? [p, wd] : [p] } // 两个都在 ⇒ 两个项目都要带号(§2.3「一笔写跨两个项目」)
391
+ return wd ? { paths: [wd] } : onlyDeclared()
392
+ }
393
+ if (toolName === "manifest") {
394
+ const t = asAbs(args?.target)
395
+ return t ? { paths: [t] } : onlyDeclared()
396
+ }
397
+ const paths = toolTouchPaths(tool, args)
398
+ .filter((p) => typeof p === "string" && p.trim())
399
+ .map((p) => resolve(cwd, p))
400
+ return paths.length === 0 ? null : { paths }
401
+ }
402
+
403
+ /** 本笔是否需要号(工具自己的枚举判写型——不解析命令字面,决策 36 的禁令不碰):
404
+ * `git` 只对写型 action 要号(只读名单 = 核钩子,决策 37);`manifest` 的 `init` ∥ `write` 都要号——
405
+ * `init` 的豁免是条件形(决策 43:目标沿祖先链取不到项目根时在取根步跳过本门),不是摘出要号集;
406
+ * `memory` 只对落 project 层档的写型 action 要号(`layer:"project"` ∥ 单条删除的 `project:` 前缀——
407
+ * 后者不带 `layer` 也能删 project 层档,只看 `layer` 就漏了那条路);`ssh` 只对 `get` 要号(`put` 写的是
408
+ * 远端主机,其余动作不写项目内档——缺此分支则整个会话面都要号,把会话面整条打死)。 */
409
+ function ledgerGateApplies(toolName, tool, args) {
410
+ if (toolName === "git") return !readonlyActionOf(tool, "git", args)
411
+ if (toolName === "manifest") return MANIFEST_WRITE_ACTIONS.has(String(args?.action ?? ""))
412
+ if (toolName === "memory") return memoryTargetsProjectLayer(args)
413
+ if (toolName === "ssh") return String(args?.action ?? "") === "get"
414
+ return true
415
+ }
416
+
417
+ /** `memory` 这一笔是否落 project 层:`layer:"project"`,或单条删除的 id 前缀 = `project:`(`layer` 省略时
418
+ * 工具按前缀路由到 project 层)。两个输入都是工具自己的枚举 ∥ 约定,不解析笔记内容(决策 36 的禁令不碰)。
419
+ * 显式给了别的层 ⇒ 不过本门(工具自己按该层处置——前缀不符它会拒)。 */
420
+ function memoryTargetsProjectLayer(args) {
421
+ const action = String(args?.action ?? "")
422
+ if (!MEMORY_WRITE_ACTIONS.has(action)) return false
423
+ const layer = String(args?.layer ?? "").trim()
424
+ if (layer === MEMORY_PROJECT_LAYER) return true
425
+ if (layer !== "") return false
426
+ const id = args?.id
427
+ return typeof id === "string" && id.startsWith(MEMORY_PROJECT_UID_PREFIX)
428
+ }
429
+
430
+ /** 判序第三步:按号找行、判 `status`(号四情形——缺 ∥ 非行号形 ∥ 不在盘 ∥ 非在途)。
431
+ * `at.error` 非空 = 该根台账读不到 ⇒ 拒(真业务门:读不到不等于放行)。 */
432
+ function ledgerRowRefusal(parsed, at) {
433
+ if (at.error) return ledgerUnreadableRefusal(CASE_UNREADABLE, at.error)
434
+ if (parsed.case === "missing") return ledgerRefusal(CASE_NO_NUMBER, at.rows)
435
+ if (parsed.case === "form") return ledgerRefusal(caseNotRowNumber(parsed.raw), at.rows)
436
+ if (!at.row) return ledgerRefusal(caseNoSuchRow(parsed.no), at.rows)
437
+ if (at.row.status !== IN_FLIGHT_STATUS) return ledgerRefusal(caseNotInFlight(parsed.no, at.row.status), at.rows)
438
+ return null
439
+ }
440
+
441
+ /**
442
+ * 写锁判据组装(**第三道门**——与跨批档写 / D5 冻结窗并列挂 `dispatch.mjs` Phase 1):
443
+ * 辖内任何写盘 ∥ 跑命令 ∧ 本笔**同时给出项目与号**而号不是该项目台账里的 `在途` 行 ⇒ 冲突;否则 null。
444
+ *
445
+ * **判据只有一条,零上位条件**(§2.3):不判批档状态、不判指针互指。多一个合取项,就会在某个真实场景
446
+ * 变成「机制走不通」的证据,接着被用来把整套机制废掉。
447
+ *
448
+ * **为什么是笔-账绑定**:写盘工具的参数原本只有路径与内容,门只能问「这个仓有没有在途行」——一条在途行
449
+ * 于是放行该根下任何一笔写,它拦不住「有账但在做别的事」。判据换成「这笔写替哪条账服务」,门才第一次
450
+ * 看得见写者报的是哪条账(决策 34;形状 = 用户 2026-11-03 02:02 裁的「甲 · 逐笔带号」——轮内绑定会把
451
+ * 几件异步在做的事压成一条账)。
452
+ *
453
+ * **`bash` ∥ `execute` 与写盘工具同一判据**(决策 36):命令行内容无法可靠静态分类(`node -e` /
454
+ * `npm run <脚本>` / 参数拼接都能落盘),按字面分类本身就是第二个洞。代价 = 工程模式下父侧任何
455
+ * `bash` ∥ `execute` 都要有在途行(含 `npm test` / `git status`),正路 = 先挂行再干活。
456
+ *
457
+ * **面不参与放行**:文档面、项目状态档、产品代码面、产品测试面读同一条判据。
458
+ * 挂门范围(工程模式 ∧ `depth = 0`)说的是「谁被这条门问到」,不是判据的合取项。
459
+ *
460
+ * **判据点 = 调用方声明的项目**(2026-11-03 项目声明面批 · 决策 40):会话锚可以是容器根 / 工作区根,
461
+ * 拿它当判据点时那种形态下本门永不设防(实机:2026-11-03 02:53 重启后 `bash` 不带号照放行)。
462
+ * 现在只有两个来源、都来自调用方:① 工具自带的定位参数;② `project`(项目根绝对路径)。
463
+ * **会话锚永不充当判据点**(决策 41):声明缺位 ⇒ `no project on this call`;声明无关键或取不到根 ⇒
464
+ * `declared project is not a project`——没有「推不出 ⇒ 门不管」那条臂。
465
+ *
466
+ * 与既有「工程模式父侧门」(同 Phase 1 的设计门,非读取路径按面判拒)是**两件事**:前门问「设计做没做」,
467
+ * 本锁问「这笔写替哪条在途行服务」。所以并列、不替换。
468
+ *
469
+ * **业务门语义**:台账读不到(库不可开 / 根推得出而 manifest 不可用)⇒ **拒**,拒文另起
470
+ * `(ledger unreadable — <原错>)` 一行——读不到不等于放行。
471
+ * **只在工程模式生效**(开关 = `agent.config?.agent?.engineering`,与设计门同一枚);普通模式按
472
+ * `docs/core/requirements/NORMAL-MODE.md` 的「纪律自持」不动。
473
+ *
474
+ * @param {Object} agent — 会话 agent(读 `config.agent.engineering` / `cwd`)
475
+ * @param {number} depth — 执行深度(只在 depth 0 生效;子代理走既有 token 门,本门不核它)
476
+ * @param {Object} tool — 工具对象(`toolTouchPaths` 的钩子源 ∥ `readonlyActionOf` 的动作谓词钩子源)
477
+ * @param {string} toolName — 工具名(= `toolCall.name`;决定入口集与各工具的判法)
478
+ * @param {Object} args — 本次调用入参(判据点、`args.project` ∥ `args.ledger` 都由本函数自己取)
479
+ * @returns {Promise<{target: string, message: string}|null>} 命中 = 拒文 + 首个被拒目标;否则 null
480
+ */
481
+ export async function ledgerBindWriteConflict(agent, depth, tool, toolName, args) {
482
+ if (!agent?.config?.agent?.engineering) return null // 普通模式零改(纪律自持)
483
+ if (!(depth === 0)) return null // 子代理走既有 token 门
484
+ const name = String(toolName ?? tool?.name ?? "")
485
+ if (!LEDGER_ENTRY_TOOLS.has(name)) return null // 入口集之外 ⇒ 本门不问
486
+ if (!ledgerGateApplies(name, tool, args)) return null // 只读 action ∥ 不写项目内档的枚举值 ⇒ 不要号
487
+ const claim = ledgerGateTargets(agent, name, tool, args)
488
+ if (!claim) return null // 没有可判的目标(写盘工具缺 path 由工具自己拒)
489
+ const parsed = parseLedgerNo(args?.ledger)
490
+ const cwd = resolve(agent?.cwd ?? ".")
491
+
492
+ // ① 判序第一步:解析项目声明(缺 ⇒ `no project on this call`)——判据点不存在时,号对不对无从谈起。
493
+ if (claim.none) return { target: cwd, message: ledgerNoProjectRefusal(CASE_NO_PROJECT, LIST_UNKNOWN_NO_PROJECT) }
494
+
495
+ const cache = new Map()
496
+ if (claim.declared !== undefined) {
497
+ // ② 声明形式:界 = 相关性判据((b) 支上溯 ∥ (a) 支自身带 manifest);两支都取不到 ⇒ 拒。
498
+ const resolved = await resolveDeclaredRoot(claim.declared, cwd)
499
+ if (!resolved) {
500
+ return { target: claim.declared, message: ledgerNoProjectRefusal(caseNotAProject(claim.declared), LIST_UNKNOWN_NOT_A_PROJECT) }
501
+ }
502
+ const at = await ledgerRowAt(resolved.root, parsed.no ?? null, cache)
503
+ const message = ledgerRowRefusal(parsed, at)
504
+ return message ? { target: resolved.root, message } : null
505
+ }
506
+
507
+ // ③ 定位参数形式:每个目标按自己的祖先链取根(一笔写跨两个项目 ⇒ 号要在每个项目的账上各查到)。
508
+ // `init` 的取根豁免(决策 43 · 条件形):目标沿祖先链取不到项目根 ⇒ 不进本门(无账可问);
509
+ // 取得到根(含既有项目内的子目录自举)⇒ 照判要号,目标自身已有 manifest 时门过后由工具自身拒。
510
+ for (const p of claim.paths) {
511
+ const root = resolveProjectRoot(p)
512
+ if (!root) {
513
+ if (String(args?.action ?? "") === "init") return null
514
+ return { target: p, message: ledgerNoProjectRefusal(caseNotAProject(p), LIST_UNKNOWN_TARGET_NOT_A_PROJECT) }
515
+ }
516
+ const at = await ledgerRowAt(root, parsed.no ?? null, cache)
517
+ const message = ledgerRowRefusal(parsed, at)
518
+ if (message) return { target: p, message }
519
+ }
520
+ return null
521
+ }