@thincoder/core 0.10.6 → 0.10.8

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 (132) hide show
  1. package/CHANGELOG.md +24 -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 +11 -6
  9. package/agent/helpers.mjs +6 -4
  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/record-results.mjs +9 -1
  14. package/agent/run-stages.mjs +2 -1
  15. package/agent/setup-reminders.mjs +14 -13
  16. package/agent/setup.mjs +62 -46
  17. package/agent/spawn-child.mjs +1 -1
  18. package/agent/subagent-event.mjs +1 -1
  19. package/agent/suspension.mjs +2 -1
  20. package/agent/timers.mjs +8 -8
  21. package/agent/turn-loop.mjs +1 -1
  22. package/agent-tools/advisor.mjs +1 -1
  23. package/agent-tools/async-discard.mjs +40 -5
  24. package/agent-tools/async-settle.mjs +3 -1
  25. package/agent-tools/audit-block.mjs +1 -1
  26. package/agent-tools/batch-paths.mjs +20 -20
  27. package/agent-tools/checkpoint.mjs +1 -1
  28. package/agent-tools/design-slots.mjs +44 -5
  29. package/agent-tools/doc-check.mjs +37 -0
  30. package/agent-tools/eng.mjs +5 -3
  31. package/agent-tools/manifest.mjs +25 -18
  32. package/agent-tools/parent-channel.mjs +3 -3
  33. package/agent-tools/review-facts.mjs +2 -2
  34. package/agent-tools/settings.mjs +1 -1
  35. package/agent-tools/spawn-gates.mjs +8 -6
  36. package/agent-tools/ssh-session.mjs +439 -0
  37. package/agent-tools/ssh-transport.mjs +311 -0
  38. package/agent-tools/subagent-scheduler.mjs +6 -5
  39. package/agent-tools/subagent.mjs +7 -5
  40. package/agent-tools/task.mjs +1 -1
  41. package/agent-tools/timer.mjs +6 -0
  42. package/agent-tools/zero-write-watch.mjs +2 -2
  43. package/agent-tools.mjs +3 -2
  44. package/attachments.mjs +4 -4
  45. package/browser/cdp.mjs +2 -2
  46. package/browser/launch.mjs +50 -4
  47. package/browser/snapshot.mjs +1 -1
  48. package/config-io.mjs +7 -2
  49. package/config-watch.mjs +2 -2
  50. package/config.mjs +7 -0
  51. package/conventions.mjs +1 -1
  52. package/declaration.mjs +157 -13
  53. package/doc-check/anchors.mjs +100 -0
  54. package/doc-check/index.mjs +61 -0
  55. package/doc-check/width.mjs +131 -0
  56. package/file-refs.mjs +6 -6
  57. package/generate-title.mjs +1 -1
  58. package/hooks.mjs +1 -1
  59. package/i18n.mjs +11 -1
  60. package/ledger-cmd.mjs +4 -5
  61. package/ledger-db.mjs +100 -21
  62. package/ledger-migrate.mjs +3 -3
  63. package/ledger-read.mjs +6 -7
  64. package/ledger-tools.mjs +24 -9
  65. package/ledger.mjs +23 -54
  66. package/log.mjs +17 -3
  67. package/manifest-discovery.mjs +21 -18
  68. package/manifest-schema.mjs +2 -13
  69. package/manifest.mjs +37 -22
  70. package/mcp/transport-stdio.mjs +1 -1
  71. package/mcp.mjs +2 -2
  72. package/memory/code-search.mjs +21 -4
  73. package/memory/code-sync.mjs +7 -4
  74. package/memory/core.mjs +1 -1
  75. package/memory/docs.mjs +23 -6
  76. package/memory/file-list.mjs +13 -7
  77. package/memory/file-walk.mjs +9 -4
  78. package/memory/scan.mjs +1 -1
  79. package/memory/sweep.mjs +1 -1
  80. package/memory/sync-tail.mjs +11 -7
  81. package/model-specs.mjs +7 -7
  82. package/notify-policy.mjs +6 -6
  83. package/package.json +2 -1
  84. package/permission.mjs +14 -0
  85. package/prompts/advisor-design.md +43 -13
  86. package/prompts/advisor-round1.md +25 -5
  87. package/prompts/advisor-round2.md +17 -4
  88. package/prompts/advisor-round3.md +17 -4
  89. package/prompts/common.md +60 -6
  90. package/prompts/consult-base.md +8 -2
  91. package/prompts/discipline-engineering.md +11 -10
  92. package/prompts/discipline-normal.md +22 -14
  93. package/prompts/persona-coder.md +1 -1
  94. package/prompts/persona-eng-designer.md +8 -3
  95. package/prompts/persona-engineering.md +21 -5
  96. package/prompts/persona-normal.md +4 -1
  97. package/provider/errors.mjs +8 -0
  98. package/provider/google.mjs +3 -0
  99. package/provider/normalize.mjs +42 -12
  100. package/provider/responses-request.mjs +1 -1
  101. package/provider/responses.mjs +2 -2
  102. package/provider/wait-status.mjs +1 -1
  103. package/provider-flows.mjs +1 -1
  104. package/queued.mjs +2 -2
  105. package/rules.mjs +4 -4
  106. package/session-gc.mjs +1 -1
  107. package/session-lifecycle.mjs +1 -1
  108. package/session-slot-write.mjs +2 -2
  109. package/session-slots-manifest.mjs +1 -1
  110. package/session.mjs +1 -1
  111. package/shell-candidates.mjs +2 -2
  112. package/shell-identity.mjs +1 -1
  113. package/team.mjs +61 -4
  114. package/think-off.mjs +3 -3
  115. package/token-ttl.mjs +1 -1
  116. package/tool-docs/bash.md +1 -1
  117. package/tool-docs/browser.md +1 -1
  118. package/tool-docs/doc_check.md +31 -0
  119. package/tool-docs/manifest.md +14 -2
  120. package/tool-docs/ssh.md +34 -0
  121. package/tool-docs/subagent.md +2 -0
  122. package/tool-docs/timer.md +2 -1
  123. package/tools/browser.mjs +1 -1
  124. package/tools/execute.mjs +1 -1
  125. package/tools/git-run.mjs +2 -2
  126. package/tools/git.mjs +8 -8
  127. package/tools/index.mjs +3 -0
  128. package/tools/ops.mjs +1 -1
  129. package/tools/process-tree.mjs +2 -2
  130. package/tools/ssh-askpass.mjs +35 -0
  131. package/tools/ssh.mjs +92 -0
  132. package/vision-reader.mjs +4 -4
