@thincoder/core 0.10.11 → 0.10.13

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 (55) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/advisor/messages.mjs +1 -1
  3. package/advisor/project-context.mjs +2 -2
  4. package/agent/run-stages.mjs +33 -29
  5. package/agent/setup.mjs +3 -0
  6. package/agent/turn-loop.mjs +3 -0
  7. package/agent-tools/batch-gate.mjs +65 -49
  8. package/agent-tools/batch-lifecycle.mjs +82 -12
  9. package/agent-tools/batch-paths.mjs +10 -8
  10. package/agent-tools/batch-skeleton.mjs +9 -1
  11. package/agent-tools/batch-unit-test-gate.mjs +107 -0
  12. package/agent-tools/design-slots.mjs +72 -12
  13. package/agent-tools/design-token.mjs +18 -2
  14. package/agent-tools/manifest.mjs +1 -1
  15. package/agent-tools/review-facts.mjs +1 -1
  16. package/agent.mjs +1 -0
  17. package/client-identity.mjs +54 -0
  18. package/config.mjs +19 -13
  19. package/context.mjs +5 -5
  20. package/doc-check/index.mjs +28 -15
  21. package/doc-check/paragraph.mjs +6 -3
  22. package/embedding.mjs +2 -0
  23. package/generate-title.mjs +3 -2
  24. package/ledger-close-proof.mjs +80 -0
  25. package/ledger-cmd.mjs +64 -53
  26. package/ledger-db.mjs +60 -59
  27. package/ledger-tools.mjs +60 -23
  28. package/ledger.mjs +12 -10
  29. package/manifest-discovery.mjs +41 -26
  30. package/manifest.mjs +17 -16
  31. package/model-specs.mjs +17 -18
  32. package/package.json +1 -1
  33. package/prompts/advisor-design.md +4 -3
  34. package/prompts/common.md +11 -1
  35. package/prompts/discipline-engineering.md +16 -7
  36. package/prompts/discipline-normal.md +2 -1
  37. package/prompts/persona-coder.md +8 -1
  38. package/prompts/persona-eng-coder.md +8 -0
  39. package/prompts/persona-eng-designer.md +11 -1
  40. package/prompts/persona-engineering.md +23 -11
  41. package/prompts/persona-explore.md +6 -0
  42. package/prompts/persona-normal.md +6 -0
  43. package/prompts/persona-plan.md +6 -0
  44. package/provider/anthropic.mjs +12 -7
  45. package/provider/core.mjs +19 -3
  46. package/provider/errors.mjs +41 -0
  47. package/provider/google.mjs +10 -4
  48. package/provider/list-models.mjs +4 -3
  49. package/provider/responses.mjs +17 -7
  50. package/session-lifecycle.mjs +3 -0
  51. package/session-slot-write.mjs +21 -1
  52. package/session-slots-manifest.mjs +21 -4
  53. package/token-ttl.mjs +68 -4
  54. package/token-window.mjs +23 -2
  55. package/tool-docs/wait_for.md +15 -6
package/ledger-db.mjs CHANGED
@@ -6,8 +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):读写同一判据(档在盘 ∨ 以目录自身路径为键的库在盘)。
9
+ * 项目根推导单源(`resolveLedgerRoot` = 判据本体 `owningProject` 的 `basis` 形状包装)+ 同级枚举
10
+ * (`ledgerChildren`)居本档——键 / 库位 / 根同源(设计档 `LEDGER.md` §2.1「根解析面」/ §3.8):
11
+ * 读写同一判据(档在盘——项目 = 带 manifest 的目录;库在盘不算项目,台账无第二定义)。
11
12
  *
12
13
  * 落点(2026-09-17 用户裁定收正):台账库**不在项目目录**——住用户数据目录
13
14
  * `~/.thincoder/ledger/<sha1(normalizeCwd(项目根绝对路径))[:16]>.db`(工作树外、天然不进 git;
@@ -17,11 +18,11 @@
17
18
  */
18
19
  import { createHash } from "node:crypto"
19
20
  import { existsSync, mkdirSync, opendirSync } from "node:fs"
20
- import { dirname, join, resolve } from "node:path"
21
+ import { join, resolve } from "node:path"
21
22
  import { DatabaseSync } from "node:sqlite"
22
23
 
23
24
  import { configDir } from "./config-io.mjs"
24
- import { MANIFEST_REL, projectRootView } from "./manifest.mjs"
25
+ import { MANIFEST_REL, owningProject, projectRootView } from "./manifest.mjs"
25
26
  // 归一步单源(KD-LN1——直引,不抽新 util:先例 = peer-domains.mjs / traces/trace-store.mjs)。
26
27
  import { normalizeCwd } from "./session-slots.mjs"
27
28
 
@@ -45,27 +46,24 @@ export function ledgerKey(root) {
45
46
  return createHash("sha1").update(normalizeCwd(root)).digest("hex").slice(0, 16)
46
47
  }
47
48
 
48
- /** 键控库文件路径(根 → 库位)——**内部形**:根推导内部判「库在盘」时用它,**不得经 `ledgerDbPath`**
49
- * (互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
49
+ /** 键控库文件路径(根 → 库位)——**内部形**:族枚举 / 库路径解析内部构造库位时用它,**不得经
50
+ * `ledgerDbPath`**(互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
50
51
  const keyedDbFile = (root) => join(ledgerDir, `${ledgerKey(root)}.db`)
51
52
 
52
- /** 「已注册」判据(设计档 §2.1——**唯一**事实,两臂取一即成立):
53
- * 档臂 = 该目录带项目声明档 `PROJECT-MANIFEST.json`(档在盘);
54
- * 库臂 = 以该目录自身路径为键的键控库在盘 ∧ 该目录**非工作区态**(键控库存在 ≠ 可作项目);
55
- * 工作区判据只在库在盘时才查(`projectRootView` 含 fs 扫描——档臂已中即短路)。 */
53
+ /** 「已注册」判据(**唯一**事实):该目录带项目声明档 `PROJECT-MANIFEST.json`。
54
+ * 项目 = 带 manifest 的目录(`docs/core/requirements/ENGINEERING-MODE-V2-SPEC-MANIFEST.md:68`)——
55
+ * 台账没有自己的「项目」定义:台账根 = 项目根,一个判据。 */
56
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"
57
+ return existsSync(join(dir, MANIFEST_REL))
60
58
  }
61
59
 
62
60
  /** 同级枚举上限(成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集)。
63
- * 2026-10-16 批 ledger-root-derivation 自 `ledger.mjs` 随迁(根推导收敛序 ∥ 族发现同级枚举同源)。 */
61
+ * 2026-10-16 批 ledger-root-derivation 自 `ledger.mjs` 随迁(同级枚举:族发现 ∥ 轻通道闸候选枚举同源)。 */
64
62
  export const MAX_SIBLING_SCAN = 100