package/ledger-cmd.mjs CHANGED
@@ -11,8 +11,7 @@
11
11
  * 消费侧静态 import 会破 W8 契约②)。
12
12
  */
13
13
  import { resolve } from "node:path"
14
- import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES, AGING_DAYS } from "./ledger-db.mjs"
15
- import { resolveProjectRoot } from "./manifest.mjs"
14
+ import { AGING_DAYS, ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES, resolveLedgerRoot } from "./ledger-db.mjs"
16
15
  import { resolveDeclaredRef } from "./declaration.mjs"
17
16
 
18
17
  /** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
@@ -60,8 +59,8 @@ export function ledgerCount({ cwd } = {}) {
60
59
  * (首个 `§` 前子串,trim)须经基准 `resolve(base, …)` 指向存在的档;缺文件部分(`§2` / 空串 /
61
60
  * 全空白)⇒ 拒;不在册 / 非文件 ⇒ 拒(throw,行不变)。
62
61
  * 判位与迁移表判同层(落盘前);文案前缀 = 调用函数名(与同函数既有两条文案同款)。
63
- * 基准 `base` = **与台账库关联键同源**(2026-09-18 fix 轮 · O1 收正):`resolveProjectRoot(cwd) ??
64
- * resolve(cwd ?? ".")`——同表达式见 `ledger-db.mjs` `ledgerDbPath`;容器根会话(锚 cwd 下唯一注册
62
+ * 基准 `base` = **与台账库关联键同源**(设计档 §6.1 口径 2——根推导单源):`resolveLedgerRoot(cwd)?.root
63
+ * ?? resolve(cwd ?? ".")`——同一子表达式见 `ledger-db.mjs` `ledgerDbPath`;容器根会话(锚 cwd 下唯一注册
65
64
  * 子仓 P)与子仓会话 ⇒ 库键与指针基准同取 P(同库同基准;原实现 `resolve(cwd, …)` = 原始 cwd 会误拒)。
66
65
  * #832 ∥ #834:声明源前缀形同判——判定单源 = `declaration.mjs` `resolveDeclaredRef`(① 仓根解析 →
67
66
  * ② 声明源前缀解析序;同名多仓 ⇒ 声明序首者)——**与迁移旗(`ledger-migrate.mjs` `writeGateFlag`)
@@ -71,7 +70,7 @@ function assertTaskBookGate(cwd, fnName, status, taskBook) {
71
70
  if (taskBook == null) throw new Error(`${fnName}:task_book 必填(在途 / 待核销 须携任务书指针)`)
72
71
  const part = String(taskBook).split("§")[0].trim()
73
72
  if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
74
- const base = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
73
+ const base = resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
75
74
  const { ok, abs } = resolveDeclaredRef(base, part)
76
75
  if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
77
76
  }
package/ledger-db.mjs CHANGED
@@ -6,6 +6,9 @@
6
6
  * 靠 DDL(AC-M2-2),不靠应用层 if。归档 = 软删除(状态值,无物理 DELETE——KD-M2-4)。
7
7
  * 本档静态 import node:sqlite ⇒ 消费侧一律动态 import 本模块链(W8 契约②——KD-M2-3)。
8
8
  *
9
+ * 项目根推导单源(`resolveLedgerRoot`)+ 同级枚举(`ledgerChildren`)居本档——键 / 库位 / 根同源
10
+ * (设计档 `LEDGER.md` §2.1「根解析面」/ §3.8):读写同一判据(档在盘 ∨ 以目录自身路径为键的库在盘)。
11
+ *
9
12
  * 落点(2026-09-17 用户裁定收正):台账库**不在项目目录**——住用户数据目录
10
13
  * `~/.thincoder/ledger/<sha1(normalizeCwd(项目根绝对路径))[:16]>.db`(工作树外、天然不进 git;
11
14
  * 同区先例 = configDir 的 memory.db / tool-results)。项目根仅作**关联键**,不在项目内留任何文件。
@@ -13,12 +16,12 @@
13
16
  * `normalizeCwd`——与 session / checkpoint / traces / peers 同契约,跨端同 cwd 同键同库。
14
17
  */
15
18
  import { createHash } from "node:crypto"
16
- import { existsSync, mkdirSync } from "node:fs"
17
- import { join, resolve } from "node:path"
19
+ import { existsSync, mkdirSync, opendirSync } from "node:fs"
20
+ import { dirname, join, resolve } from "node:path"
18
21
  import { DatabaseSync } from "node:sqlite"
19
22
 
20
23
  import { configDir } from "./config-io.mjs"
21
- import { projectRootView, resolveProjectRoot } from "./manifest.mjs"
24
+ import { MANIFEST_REL, projectRootView } from "./manifest.mjs"
22
25
  // 归一步单源(KD-LN1——直引,不抽新 util:先例 = peer-domains.mjs / traces/trace-store.mjs)。
23
26
  import { normalizeCwd } from "./session-slots.mjs"
24
27
 
@@ -42,14 +45,89 @@ export function ledgerKey(root) {
42
45
  return createHash("sha1").update(normalizeCwd(root)).digest("hex").slice(0, 16)
43
46
  }
44
47
 
48
+ /** 键控库文件路径(根 → 库位)——**内部形**:根推导内部判「库在盘」时用它,**不得经 `ledgerDbPath`**
49
+ * (互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
50
+ const keyedDbFile = (root) => join(ledgerDir, `${ledgerKey(root)}.db`)
51
+
52
+ /** 「已注册」判据(设计档 §2.1——**唯一**事实,两臂取一即成立):
53
+ * 档臂 = 该目录带项目声明档 `PROJECT-MANIFEST.json`(档在盘);
54
+ * 库臂 = 以该目录自身路径为键的键控库在盘 ∧ 该目录**非工作区态**(键控库存在 ≠ 可作项目);
55
+ * 工作区判据只在库在盘时才查(`projectRootView` 含 fs 扫描——档臂已中即短路)。 */
56
+ function isRegistered(dir) {
57
+ if (existsSync(join(dir, MANIFEST_REL))) return true
58
+ if (!existsSync(keyedDbFile(dir))) return false
59
+ return projectRootView(dir).state !== "workspace"
60
+ }
61
+
62
+ /** 同级枚举上限(成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集)。
63
+ * 2026-10-16 批 ledger-root-derivation 自 `ledger.mjs` 随迁(根推导收敛序 ∥ 族发现同级枚举同源)。 */
64
+ export const MAX_SIBLING_SCAN = 100
65
+
66
+ /** 目录下**已注册**(档 ∨ 库——§2.1 单谓词)的直接子目录(目录名升序;不可读 ∥ 超限 ⇒ 空集;
67
+ * **惰性枚举 + 上限**——大目录零全量扫描)。消费面 = 根推导收敛序 ∥ 族发现 ∥ 轻通道闸候选枚举。
68
+ * `ledger` = 该子目录根的键控库路径(谓词含档臂 ⇒ 可能尚未在盘)。2026-10-16 批自 `ledger.mjs` 随迁。 */
69
+ export function ledgerChildren(dir) {
70
+ const out = []
71
+ let handle
72
+ try { handle = opendirSync(dir) } catch { return out }
73
+ try {
74
+ let n = 0
75
+ for (;;) {
76
+ const ent = handle.readSync()
77
+ if (!ent) break
78
+ if (++n > MAX_SIBLING_SCAN) return []
79
+ if (!ent.isDirectory()) continue
80
+ const root = join(dir, ent.name)
81
+ if (!isRegistered(root)) continue
82
+ out.push({ root, ledger: keyedDbFile(root) })
83
+ }
84
+ } catch { /* 读中断 → 已得子集(尽力而为) */ }
85
+ finally { try { handle.closeSync() } catch { /* 已关 */ } }
86
+ return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
87
+ }
88
+
89
+ /** 项目根推导**单源**(设计档 §2.1「根解析面」/ §3.8——读写同一判据、同一实现):
90
+ * 自 `target`(「动作目标路径」;缺省 = `.`)起沿**祖先链**向上(含自身)取**最近一个「已注册」目录**
91
+ * ⇒ `{ root, basis: 'ancestor' }`(自明——回执逐字零改)。
92
+ * 祖先链无命中 ∧ `descend: true` ⇒ **向下收敛**锚的直接子目录(`ledgerChildren` 同款惰性枚举 +
93
+ * 上限、名序升序):恰一 ⇒ 取之;≥2 ⇒ ① 先取**有键控库**者(账已在盘 = 事实)② 该集名序首者 ⇒
94
+ * `{ root, basis: 'descendant' }`(向下发现——**须报明**);零已注册子目录 ⇒ `null`(无根)。
95
+ * `descend: false` ⇒ 只问祖先支(族发现 ∥ 标记面——容器 ∥ 工作区锚 ⇒ `null` ⇒ 族求和)。
96
+ * **无自指**:内部「库在盘」判据只用 `ledgerKey` + `keyedDbFile`(不经 `ledgerDbPath`)。
97
+ * @param {string} [target] 动作目标路径(目录)
98
+ * @param {{descend?: boolean}} [opts] `descend` 缺省 `true`(动作面——`openLedger` / `ledgerDbPath` / 单范围读)
99
+ * @returns {{root: string, basis: 'ancestor'|'descendant'}|null} 项目根 + 判据来源 / 无根(`null`) */
100
+ export function resolveLedgerRoot(target, { descend = true } = {}) {
101
+ const start = resolve(target ?? ".")
102
+ let dir = start
103
+ for (;;) {
104
+ if (isRegistered(dir)) return { root: dir, basis: "ancestor" }
105
+ const parent = dirname(dir)
106
+ if (parent === dir) break // 盘根 → 祖先链无已注册者
107
+ dir = parent
108
+ }
109
+ if (!descend) return null
110
+ const children = ledgerChildren(start)
111
+ if (children.length === 0) return null
112
+ if (children.length === 1) return { root: children[0].root, basis: "descendant" }
113
+ const withDb = children.filter((c) => existsSync(c.ledger))
114
+ return { root: (withDb.length > 0 ? withDb : children)[0].root, basis: "descendant" }
115
+ }
116
+
117
+ /** 无根文案(单源——设计档 §3.8「无根契约」逐字;`openLedger` 写面 ∥ `ledgerDbPath` 共用同一句)。 */
118
+ const noRootMessage = (start) =>
119
+ `台账:自 ${start} 未能推导项目根——祖先链与本级子目录均无已注册项目(带 PROJECT-MANIFEST.json 或已有键控台账库)`
120
+
45
121
  /** 项目根 → 库路径(稳定键 = `ledgerKey` 前 16 位——路径唯一标识,不含分隔符)。
46
- * 项目根与 manifest 同一判据(归属 ∨ 发现——`resolveProjectRoot`;无 manifest ⇒ `resolve(cwd ?? ".")`
47
- * 兜底且**单值**)——与写门指针基准同一子表达式(键另加 `normalizeCwd` 一步,§2.1)。
122
+ * **归单源**(2026-10-16 批 ledger-root-derivation):根 = `resolveLedgerRoot(cwd)`——与读面 / 写面
123
+ * 同一推导;无根 ⇒ 抛**同一**无根文案(键需根,不产第二答案)。
48
124
  * v2 键归一(2026-09-25 批):盘符本大写(CLI)⇒ 键恒等零变;小写盘符(VSC `uri.fsPath`)
49
125
  * 并入同键 ⇒ 同项目两端同库(收正前 = 同项目两键两库)。 */