65
63
 
66
- /** 目录下**已注册**(档 ∨ 库——§2.1 单谓词)的直接子目录(目录名升序;不可读 ∥ 超限 ⇒ 空集;
67
- * **惰性枚举 + 上限**——大目录零全量扫描)。消费面 = 根推导收敛序 ∥ 族发现 ∥ 轻通道闸候选枚举。
68
- * `ledger` = 该子目录根的键控库路径(谓词含档臂 ⇒ 可能尚未在盘)。2026-10-16 批自 `ledger.mjs` 随迁。 */
64
+ /** 目录下**已注册**(档在盘——§2.1 单谓词)的直接子目录(目录名升序;不可读 ∥ 超限 ⇒ 空集;
65
+ * **惰性枚举 + 上限**——大目录零全量扫描)。消费面 = 族发现 ∥ 轻通道闸候选枚举。
66
+ * `ledger` = 该子目录根的键控库路径(推算值 ⇒ 可能尚未在盘)。2026-10-16 批自 `ledger.mjs` 随迁。 */
69
67
  export function ledgerChildren(dir) {
70
68
  const out = []
71
69
  let handle
@@ -86,48 +84,49 @@ export function ledgerChildren(dir) {
86
84
  return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
87
85
  }
88
86
 
89
- /** 项目根推导**单源**(设计档 §2.1「根解析面」/ §3.8——读写同一判据、同一实现):
90
- * 自 `target` 沿**祖先链**上溯取最近「已注册」目录——**以会话锚(`process.cwd()`)为界,越界即停**:
91
- * 界内上溯 = 子目录归到自己的项目根(应该);越过锚 = 挂到无关外层项目上(禁止)。
92
- * 命中 ⇒ `{ root, basis: 'self' | 'ancestor' }`(自身 vs 界内祖先);
93
- * 界内无命中 ∧ `descend: true` ⇒ **向下收敛**锚的直接子目录(`ledgerChildren` 同款惰性枚举 +
94
- * 上限、名序升序):恰一 ⇒ 取之;≥2 ⇒ ① 先取**有键控库**者(账已在盘 = 事实)② 该集名序首者 ⇒
95
- * `{ root, basis: 'descendant' }`(向下发现——**须报明**);零已注册子目录 ⇒ `null`(无根)。
96
- * `descend: false` ⇒ 只问界内上溯支(族发现 ∥ 标记面——容器 ∥ 工作区锚 ⇒ `null` ⇒ 族求和)。
97
- * **无自指**:内部「库在盘」判据只用 `ledgerKey` + `keyedDbFile`(不经 `ledgerDbPath`)。
98
- * @param {string} [target] 动作目标路径(目录)
99
- * @param {{descend?: boolean}} [opts] `descend` 缺省 `true`(动作面——`openLedger` / `ledgerDbPath` / 单范围读)
100
- * @returns {{root: string, basis: 'self'|'ancestor'|'descendant'}|null} 项目根 + 判据来源 / 无根(`null`) */
101
- export function resolveLedgerRoot(target, { descend = true } = {}) {
87
+ /** 项目根推导**单源**(设计档 §2.1「根解析面」/ §3.8——读写同一判据、同一实现):**判据本体的形状包装**
88
+ * = `owningProject(target, boundary)`(祖先链上溯的唯一实现——本档零自持上溯)+ `basis`。
89
+ * 起点 = `target`(动作目标路径:目录 = 自身,文件 = 其所在目录);界 = **会话锚**(越界即停——
90
+ * 不越出工作目录去挂外层项目,也不向下找、不猜);`basis` = `self`(目标自身即带档目录)∥
91
+ * `ancestor`(最近带档祖先)。「项目根报明 / 建档」由用点决定。
92
+ * **无自指**:本档不判库在盘(台账无第二定义——库在盘不算项目)。
93
+ * @param {string} [target] 动作目标路径(目录 | 文件则取其所在目录)
94
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`)
95
+ * @returns {{root: string, basis: 'self'|'ancestor'}|null} 项目根 + 判据来源 / 无根(`null`) */
96
+ export function resolveLedgerRoot(target, boundary) {
102
97
  const start = resolve(target ?? ".")
103
- let dir = start
104
- for (;;) {
105
- if (isRegistered(dir)) return { root: dir, basis: dir === start ? "self" : "ancestor" }
106
- const parent = dirname(dir)
107
- if (parent === dir) break
108
- dir = parent
109
- }
110
- if (!descend) return null
111
- const children = ledgerChildren(start)
112
- if (children.length === 0) return null
113
- if (children.length === 1) return { root: children[0].root, basis: "descendant" }
114
- const withDb = children.filter((c) => existsSync(c.ledger))
115
- return { root: (withDb.length > 0 ? withDb : children)[0].root, basis: "descendant" }
98
+ const edge = resolve(boundary ?? process.cwd())
99
+ const root = owningProject(start, edge)
100
+ return root ? { root, basis: root === start ? "self" : "ancestor" } : null
116
101
  }
117
102
 
118
- /** 无根文案(单源——设计档 §3.8「无根契约」逐字;`openLedger` 写面 ∥ `ledgerDbPath` 共用同一句)。 */
119
- const noRootMessage = (start) =>
120
- `台账:自 ${start} 未能推导项目根——该目录与本级子目录均无已注册项目(带 PROJECT-MANIFEST.json 或已有键控台账库)`
103
+ /** 无根文案(单源——`openLedger` 写面 ∥ `ledgerDbPath` ∥ 工具面读面(`ledger-tools.mjs`)共用同一句):
104
+ * **只陈述事实**——起点 + 上溯上界(界内无项目根),不写「祖先链无已注册项目」这类越界断言:
105
+ * 归属根可能就在界**之上**(界外)——那一格该句为假,读它会教人误判层级。
106
+ * 锚态 = 工作区时追报下辖候选(报明面读数 = `projectRootView` 的 `workspace` 态——
107
+ * **只供报明,不参与取根**:取根仍以会话锚为界)。
108
+ * @param {string} start 起点(动作目标路径)
109
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`)——与 `resolveLedgerRoot` 同一缺省 */
110
+ export const noRootMessage = (start, boundary) => {
111
+ const view = projectRootView(start)
112
+ const edge = resolve(boundary ?? process.cwd())
113
+ const tail = view.state === "workspace" ? `;下辖候选项目:${view.candidates.join(" ∥ ")}` : ""
114
+ return `台账:自 ${start} 未找到项目根(向上找到 ${edge} 为止)${tail}`
115
+ }
121
116
 
122
117
  /** 项目根 → 库路径(稳定键 = `ledgerKey` 前 16 位——路径唯一标识,不含分隔符)。
123
- * **归单源**(2026-10-16 批 ledger-root-derivation):根 = `resolveLedgerRoot(cwd)`——与读面 / 写面
118
+ * **归单源**(2026-10-16 批 ledger-root-derivation):根 = `resolveLedgerRoot(cwd, boundary)`——与读面 / 写面
124
119
  * 同一推导;无根 ⇒ 抛**同一**无根文案(键需根,不产第二答案)。
120
+ * `boundary` = 上溯上界(**会话锚**——设计档 §3.8「界(单源)」):调用方知道锚就传;
121
+ * 缺省 = `process.cwd()`(CLI / 进程锚 = 会话锚的调用面不必传)。
125
122
  * v2 键归一(2026-09-25 批):盘符本大写(CLI)⇒ 键恒等零变;小写盘符(VSC `uri.fsPath`)
126
- * 并入同键 ⇒ 同项目两端同库(收正前 = 同项目两键两库)。 */
127
- export function ledgerDbPath(cwd) {
123
+ * 并入同键 ⇒ 同项目两端同库(收正前 = 同项目两键两库)。
124
+ * @param {string} [cwd] 动作目标路径
125
+ * @param {string} [boundary] 上溯上界(会话锚;缺省 = `process.cwd()`) */
126
+ export function ledgerDbPath(cwd, boundary) {
128
127
  const start = resolve(cwd ?? ".")
129
- const derived = resolveLedgerRoot(start)
130
- if (!derived) throw new Error(noRootMessage(start))
128
+ const derived = resolveLedgerRoot(start, boundary)
129
+ if (!derived) throw new Error(noRootMessage(start, boundary))
131
130
  return keyedDbFile(derived.root)
132
131
  }
133
132
 
@@ -230,24 +229,26 @@ function markSchemaVersion(db) {
230
229
  if (v < LEDGER_SCHEMA_VERSION) db.exec(`PRAGMA user_version = ${LEDGER_SCHEMA_VERSION}`)
231
230
  }
232
231
 
233
- /** 开库(cwd = 「动作目标路径」)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
234
- * **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd)`——读写同一判据;嵌套取近者、
235
- * 容器锚向下收敛。**无根(`null`)**:写面(`create:true`)抛(fail-loud、零落盘零建库);读面空账
232
+ /** 开库(cwd = 「动作目标路径」;`boundary` = 上溯上界 / 会话锚)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
233
+ * **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd, boundary)`——读写同一判据;嵌套取近者、
234
+ * 容器锚不向下取根(无根 ⇒ 报明)。**无根(`null`)**:写面(`create:true`)抛(fail-loud、零落盘零建库);读面空账
236
235
  * (`null`——与「库不在 = 空账」同形)。
237
236
  * 读面(create=false):库文件不存在 → 返回 null(读面不建库、无副作用)。
238
237
  * 写面(create=true):库文件不存在自动建(台账目录随之创建)。
239
238
  * **只读态**(readOnly=true——读面专用):只读句柄 + 零 DDL / ALTER + items 探针(见 openReadOnly);
240
239
  * 如实副作用:旧库(缺 executor 列)**读**不再自动补列——补列归写面开库;读出行缺列按 null 归一。
241
- * 「目录不存在」的真错照抛(fail-loud——先于推导,文案逐字零改)。 */
242
- export function openLedger(cwd, { create = false, readOnly = false } = {}) {
240
+ * 「目录不存在」的真错照抛(fail-loud——先于推导,文案逐字零改)。
241
+ * `boundary` = 上溯上界(**会话锚**——设计档 §3.8「界(单源)」):工具面传 `ctx.agent.cwd`;
242
+ * 缺省 = `process.cwd()`(CLI / 进程锚 = 会话锚的调用面不必传)。 */
243
+ export function openLedger(cwd, { create = false, readOnly = false, boundary } = {}) {
243
244
  const start = resolve(cwd ?? ".")
244
245
  if (!existsSync(start)) {
245
246
  throw new Error(`项目目录不存在:${start}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
246
247
  }
247
- const derived = resolveLedgerRoot(start)
248
+ const derived = resolveLedgerRoot(start, boundary)
248
249
  if (!derived) {
249
- if (create) throw new Error(noRootMessage(start)) // 写面抛——零落盘零建库
250
- return null // 读面空账(未注册目录下旧行为亦为空 ⇒ 零回归)
250
+ if (create) throw new Error(noRootMessage(start, boundary)) // 写面抛——零落盘零建库
251
+ return null // 读面空账(两类同形:未注册目录 ∥ 目标落在界外的已注册项目——后者见 §3.8「界」;读面不抛)
251
252
  }
252
253
  const file = keyedDbFile(derived.root)
253
254
  if (!existsSync(file) && !create) return null
package/ledger-tools.mjs CHANGED
@@ -10,19 +10,50 @@
10
10
  * 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
11
11
  * 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
12
12
  */
13
+ import { existsSync } from "node:fs"
14
+ import { isAbsolute, dirname, join, resolve } from "node:path"
13
15
  import { ledgerAdd, ledgerClose, ledgerCloseBatch, ledgerCount, ledgerQuery, ledgerUpdate, ledgerUpdateBatch } from "./ledger-cmd.mjs"
14
- import { resolveLedgerRoot } from "./ledger-db.mjs"
16
+ import { noRootMessage, resolveLedgerRoot } from "./ledger-db.mjs"
15
17
  import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
16
18
 
17
- /** 工具 cwd 供值面(缺省 → 当前会话锚——「动作目标路径」;项目根由核 `resolveLedgerRoot` 推导)。 */
18
- const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
19
+ /** 指针值 → 文件路径(剥 `§N` 段号;空值跳过)。 */
20
+ const ptrPath = (v) => {
21
+ if (typeof v !== "string") return null
22
+ const p = v.split("§")[0].trim()
23
+ return p || null
24
+ }
25
+
26
+ /** 会话锚供值面(**单源**——`cwdOf` 的缺省起点与上溯上界(`boundary`)同一表达式):
27
+ * `ctx.agent.cwd` > `ctx.cwd` > 进程 cwd。界 = 会话锚(设计档 §3.8「界(单源)· 两面两界」)——
28
+ * 工具面传给 `resolveLedgerRoot` / `openLedger` 的上界恒取本值,不取 `process.cwd()`(desktop / VSC
29
+ * 宿主下两者不同:会话锚才是「不能跑到工作目录以外去」的那道界)。 */
30
+ const anchorOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
19
31
 
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
32
+ /** 工具 cwd 供值面:**起点 = 动作目标路径**(`docs/core/requirements/ENGINEERING-MODE-V2-SPEC-MANIFEST.md:72`
33
+ * 「参数按用点解析——动作作用于哪个项目的路径,就取那个项目的档」)。
34
+ * 指针(`task_book` ∥ `req_doc` ∥ `batch`)在场 ⇒ 取该文件所在目录为起点(文件 = 其所在目录);
35
+ * 无指针的动作(`query` ∥ `count` ∥ 首次建行)才回退会话锚。项目根由核 `resolveLedgerRoot` 推导。 */
36
+ const cwdOf = (ctx, ...ptrs) => {
37
+ for (const raw of ptrs) {
38
+ const p = ptrPath(raw)
39
+ if (!p) continue
40
+ return dirname(isAbsolute(p) ? p : join(anchorOf(ctx), p))
41
+ }
42
+ return anchorOf(ctx)
43
+ }
44
+
45
+ /** 起点归一(**单源**——显式 `cwd` 与指针同基底):显式 `cwd` 在场 ⇒ 相对路径按**会话锚**解析、
46
+ * 绝对路径照用;不在场 ⇒ 按指针 / 会话锚推导(`cwdOf`)。两径同一基底,避免「界 = 会话锚、起点按进程 cwd」
47
+ * 的自相矛盾(锚 ≠ 进程目录的宿主下,相对 `cwd` 否则恒落界外 ⇒ 一律报无根)。 */
48
+ const startOf = (ctx, anchor, explicit, ...ptrs) => (explicit != null ? resolve(anchor, explicit) : cwdOf(ctx, ...ptrs))
49
+
50
+ /** 起点推导无根 ⇒ 抛(设计档 §3.8「无根契约」· **工具面**:空集与「记到别的项目」在回执上无法区分——
51
+ * 静默空集不成立)。判据 = 同一单源 `resolveLedgerRoot`(无第二套上溯);目录不存在不在此判
52
+ * (交 `openLedger` 的既有文案——两判据不混)。展示面(`buildScan` ∥ 族发现)不经本函数:
53
+ * 无根 ⇒ `ledger: null` + 零行,不抛。`boundary` = 会话锚(界与 `openLedger` 同一枚)。 */
54
+ function assertLedgerStart(start, boundary) {
55
+ if (!existsSync(start)) return
56
+ if (!resolveLedgerRoot(start, boundary)) throw new Error(noRootMessage(start, boundary))
26
57
  }
27
58
 
28
59
  // ── 工具层参数守卫(设计档 §3.2 · 2026-09-27 快车道修复 · Gitee #IKIQGK / 台账 #472) ──
@@ -105,7 +136,7 @@ const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
105
136
  const READ_ACTIONS = ["query", "count"]
106
137
 
107
138
  /** cwd 参数描述(五前身声明同文——文本单点)。 */
108
- const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链最近的项目目录(带 PROJECT-MANIFEST.json 或已有台账库);容器目录下收敛到子项目。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话锚"
139
+ const CWD_DESC = "本次动作的目标路径——项目根由它推导:沿祖先链取最近的项目目录(带 PROJECT-MANIFEST.json 的那一层;库在盘不算项目)。多根工作区 / 无项目目录 ⇒ 报明并给候选,不向下取子项目当根。台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前会话锚解析;项目根按上溯界取——界外不挂项目。缺省 = 当前会话锚"
109
140
 
110
141
  /** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。
111
142
  * `parameters` 内的键 = 交模型 / provider 的那一份声明;**组判据两键(`oneOfRequired` / `whenPresent`)
@@ -255,16 +286,18 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
255
286
  const spec = ACTION_SPECS[action]
256
287
  const rest = assertToolArgs({ name: `ledger(${action})`, ...spec }, rest0)
257
288
  // ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
289
+ // 界 = 会话锚(`anchorOf` 单源)——逐 action 传 `boundary`:`openLedger` / `resolveLedgerRoot` 的上溯上界
290
+ const anchor = anchorOf(ctx)
258
291
  switch (action) {
259
292
  case "add": {
260
293
  const { cwd, ...row } = rest
261
- const target = cwd ?? cwdOf(ctx)
262
- const id = ledgerAdd({ cwd: target, row })
263
- return JSON.stringify({ id, status: "待讨论", ...reportedRoot(target) })
294
+ const target = startOf(ctx, anchor, cwd, row.task_book, row.req_doc)
295
+ const id = ledgerAdd({ cwd: target, row, boundary: anchor })
296
+ return JSON.stringify({ id, status: "待讨论" })
264
297
  }
265
298
  case "update": {
266
299
  const { cwd, id, batch, ...patch } = rest
267
- const target = cwd ?? cwdOf(ctx)
300
+ const target = startOf(ctx, anchor, cwd, batch, patch.task_book, patch.req_doc)
268
301
  let executorSessionId
269
302
  if (patch.executor == null) {
270
303
  // 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
@@ -272,20 +305,24 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
272
305
  executorSessionId = getSessionId()
273
306
  }
274
307
  // 批级批量(§3.3)——`batch` 与 `id` 恰携其一(守卫 P7 已先裁)
275
- if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId }), ...reportedRoot(target) })
276
- return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId }), ...reportedRoot(target) })
308
+ if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId, boundary: anchor, agent: ctx?.agent }) })
309
+ return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId, boundary: anchor, agent: ctx?.agent }) })
277
310
  }
278
311
  case "close": {
279
- const target = rest.cwd ?? cwdOf(ctx)
312
+ const target = startOf(ctx, anchor, rest.cwd, rest.batch, rest.task_book)
280
313
  // 批级批量结算(§3.3——勾销径)
281
- if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
282
- return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace }), ...reportedRoot(target) })
314
+ if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor, agent: ctx?.agent }) })
315
+ return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor, agent: ctx?.agent }) })
316
+ }
317
+ case "query": {
318
+ const target = startOf(ctx, anchor, rest.cwd, rest.task_book)
319
+ assertLedgerStart(target, anchor)
320
+ return JSON.stringify(ledgerQuery({ cwd: target, boundary: anchor, status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger, task_book: rest.task_book }), null, 2)
283
321
  }
284
- case "query":
285
- return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board, trigger: rest.trigger, task_book: rest.task_book }), null, 2)
286
322
  case "count": {
287
- const target = rest.cwd ?? cwdOf(ctx)
288
- return JSON.stringify({ ...ledgerCount({ cwd: target, task_book: rest.task_book }), ...reportedRoot(target) })
323
+ const target = startOf(ctx, anchor, rest.cwd, rest.task_book)
324
+ assertLedgerStart(target, anchor)
325
+ return JSON.stringify({ ...ledgerCount({ cwd: target, boundary: anchor, task_book: rest.task_book }) })
289
326
  }
290
327
  default:
291
328
  // 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
package/ledger.mjs CHANGED
@@ -44,11 +44,13 @@ export function entryTitle(text) {
44
44
  }
45
45
 
46
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)。 */
47
+ * 薄委托 `resolveLedgerRoot(anchor, anchor)`(设计档 §3.8——上溯以会话锚为界):锚即本面的会话锚 ⇒
48
+ * 界 = 锚自身(不越界探外层项目,与工具面同判);`root` = 推导所得项目根(非锚);
49
+ * 多根工作区锚 ⇒ `null` ⇒ 族发现按工作区根形(族求和——§7.2 / AC-M2-18 / AC-M2-19)。
50
+ * `ledgerDbPath` 同一界(同一笔读数不能两界)。 */
49
51
  export function findProject(anchor) {
50
- const derived = resolveLedgerRoot(anchor, { descend: false })
51
- return derived ? { root: derived.root, ledger: ledgerDbPath(derived.root) } : null
52
+ const derived = resolveLedgerRoot(anchor, anchor)
53
+ return derived ? { root: derived.root, ledger: ledgerDbPath(derived.root, derived.root) } : null
52
54
  }
53
55
 
54
56
  /** 项目族发现:{current, projects}——current = `findProject`(**祖先支**:容器 ∥ 工作区锚 ⇒ `null` ⇒ 按
@@ -70,16 +72,16 @@ export function discoverFamily(anchor) {
70
72
  * `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。
71
73
  * **`days` 只作用于展示面 `aged`**(本函数局部阈值);`overdue`(出口态)恒按核常量 `AGING_DAYS`
72
74
  * (`overdueOf` 单源)——故 `days` 调小时 `aged` 会先于 `overdue` 涨(两数口径不同,非矛盾)。
73
- * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor)` 推导所得项目根(非锚
74
- * ——容器 ∥ 工作区锚取收敛序之根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`(不落兜底根——
75
- * 该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 容器支只列已注册子目录))。
75
+ * **根推导**(设计档 §3.8):`root` / `name` / `ledger` = `resolveLedgerRoot(anchor, anchor)` 推导所得项目根
76
+ * (界 = 本面会话锚 ⇒ 命中即根 = 锚自身;多根工作区锚 ⇒ 无根)——与行集读取同一单源;无根 ⇒ `ledger` = `null`
77
+ * (不落兜底根——该态下 `root` / `name` 回落锚、行集为空,且该 scan 不进族枚举(`discoverFamily` 只列已注册子目录))。
76
78
  * `inflightExecutors`(F-LX1 · sync 零变——K-LX2):在途且 executor 非空行集原样携带
77
79
  * {executor, updated_at, created_at}——判活解析由 `resolveExecutorStates` async 单源负责,本函数零探测。 */
78
80
  export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
79
81
  const anchor = resolve(cwd)
80
- const derived = resolveLedgerRoot(anchor)
82
+ const derived = resolveLedgerRoot(anchor, anchor) // 界 = 会话锚(本面锚即会话锚——与工具面同判)
81
83
  const root = derived ? derived.root : anchor
82
- const rows = ledgerQuery({ cwd: anchor, now }) // `now` 透传(两读数同时钟——`overdue` 的行键由核同一 `now` 算)
84
+ const rows = ledgerQuery({ cwd: anchor, boundary: anchor, now }) // `now` 透传(两读数同时钟——`overdue` 的行键由核同一 `now` 算)
83
85
  const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
84
86
  const poolEntries = pending.filter((r) => r.kind === "requirement")
85
87
  const techEntries = pending.filter((r) => r.kind === "tech_todo")
@@ -98,7 +100,7 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
98
100
  .filter((r) => r.status === "在途" && r.executor)
99
101
  .map((r) => ({ executor: r.executor, updated_at: r.updated_at ?? null, created_at: r.created_at ?? null }))
100
102
  return {
101
- name: basename(root), root, ledger: derived ? ledgerDbPath(root) : null,
103
+ name: basename(root), root, ledger: derived ? ledgerDbPath(root, root) : null,
102
104
  pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
103
105
  overdue: pending.filter((r) => r.overdue != null).length, // 待处置(§3.7——机判单源 = 行集 `overdue` 键,行值与 `now` 均同源)
104
106
  agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
@@ -2,15 +2,18 @@
2
2
  * manifest-discovery.mjs — **发现 ∕ 归属族**出档(2026-09-29 structure-split-2 · 台账 #620):自 `manifest.mjs`
3
3
  * 全量迁出——`MANIFEST_REL` · 测试注入位(`_setProjectRootForTest` / `_resetProjectRootForTest`)·
4
4
  * `scanChildren`(两条梯表共用 walk 内核)· `discoverProjects`(项目梯)· `discoverRepos`(仓梯)·
5
- * `owningProject`(归属形)· `resolveProjectRoot`(归属 ∨ 发现);**结构拆分零语义**(面不变 ∕ 判据不变,
6
- * 只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
5
+ * `owningProject`(归属形 = 判据本体)· `projectRootView`(三态报明)· `resolveProjectRoot`(归属形薄委托);
6
+ * **结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-C)。
7
+ * 拆分后判据变更(2026-10-25「判据本体单源 + 不向下取根」):`projectRootView` 取根臂收为**仅归属命中**
8
+ * (界 = 目标所在盘根),`discoverProjects` 的 `self` 只供报明、不进 `root`(项目梯无 `unique`——≥1 即
9
+ * `workspace`,1 ∥ 2 ∥ 更多同判;仓梯 `discoverRepos` 的 `unique` 保留)。
7
10
  *
8
- * 缝 = 同名再出口:宿主 `manifest.mjs` 转口本档七名既有导出(消费者 import 面零改);宿主自用四名
11
+ * 缝 = 同名再出口:宿主 `manifest.mjs` 转口本档八名导出(消费者 import 面零改);宿主自用四名
9
12
  * (`MANIFEST_REL` ∥ `discoverProjects` ∥ `owningProject` ∥ `resolveProjectRoot`)随 import。
10
13
  * 零环:本档不引宿主(方向单行);档外依赖 = `node:fs` ∥ `node:path`;机制详述 = `docs/core/design/MANIFEST.md` §2.2 / §2.9。
11
14
  */
12
15
  import { existsSync, readdirSync, statSync } from "node:fs"
13
- import { dirname, join, resolve } from "node:path"
16
+ import { dirname, join, parse, resolve } from "node:path"
14
17
 
15
18
  /** 数据档文件名(**每个项目一份**——项目根 = 带档目录;**git 非前提**。2026-09-17 / 2026-09-21 用户裁定)。 */
16
19
  export const MANIFEST_REL = "PROJECT-MANIFEST.json"
@@ -41,14 +44,14 @@ function scanChildren(anchor, isHit) {
41
44
  /**
42
45
  * **项目发现**(项目梯五级——KD-M1-23 / 设计 `docs/core/design/MANIFEST.md` §2.2;**git 非前提**,
43
46
  * 2026-09-21 用户裁定):① 锚自身带 `MANIFEST_REL` ⇒ 锚;② 锚含 `.git` ⇒ 锚(缺档 = 建档机会);
44
- * ③ 直接子目录中**带档**者优先(非空即只看此级:恰一 ⇒ `unique` / ≥2 ⇒ `workspace`——下辖多项目
45
- * = 工作区常态,非错误态);④ 零带档才看含 `.git` 的**裸仓**(同判);⑤ 均无 ⇒ `none`(⇒ 建档流,
46
- * 落点 = 会话锚)。
47
+ * ③ 直接子目录中**带档**者非空 ⇒ `workspace`(只看此级;1 ∥ 2 ∥ 更多同判——下辖项目 = 多根工作区,
48
+ * 常态、非错误态,项目梯无 `unique` 态);④ 零带档才看含 `.git` 的**裸仓**(≥1 ⇒ `workspace`,同判);
49
+ * ⑤ 均无 ⇒ `none`(⇒ 建档流,落点 = 会话锚)。
47
50
  * `candidates` 一律**按名排序**(`self` / `none` ⇒ `[]`);`matched` ∈ `manifest` / `git` / `null`
48
51
  * 记命中(或工作区)出自哪一级。**纯 fs**(只判存在性——不解析档内容)· **不抛** · **不向上** · **不递归**。
49
52
  * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**(测试注入面——先于真判据,同 `discoverRepos`)。
50
53
  * @param {string} [cwd] 会话锚(缺省 → 进程 cwd)
51
- * @returns {{kind:'self'|'unique'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
54
+ * @returns {{kind:'self'|'none'|'workspace', root:string|null, candidates:string[], matched:'manifest'|'git'|null}}
52
55
  */
53
56
  export function discoverProjects(cwd) {
54
57
  if (_projectRootOverride) return { kind: "self", root: resolve(_projectRootOverride), candidates: [], matched: null }
@@ -56,11 +59,9 @@ export function discoverProjects(cwd) {
56
59
  if (existsSync(join(anchor, MANIFEST_REL))) return { kind: "self", root: anchor, candidates: [], matched: "manifest" }
57
60
  if (existsSync(join(anchor, ".git"))) return { kind: "self", root: anchor, candidates: [], matched: "git" }
58
61
  const withManifest = scanChildren(anchor, (d) => existsSync(join(d, MANIFEST_REL)))
59
- if (withManifest.length > 1) return { kind: "workspace", root: null, candidates: withManifest, matched: "manifest" }
60
- if (withManifest.length === 1) return { kind: "unique", root: withManifest[0], candidates: [...withManifest], matched: "manifest" }
62
+ if (withManifest.length >= 1) return { kind: "workspace", root: null, candidates: withManifest, matched: "manifest" }
61
63
  const bareRepos = scanChildren(anchor, (d) => existsSync(join(d, ".git")))
62
- if (bareRepos.length > 1) return { kind: "workspace", root: null, candidates: bareRepos, matched: "git" }
63
- if (bareRepos.length === 1) return { kind: "unique", root: bareRepos[0], candidates: [...bareRepos], matched: "git" }
64
+ if (bareRepos.length >= 1) return { kind: "workspace", root: null, candidates: bareRepos, matched: "git" }
64
65
  return { kind: "none", root: null, candidates: [], matched: null }
65
66
  }
66
67
 
@@ -88,22 +89,32 @@ export function discoverRepos(cwd) {
88
89
  }
89
90
 
90
91
  /**
91
- * **归属形单点**(KD-M1-30 · 2026-10-23 用户裁定):自 `target`(目录 = 自身;文件 = 其所在目录)
92
- * 沿**祖先链**上溯,取最近带 `MANIFEST_REL` 的目录 ⇒ 该目录;无 ⇒ `null`(交发现兜底)。
92
+ * **归属形单点 = 判据本体**(KD-M1-30 · 2026-10-23 用户裁定 · 2026-10-25「判据本体真单源」):
93
+ * 自 `target`(目录 = 自身;文件 = 其所在目录)沿**祖先链**上溯,取最近带 `MANIFEST_REL` 的目录
94
+ * ⇒ 该目录;无 ⇒ `null`(不向下、不猜——发现梯是另问,交调用方处置)。
93
95
  * 会话落在项目内层子目录时,这一路上溯正是它归到自己项目根的通路(用户 2026-10-23 23:24
94
96
  * 「本来就该爬到项目根啊」);**发现梯(`discoverProjects`)仍纯向下**——从工作区容器只找直接
95
97
  * 子目录,不把容器挂到外层项目上。**纯 fs** · **不跨兄弟** · **无全局优先级**。
98
+ * **界(`boundary`)由调用方定——两面两界**:取根面(`projectRootView`)传**目标所在盘根**(向上
99
+ * 无界——目标由调用方指名);台账面(`resolveLedgerRoot`)传**会话锚**(越界即停——用户
100
+ * 2026-10-25 19:09「不能跑到工作目录以外去」)。界比较按**段归一**(`isInsideOrEqual`:盘根形带
101
+ * 尾分隔符,`edge + sep` 直比会假阴)。
96
102
  * **覆盖位**(`_setProjectRootForTest` 在场)⇒ **头部短路**:直接返回覆盖值(不查档存在性)。
97
103
  * @param {string} [target] 目标路径(目录 / 文件)
104
+ * @param {string} [boundary] 上溯上界(缺省 → 进程 cwd)
98
105
  * @returns {string|null} 带档目录绝对路径 / null
99
106
  */
100
- export function owningProject(target) {
107
+ export function owningProject(target, boundary) {
101
108
  if (_projectRootOverride) return resolve(_projectRootOverride)
102
109
  let dir = resolve(target ?? ".")
110
+ const edge = resolve(boundary ?? process.cwd())
111
+ // 界:target 在界外(含「target 是界的祖先」)⇒ 无归属——界由调用方定,越界不猜。
112
+ if (!isInsideOrEqual(dir, edge)) return null
103
113
  const st = statSync(dir, { throwIfNoEntry: false })
104
114
  if (st && !st.isDirectory()) dir = dirname(dir)
105
115
  for (;;) {
106
116
  if (existsSync(join(dir, MANIFEST_REL))) return dir
117
+ if (dir === edge) return null
107
118
  const parent = dirname(dir)
108
119
  if (parent === dir) return null
109
120
  dir = parent
@@ -119,24 +130,28 @@ function isInsideOrEqual(dir, bound) {
119
130
  }
120
131
 
121
132
  /** **项目根解析的直读形**(判定单点——设计 `MANIFEST.md` §2.2 `projectRootView` 契约行 / §2.5
122
- * 「单项目解析律与四处会话级缺省收口」条):`ok`(唯一解析:归属命中 ∥ 发现 `self` / `unique`)/
123
- * `workspace`(锚自身无档 ∧ 下辖 **≥2** 项目 ⇒ `root` null + 下辖项目全列**按名排序**——工作区
124
- * 常态,非错误态)/ `none`(无项目 ⇒ 双空);覆盖位在场 ⇒ 头部短路(`ok` + 覆盖值)。
125
- * **非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数(行为逐字零变)。
133
+ * 「单项目解析律与四处会话级缺省收口」条):`ok`(**仅归属命中**——判据本体 = `owningProject`,
134
+ * 界 = **目标所在盘根** ⇒ 向上无界;函数体唯一 `ok` 出口)/ `workspace`(归属无命中 ∧
135
+ * `discoverProjects` 报工作区:下辖带档者 ≥1 ∥ 零带档时裸仓 ≥1 ⇒ `root` null + 下辖项目全列
136
+ * **按名排序**——多根工作区常态,非错误态;1 ∥ ≥2 同判)/ `none`(其余,含裸仓锚——
137
+ * **不向下取根** ⇒ 双空);覆盖位在场 ⇒ 头部短路(`ok` + 覆盖值)。
138
+ * **发现读数只供 `workspace` 报明**:`discoverProjects` 的 `self` **不进 `root`**
139
+ * (判据单源 = `owningProject`)。**非抛错 / 零写**;`resolveProjectRoot` 薄委托本函数。
126
140
  * @returns {{state:'ok'|'workspace'|'none', root:string|null, candidates:string[]}} */
127
141
  export function projectRootView(cwd) {
128
- const owning = owningProject(cwd)
142
+ const target = resolve(cwd ?? ".")
143
+ const owning = owningProject(target, parse(target).root)
129
144
  if (owning) return { state: "ok", root: owning, candidates: [] }
130
- const d = discoverProjects(cwd)
131
- if (d.root) return { state: "ok", root: d.root, candidates: [] }
145
+ const d = discoverProjects(target)
132
146
  if (d.kind === "workspace") return { state: "workspace", root: null, candidates: d.candidates }
133
147
  return { state: "none", root: null, candidates: [] }
134
148
  }
135
149
 
136
- /** 项目根解析(**归属 ∨ 发现**——KD-M1-30 / M1-24):**薄委托** `projectRootView(cwd).root`(判定
137
- * 单点见上——#828)。带档路径与批前**逐字同**;变更面两条(设计 §2.2 同条):① 项目树内路径
138
- * (祖先带档)⇒ 该项目根;② 裸仓恰一 ⇒ 该仓根(零档降级 = 建档机会)。`none` / `workspace` ⇒
139
- * `null`。调用方零改(行为随语义变更——设计 §2.5 键面条 / 错层条)。
150
+ /** 项目根解析(**归属形**——KD-M1-30 / M1-24):**薄委托** `projectRootView(cwd).root`(判定
151
+ * 单点见上——#828)。带档路径与批前**逐字同**;变更面(设计 §2.2 同条):① 项目树内路径
152
+ * (祖先带档)⇒ 该项目根(子目录归到自己的项目根 = 正常通路);② 祖链无档 ⇒ `null`——
153
+ * **不向下取根**(下辖带档子目录恰一 ∥ 裸仓恰一均 `null`,发现读数不进 `root`)。
154
+ * `none` / `workspace` ⇒ `null`。调用方零改(行为随语义变更——设计 §2.5 键面条 / 错层条)。
140
155
  * @returns {string|null} 项目根绝对路径 / null */
141
156
  export function resolveProjectRoot(cwd) {
142
157
  return projectRootView(cwd).root
package/manifest.mjs CHANGED
@@ -21,13 +21,13 @@
21
21
  * - discoverProjects(cwd) → { kind, root, candidates, matched }:**项目梯**(五级 / git 非前提——KD-M1-23);
22
22
  * `discoverRepos(cwd)` 同形态 = **仓梯**(`.git` 视图——`git` 工具经它接线,KD-M1-22)。
23
23
  * 两梯共一模块私有 walk 内核 `scanChildren`(单源);`candidates` 按名排序、`matched` 记档位。
24
- * - owningProject(target) → 归属形单点(沿祖先链取**最近带档目录**——KD-M1-30)。
24
+ * - owningProject(target, boundary) → 归属形单点(沿祖先链取**最近带档目录**;界由调用方定——KD-M1-30)。
25
25
  * - projectView(target) → { state, root, path, manifest?, candidates?, errors?, matched }:**按用点解析**
26
26
  * 的读侧单点(归属 ∨ 发现兜底 / 五态 / 非抛错 / **零写**——KD-M1-24)。
27
- * - resolveProjectRoot(cwd):项目根(归属 ∨ 发现——KD-M1-30)= `projectRootView(cwd).root`(薄委托);
27
+ * - resolveProjectRoot(cwd):项目根(**归属形**——KD-M1-30)= `projectRootView(cwd).root`(薄委托;取值规则见下行);
28
28
  * projectRootView(cwd) → { state: ok|workspace|none, root, candidates }:项目根直读形(判定单点——§2.2)。
29
29
  * - resolveEngineeringManifest(cwd, { writer, init }) → 入口决策树(**非抛错**——KD-M1-20):
30
- * 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——失败码五枚
30
+ * 两端入口钩子与翻转面(拒翻)共用同一张树(判据单源;§2.8 F1)——态码五枚
31
31
  * `missing` / `invalid` / `no-project` / `workspace` / `init-failed`(KD-M1-28)。名中 `Engineering` = 历史命名(沿用不改名);消费面 = 两端入口钩子 + 翻转面——**非**「模式拥有本模块」。
32
32
  * - manifestFilePath(cwd) → 数据档绝对路径(档路径单源 KD-M1-18——读 / 写 / mtime 门控三处同源)。
33
33
  * - initManifest(cwd, { writer = 'subagent' } = {}) → 经写门写 DEFAULT_MANIFEST,缺省拒。
@@ -38,7 +38,7 @@
38
38
  * N3 可迁移:本模块不硬编码任何本仓路径(docRoot 由被开发项目声明;机检参数不在 manifest 面——住引擎内置常量)。
39
39
  */
40
40
  import { readFileSync, writeFileSync } from "node:fs"
41
- import { join, resolve } from "node:path"
41
+ import { join, parse, resolve } from "node:path"
42
42
  import { DEFAULT_MANIFEST, isValidDocRootValue, validateManifest, fillDefaults } from "./manifest-schema.mjs"
43
43
  import { MANIFEST_REL, discoverProjects, owningProject, resolveProjectRoot } from "./manifest-discovery.mjs"
44
44
  // #802(KD-M1-36):未知键读面告警的事件面——诊断日志(fire-and-forget,失败静默降级)。
@@ -50,10 +50,11 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
50
50
 
51
51
  /**
52
52
  * **按用点解析**的读侧单点(KD-M1-24 / M1-30——**非抛错 / 零写 / 无缓存**):两段合成——
53
- * · **归属(第一段——§⑥ 归属形)**:`owningProject(target)` 沿祖先链取最近带档目录
54
- * (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。
55
- * · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒ 命中
56
- * (`self` / `unique`)⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`workspace` ⇒
53
+ * · **归属(第一段——§⑥ 归属形)**:`owningProject(target, target 所在盘根)` 沿祖先链取最近带档目录
54
+ * (子优于根 / 不跨兄弟 / 无全局优先级)⇒ 读该档 ⇒ `ok` / `invalid`。界与 `projectRootView`
55
+ * 同源(目标所在盘根)——同一 target 两面同归属,非会话锚。
56
+ * · **发现兜底(第二段——§⑥ 发现规则,纯向下)**:祖先链无档 ⇒ `discoverProjects(target)` ⇒
57
+ * `self` ⇒ 读其档(带档 ⇒ `ok` / `invalid`;缺档 ⇒ `missing`);`workspace` ⇒
57
58
  * `workspace`(+ `candidates`);`none` ⇒ `no-project`。
58
59
  * 写侧(建档)**不在**此函数(归入口决策树——KD-M1-25 / M1-29)⇒ 注入器 / 只读消费面不会变写点。
59
60
  * `matched` 契约:归属段命中 ⇒ `'manifest'`;发现段 ⇒ `discoverProjects.matched` 逐字;
@@ -65,7 +66,7 @@ export { DEFAULT_MANIFEST, MANIFEST_SCHEMA, isValidDocRootValue, validateManifes
65
66
  */
66
67
  export function projectView(target) {
67
68
  const anchor = resolve(target ?? ".")
68
- const owning = owningProject(anchor)
69
+ const owning = owningProject(anchor, parse(anchor).root)
69
70
  if (owning) return viewAtRoot(owning, "manifest")
70
71
  const d = discoverProjects(anchor)
71
72
  if (d.kind === "workspace") return { state: "workspace", root: null, path: null, candidates: d.candidates, matched: d.matched }
@@ -180,7 +181,7 @@ export function readManifest(cwd) {
180
181
 
181
182
  /**
182
183
  * 装配钩子入口(模块设计 §2.2)——同 readManifest(cwd)。ok:true 返回补默认值后的 manifest;
183
- * reason:'missing' 由调用方走**建档流**(**工程模式会话**口径——梯②④⑤ 就地建档,**不拒会话**;
184
+ * reason:'missing' 由调用方走**建档流**(**工程模式会话**口径——梯②⑤ 就地建档,**不拒会话**;
184
185
  * KD-M1-29);普通会话 = 装配钩子零 manifest I/O(KD-M1-12)。
185
186
  */
186
187
  export function requireManifest(cwd) {
@@ -205,17 +206,17 @@ export function onManifestInit(fn) {
205
206
  *
206
207
  * 分支(两分支六出口——§2.8 F1 树):
207
208
  * 档合法 → { ok:true, manifest, created:false }(manifest = 补默认值后的档内容)
208
- * 缺档 + init → 梯②④⑤(锚 = 裸仓 / 裸仓命中 / 无项目)⇒ `initManifest(cwd, { writer })`
209
- * (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥2 项目)⇒
209
+ * 缺档 + init → 梯②⑤(锚 = 裸仓 / 无项目)⇒ `initManifest(cwd, { writer })`
210
+ * (抛错 → `init-failed`);工作区(锚自身无档 ∧ 下辖 ≥1 项目——1 ∥ 更多同判)⇒
210
211
  * `{ ok:false, code:'workspace', message, candidates }`(**不建 / 不猜**)
211
212
  * 缺档 + !init → 梯⑤(无项目)⇒ `{ ok:false, code:'no-project' }`(KD-M1-28;`init:false` 面);
212
- * 梯②④(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;工作区 ⇒ 同上
213
+ * 梯②(裸仓可解析、档缺)⇒ `{ ok:false, code:'missing' }`;工作区 ⇒ 同上
213
214
  * 档非法 → { ok:false, code:'invalid', message, errors }
214
215
  *
215
216
  * `writer` 由调用点**显式**传(生产调用点全传 `'main'`):缺省 `'subagent'` 是写门 fail-closed
216
- * 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②④⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
217
+ * 缺省(KD-M1-3),误用缺省 ⇒「缺档 + 梯②⑤」退化为 `init-failed`(拒翻——与 §2.8 F2 /
217
218
  * AC-20② 语义相反;T38 反证格)。
218
- * 失败码文案分野(KD-M1-28):无项目 ⇒ 可在锚处落地 / 工作区 ⇒ 报明事实 + 点名项目根;文案族 =
219
+ * 态码文案分野(KD-M1-28):无项目 ⇒ 可在锚处落地 / 工作区 ⇒ 报明事实 + 点名项目根;文案族 =
219
220
  * 「项目不可解析」(稳定锚句 `/项目不可解析/`)——**仅「档非法」格**(§2.8 F2 拒翻面唯一拒格)。
220
221
  * 边界:`readManifest` 的非 ENOENT 读错(权限 / 目录等)由 `projectView` 收为 `invalid`(非抛错
221
222
  * 契约——`readManifest` 返回语义零改);本树只承诺上列六出口。
@@ -236,7 +237,7 @@ export function resolveEngineeringManifest(cwd, { writer = "subagent", init = tr
236
237
  if (view.state === "workspace") {
237
238
  return { ok: false, code: "workspace", candidates: view.candidates, message: workspaceProjectMessage(cwd, view.candidates) }
238
239
  }
239
- // 缺档(梯②④⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
240
+ // 缺档(梯②⑤):init ⇒ 就地建档(内容 = DEFAULT_MANIFEST,经写门 writer:'main');!init ⇒ 归码。
240
241
  if (!init) return { ok: false, code: view.state === "no-project" ? "no-project" : "missing" }
241
242
  try {
242
243
  const manifest = initManifest(cwd, { writer })