50
126
  export function ledgerDbPath(cwd) {
51
- const root = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
52
- return join(ledgerDir, `${ledgerKey(root)}.db`)
127
+ const start = resolve(cwd ?? ".")
128
+ const derived = resolveLedgerRoot(start)
129
+ if (!derived) throw new Error(noRootMessage(start))
130
+ return keyedDbFile(derived.root)
53
131
  }
54
132
 
55
133
  /** 未决四态(「活条目」口径 = 计数单源 WHERE 集;已核销 / 已废弃 = 归档态)。 */
@@ -135,25 +213,26 @@ function markSchemaVersion(db) {
135
213
  if (v < LEDGER_SCHEMA_VERSION) db.exec(`PRAGMA user_version = ${LEDGER_SCHEMA_VERSION}`)
136
214
  }
137
215
 
138
- /** 开库(cwd = 项目根——仅作关联键)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
139
- * 读面(create=false):库文件不存在 → 返回 null(空账——读面不建库、无副作用)。
140
- * 写面(create=true):库文件不存在自动建(台账目录随之创建);项目根目录不存在 → 抛友好错误。
216
+ /** 开库(cwd = 「动作目标路径」)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
217
+ * **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd)`——读写同一判据;嵌套取近者、
218
+ * 容器锚向下收敛。**无根(`null`)**:写面(`create:true`)抛(fail-loud、零落盘零建库);读面空账
219
+ * (`null`——与「库不在 = 空账」同形)。
220
+ * 读面(create=false):库文件不存在 → 返回 null(读面不建库、无副作用)。
221
+ * 写面(create=true):库文件不存在自动建(台账目录随之创建)。
141
222
  * **只读态**(readOnly=true——读面专用):只读句柄 + 零 DDL / ALTER + items 探针(见 openReadOnly);
142
223
  * 如实副作用:旧库(缺 executor 列)**读**不再自动补列——补列归写面开库;读出行缺列按 null 归一。
143
- * **歧义锚**(≥2 候选——`projectRootView` `ambiguous` 态)⇒ **显式拒**(列候选 + 显式项目根指引;
144
- * 零写——不静默回退锚;族锚「项目不可解析」,AC-M2-17);兜底 `resolve(cwd ?? ".")` 只属 none 态。 */
224
+ * 「目录不存在」的真错照抛(fail-loud——先于推导,文案逐字零改)。 */
145
225
  export function openLedger(cwd, { create = false, readOnly = false } = {}) {
146
- const dir = resolve(cwd ?? ".")
147
- // 歧义锚 ⇒ 显式拒(零写——消「缺省空读 / 错写落锚」双面症;读 / 写五工具同经本门)。
148
- const view = projectRootView(dir)
149
- if (view.state === "ambiguous") {
150
- const list = view.candidates.map((c) => `- ${c}`).join("\n")
151
- throw new Error(`项目不可解析:cwd ${dir} 下候选项目不是恰好一个(下列 ${view.candidates.length} 个)——台账按项目根键控,请显式给出项目根 cwd:\n${list}`)
226
+ const start = resolve(cwd ?? ".")
227
+ if (!existsSync(start)) {
228
+ throw new Error(`项目目录不存在:${start}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
152
229
  }
153
- const file = ledgerDbPath(dir)
154
- if (!existsSync(dir)) {
155
- throw new Error(`项目目录不存在:${dir}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
230
+ const derived = resolveLedgerRoot(start)
231
+ if (!derived) {
232
+ if (create) throw new Error(noRootMessage(start)) // 写面抛——零落盘零建库
233
+ return null // 读面空账(未注册目录下旧行为亦为空 ⇒ 零回归)
156
234
  }
235
+ const file = keyedDbFile(derived.root)
157
236
  if (!existsSync(file) && !create) return null
158
237
  if (readOnly) return openReadOnly(file)
159
238
  try { mkdirSync(ledgerDir, { recursive: true }) } catch { /* 已存在 / 并发建目录竞争——忽略 */ }
@@ -12,7 +12,7 @@ import { basename, dirname, join, resolve } from "node:path"
12
12
  import { DatabaseSync } from "node:sqlite"
13
13
 
14
14
  import { resolveDeclaredRef } from "./declaration.mjs"
15
- import { ledgerDirPath, ledgerKey, openLedger } from "./ledger-db.mjs"
15
+ import { ledgerDirPath, ledgerKey, openLedger, resolveLedgerRoot } from "./ledger-db.mjs"
16
16
  import { resolveProjectRoot } from "./manifest.mjs"
17
17
 
18
18
  /** 12 数据列(**等值比对列集**——不含 id:id 重发,§2.2 语义保全 / AC-M2-12)。 */
@@ -92,9 +92,9 @@ function fromKeyRefusal(fromKeys, dir) {
92
92
  return null
93
93
  }
94
94
 
95
- /** 迁移计划(dry-run / confirm 共用)。cwd 口径 = 写门口径 2 同一子表达式(`resolveProjectRoot` ∨ 兜底)。 */
95
+ /** 迁移计划(dry-run / confirm 共用)。cwd 口径 = 写门口径 2 同一子表达式(根推导单源 `resolveLedgerRoot(cwd)?.root` ∨ 兜底)——与实开库(`openLedger`)同源。 */
96
96
  export function planLedgerMigration(cwd, { fromKeys = [] } = {}) {
97
- const projectRoot = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
97
+ const projectRoot = resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
98
98
  const dir = ledgerDirPath()
99
99
  const targetKey = ledgerKey(projectRoot)
100
100
  const targetPath = join(dir, `${targetKey}.db`)
package/ledger-read.mjs CHANGED
@@ -10,19 +10,18 @@
10
10
  * (列集单源 = `DATA_COLUMNS`——D-10 直引,不另立副本;`evidence` 仅 full 入行);缺列(旧库)按
11
11
  * null 归一;`schemaVersion` = 库 `PRAGMA user_version` 如实回读(旧库未标 = 0);库不在 / 非台账库
12
12
  * ⇒ 项目不出项(空集)。
13
- * 范围(§2.4):单范围 = 解析后项目根一项(`resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")`——与库键
14
- * 同源);`--family` = `discoverFamily` 发现序逐项(不可读跳过——既有语义)。
13
+ * 范围(§2.4):单范围 = 推导所得项目根一项(`resolveLedgerRoot(cwd)`——与库键同源;无根 ⇒ 空集);
14
+ * `--family` = `discoverFamily` 发现序逐项(不可读跳过——既有语义)。
15
15
  *
16
16
  * W8 契约②:本档静态入 `ledger-db.mjs`(node:sqlite)⇒ 消费侧一律**动态 import** 本档。
17
17
  * D-11:`ledger.mjs` 零编辑——族发现只 import `discoverFamily`。
18
18
  */
19
- import { basename, resolve } from "node:path"
19
+ import { basename } from "node:path"
20
20
 
21
21
  import { ledgerQuery } from "./ledger-cmd.mjs"
22
22
  import { discoverFamily } from "./ledger.mjs"
23
23
  import { DATA_COLUMNS } from "./ledger-migrate.mjs"
24
- import { resolveProjectRoot } from "./manifest.mjs"
25
- import { openLedger } from "./ledger-db.mjs"
24
+ import { openLedger, resolveLedgerRoot } from "./ledger-db.mjs"
26
25
 
27
26
  /** 行出形(§3.2):`id` + 数据列序(`evidence` 仅 full);缺列按 null 归一。 */
28
27
  function exportRow(r, full) {
@@ -50,8 +49,8 @@ function readProject(root, full) {
50
49
  export function ledgerExport({ cwd, family = false, full = false } = {}) {
51
50
  const anchor = cwd ?? process.cwd()
52
51
  if (!family) {
53
- const root = resolveProjectRoot(anchor) ?? resolve(anchor)
54
- const item = readProject(root, full)
52
+ const derived = resolveLedgerRoot(anchor)
53
+ const item = derived ? readProject(derived.root, full) : null
55
54
  return { projects: item ? [item] : [] }
56
55
  }
57
56
  const projects = []
package/ledger-tools.mjs CHANGED
@@ -11,11 +11,20 @@
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
13
  import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } 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 字符截断加 `…`
@@ -80,7 +89,7 @@ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
80
89
  const READ_ACTIONS = ["query", "count"]
81
90
 
82
91
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
83
- const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
92
+ const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
84
93
 
85
94
  /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
86
95
  const ACTION_SPECS = {
@@ -207,25 +216,31 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
207
216
  switch (action) {
208
217
  case "add": {
209
218
  const { cwd, ...row } = rest
210
- const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
211
- return JSON.stringify({ id, status: "待讨论" })
219
+ const target = cwd ?? cwdOf(ctx)
220
+ const id = ledgerAdd({ cwd: target, row })
221
+ return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
212
222
  }
213
223
  case "update": {
214
224
  const { cwd, id, ...patch } = rest
225
+ const target = cwd ?? cwdOf(ctx)
215
226
  let executorSessionId
216
227
  if (patch.executor == null) {
217
228
  // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
218
229
  const { getSessionId } = await import("./session-slots.mjs")
219
230
  executorSessionId = getSessionId()
220
231
  }
221
- return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
232
+ return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId }), ...reportedRoot(target) })
233
+ }
234
+ case "close": {
235
+ const target = rest.cwd ?? cwdOf(ctx)
236
+ return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
222
237
  }
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
238
  case "query":
226
239
  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) }) })
240
+ case "count": {
241
+ const target = rest.cwd ?? cwdOf(ctx)
242
+ return JSON.stringify({ count: ledgerCount({ cwd: target }), ...reportedRoot(target) })
243
+ }
229
244
  default:
230
245
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
231
246
  throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
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] }
@@ -105,10 +68,16 @@ export function discoverFamily(anchor) {
105
68
  * agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
106
69
  * (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
107
70
  * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
71
+ * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor)` 推导所得项目根(非锚
72
+ * ——容器 ∥ 工作区锚取收敛序之根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`(不落兜底根——
73
+ * 该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 容器支只列已注册子目录))。
108
74
  * `inflightExecutors`(F-LX1 · sync 零变——K-LX2):在途且 executor 非空行集原样携带
109
75
  * {executor, updated_at, created_at}——判活解析由 `resolveExecutorStates` async 单源负责,本函数零探测。 */
110
76
  export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
111
- const rows = ledgerQuery({ cwd })
77
+ const anchor = resolve(cwd)
78
+ const derived = resolveLedgerRoot(anchor)
79
+ const root = derived ? derived.root : anchor
80
+ const rows = ledgerQuery({ cwd: anchor })
112
81
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
113
82
  const poolEntries = pending.filter((r) => r.kind === "requirement")
114
83
  const techEntries = pending.filter((r) => r.kind === "tech_todo")
@@ -123,12 +92,11 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
123
92
  if (key) boards.set(key, (boards.get(key) ?? 0) + 1)
124
93
  }
125
94
  const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
126
- const root = resolve(cwd)
127
95
  const inflightExecutors = pending
128
96
  .filter((r) => r.status === "在途" && r.executor)
129
97
  .map((r) => ({ executor: r.executor, updated_at: r.updated_at ?? null, created_at: r.created_at ?? null }))
130
98
  return {
131
- name: basename(root), root, ledger: ledgerDbPath(root),
99
+ name: basename(root), root, ledger: derived ? ledgerDbPath(root) : null,
132
100
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
133
101
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
134
102
  thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
@@ -178,8 +146,9 @@ export function detailScans(scans, current) {
178
146
  }
179
147
 
180
148
  // ── 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"
149
+ // ——2026-10-07 批 ledger-tool 自本档迁入,公共面零变;`ledgerChildren` / `MAX_SIBLING_SCAN` / `resolveLedgerRoot`
150
+ // ——2026-10-16 批 ledger-root-derivation 随迁 / 新增,公共面形零改) ──
151
+ export { AGING_DAYS, ALLOWED_MIGRATIONS, MAX_SIBLING_SCAN, ledgerChildren, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, resolveLedgerRoot, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
183
152
  export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
184
153
  export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
185
154
  export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.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 design-token-recovery 批并入):design-slots
22
+ * reveal 结果经 `tool:done` head 落盘时不落值(与 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)
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * manifest-discovery.mjs — **发现 ∕ 归属族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
- * 全量迁出〔原 `:39-143`〕——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
3
+ * 全量迁出——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
4
4
  * `scanChildren`(两条梯表共用 walk 内核)· `discoverProjects`(项目梯)· `discoverRepos`(仓梯)·
5
5
  * `owningProject`(归属形)· `resolveProjectRoot`(归属 ∨ 发现);**结构拆分零语义**(面不变 ∕ 判据不变,
6
6
  * 只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
@@ -41,13 +41,14 @@ function scanChildren(anchor, isHit) {
41
41
  /**
42
42
  * **项目发现**(项目梯五级——KD-M1-23 / 设计 `docs/core/design/MANIFEST.md` §2.2;**git 非前提**,
43
43
  * 2026-09-21 用户裁定):① 锚自身带 `MANIFEST_REL` ⇒ 锚;② 锚含 `.git` ⇒ 锚(缺档 = 建档机会);
44
- * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `ambiguous`);
45
- * ④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,落点 = 会话锚)。
44
+ * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `workspace`——下辖多项目
45
+ * = 工作区常态,非错误态);④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,
46
+ * 落点 = 会话锚)。
46
47
  * `candidates` 一律**按名排序**(`self` / `none` ⇒ `[]`);`matched` ∈ `manifest` / `git` / `null`
47
- * 记命中(或歧义)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
48
+ * 记命中(或工作区)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
48
49
  * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**(测试注入面——先于真判据,同 `discoverRepos`)。
49
50
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
50
- * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
51
+ * @returns {{kind:'self'|'unique'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
51
52
  */
52
53
  export function discoverProjects(cwd) {
53
54
  if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
@@ -55,10 +56,10 @@ export function discoverProjects(cwd) {
55
56
  if (existsSync(join(anchor, MANIFEST_REL))) return { kind: "self", root: anchor, candidates: [], matched: "manifest" }
56
57
  if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
57
58
  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: "workspace", root: null, candidates: withManifest, matched: "manifest" }
59
60
  if (withManifest.length === 1) return { kind: "unique", root: withManifest[0], candidates: [...withManifest], matched: "manifest" }
60
61
  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: "workspace", root: null, candidates: bareRepos, matched: "git" }
62
63
  if (bareRepos.length === 1) return { kind: "unique", root: bareRepos[0], candidates: [...bareRepos], matched: "git" }
63
64
  return { kind: "none", root: null, candidates: [], matched: null }
64
65
  }
@@ -66,21 +67,22 @@ export function discoverProjects(cwd) {
66
67
  /**
67
68
  * **仓发现**(仓梯——KD-M1-23 / `docs/core/design/TOOLS.md` §6.13;**单源** KD-M1-22——`git` 工具
68
69
  * 经此符号接线,禁第二份实现):① 锚含 `.git` ⇒ 锚;② 直接子目录中 `.git` **∧** 带档者恰一 ⇒ 命中
69
- * (≥2 ⇒ 歧义);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 / ≥2 ⇒ 歧义);④ 均无 ⇒ `none`。
70
+ * (≥2 ⇒ `workspace`——下辖多仓 = 常态);③ **零个此类时才看**含 `.git` 的裸仓(恰一 ⇒ 命中 /
71
+ * ≥2 ⇒ `workspace`);④ 均无 ⇒ `none`。
70
72
  * 与 `discoverProjects` = **同一 walk 内核**(`scanChildren`)的两种梯表——差异只在谓词与级序。
71
73
  * `candidates` 按名排序;`matched` 记档位;纯 fs / 不抛 / 不递归 / 不向上;覆盖位 ⇒ 头部短路。
72
74
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
73
- * @returns {{kind:'self'|'unique'|'none'|'ambiguous', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
75
+ * @returns {{kind:'self'|'unique'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
74
76
  */
75
77
  export function discoverRepos(cwd) {
76
78
  if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
77
79
  const anchor = resolve(cwd ?? ".")
78
80
  if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
79
81
  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" }
82
+ if (scoped.length > 1) return { kind: "workspace", root: null, candidates: scoped, matched: "manifest" }
81
83
  if (scoped.length === 1) return { kind: "unique", root: scoped[0], candidates: [...scoped], matched: "manifest" }
82
84
  const bare = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
83
- if (bare.length > 1) return { kind: "ambiguous", root: null, candidates: bare, matched: "git" }
85
+ if (bare.length > 1) return { kind: "workspace", root: null, candidates: bare, matched: "git" }
84
86
  if (bare.length === 1) return { kind: "unique", root: bare[0], candidates: [...bare], matched: "git" }
85
87
  return { kind: "none", root: null, candidates: [], matched: null }
86
88
  }
@@ -107,23 +109,24 @@ export function owningProject(target) {
107
109
  }
108
110
  }
109
111
 
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[]}} */
112
+ /** **项目根解析的直读形**(判定单点——设计 `MANIFEST.md` §2.2 `projectRootView` 契约行 / §2.5
113
+ * 「单项目解析律与四处会话级缺省收口」条):`ok`(唯一解析:归属命中 ∥ 发现 `self` / `unique`)/
114
+ * `workspace`(锚自身无档 ∧ 下辖 **≥2** 项目 ⇒ `root` null + 下辖项目全列**按名排序**——工作区
115
+ * 常态,非错误态)/ `none`(无项目 ⇒ 双空);覆盖位在场 ⇒ 头部短路(`ok` + 覆盖值)。
116
+ * **非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数(行为逐字零变)。
117
+ * @returns {{state:'ok'|'workspace'|'none', root:string|null, candidates:string[]}} */
115
118
  export function projectRootView(cwd) {
116
119
  const owning = owningProject(cwd)
117
120
  if (owning) return { state: "ok", root: owning, candidates: [] }
118
121
  const d = discoverProjects(cwd)
119
122
  if (d.root) return { state: "ok", root: d.root, candidates: [] }
120
- if (d.kind === "ambiguous") return { state: "ambiguous", root: null, candidates: d.candidates }
123
+ if (d.kind === "workspace") return { state: "workspace", root: null, candidates: d.candidates }
121
124
  return { state: "none", root: null, candidates: [] }
122
125
  }
123
126
 
124
127
  /** 项目根解析(**归属 ∨ 发现**——KD-M1-30 / M1-24):**薄委托** `projectRootView(cwd).root`(判定
125
128
  * 单点见上——#828)。带档路径与批前**逐字同**;变更面两条(设计 §2.2 同条):① 项目树内路径
126
- * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `ambiguous` ⇒
129
+ * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `workspace` ⇒
127
130
  * `null`。调用方零改(行为随语义变更——设计 §2.5 键面条 / 错层条)。
128
131
  * @returns {string|null} 项目根绝对路径 / null */
129
132
  export function resolveProjectRoot(cwd) {