@thincoder/core 0.10.11 → 0.10.12
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.
- package/CHANGELOG.md +8 -0
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent-tools/batch-gate.mjs +65 -49
- package/agent-tools/batch-lifecycle.mjs +66 -9
- package/agent-tools/batch-paths.mjs +10 -8
- package/agent-tools/design-slots.mjs +72 -12
- package/agent-tools/design-token.mjs +18 -2
- package/agent-tools/manifest.mjs +1 -1
- package/agent-tools/review-facts.mjs +1 -1
- package/ledger-cmd.mjs +39 -37
- package/ledger-db.mjs +60 -59
- package/ledger-tools.mjs +60 -23
- package/ledger.mjs +12 -10
- package/manifest-discovery.mjs +41 -26
- package/manifest.mjs +17 -16
- package/model-specs.mjs +11 -13
- package/package.json +1 -1
- package/prompts/advisor-design.md +4 -3
- package/prompts/common.md +11 -1
- package/prompts/discipline-engineering.md +16 -7
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-coder.md +8 -1
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +11 -1
- package/prompts/persona-engineering.md +21 -11
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/session-slot-write.mjs +21 -1
- package/session-slots-manifest.mjs +21 -4
- package/token-ttl.mjs +68 -4
- package/tool-docs/wait_for.md +15 -6
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { randomUUID } from "node:crypto"
|
|
15
15
|
import { tokenExpiryMs } from "../token-ttl.mjs"
|
|
16
|
+
// 批归属派生(DESIGN-TOKEN-SETTLEMENT §10)——判据与清点面同源(同一个 `lastIndexOf("batches")`)。
|
|
17
|
+
// 本档 ⇄ `design-slots.mjs` 为**静态环**(对方取 `effectiveTokenTtlMs`):两侧绑定只在函数体内
|
|
18
|
+
// 运行时使用,求值期零顶层调用 ⇒ 环安全(与 token-ttl ⇄ session 同构)。
|
|
19
|
+
import { batchAbsPath } from "./design-slots.mjs"
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* F2c/F2e (§29.1 2026-09-07): the engine-generated Approved suffix — ONE builder
|
|
@@ -120,8 +124,10 @@ const VERDICT_GATE_MARK = (reason) => `裁定非通过——token 未签发 (rev
|
|
|
120
124
|
* IS the verdict — the advisor echoes it only on approval. On echo (full string
|
|
121
125
|
* OR truncated — uuid present; §5.1①): slot the token under designId (always the
|
|
122
126
|
* FULL token; + eng-coder gate flag; single-value mirror retired per
|
|
123
|
-
* DESIGN-TOKEN-SETTLEMENT D3)
|
|
124
|
-
*
|
|
127
|
+
* DESIGN-TOKEN-SETTLEMENT D3), record its batch attribution (`_engDesignTokenBatches`
|
|
128
|
+
* — the reviewed doc-set's `batches/` file, §10 E1; no batch in the set ⇒ no
|
|
129
|
+
* attribution) and return the clean output with the
|
|
130
|
+
* Approved suffix (truncated echo: the M2 marker sits before the suffix); the review
|
|
125
131
|
* instance CLOSES (a later review of the same doc-set starts a fresh full
|
|
126
132
|
* review). On non-echo: strip every echo form (single source §5.1③) and return
|
|
127
133
|
* the findings text + the constant M1 marker — slots stay untouched
|
|
@@ -164,6 +170,16 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
|
|
|
164
170
|
// Echoed the token → review passed. Issue it to the parent for eng-coder.
|
|
165
171
|
agent._engDesignTokens ??= new Map()
|
|
166
172
|
agent._engDesignTokens.set(run.designId, designToken)
|
|
173
|
+
// §10 E1(DESIGN-TOKEN-SETTLEMENT):批归属旁列同拍写——派生 = 本评审文档集 `docSetKey` 中
|
|
174
|
+
// `batches/` 段档的绝对路径(零新输入)。文档集不含批档 ⇒ 归属 = 无(不猜):**删**该 designId
|
|
175
|
+
// 的旧行——否则一次改了范围的复评会让新槽继承上一轮的归属,收口时消费掉本不该消费的授权。
|
|
176
|
+
const batchAbs = batchAbsPath(run.docSetKey)
|
|
177
|
+
if (batchAbs) {
|
|
178
|
+
agent._engDesignTokenBatches ??= new Map()
|
|
179
|
+
agent._engDesignTokenBatches.set(run.designId, batchAbs)
|
|
180
|
+
} else if (agent._engDesignTokenBatches instanceof Map) {
|
|
181
|
+
agent._engDesignTokenBatches.delete(run.designId)
|
|
182
|
+
}
|
|
167
183
|
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
|
|
168
184
|
// 只写多槽 Map(AC3 零镜像写);settle 落盘由调用方(settleAdvisorRun)当场做。
|
|
169
185
|
// Unlock the dispatch design gate for eng-coder SELF-review (defense-in-depth —
|
package/agent-tools/manifest.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* 目标面(KD-M1-39——判据复用零新解析器):`read` = `projectView`;`init` / `write` =
|
|
21
21
|
* `projectRootView`(工作区 ⇒ 拒 + 下辖项目全列 + 显式 `target` 指引);`resolveProjectRoot` 对
|
|
22
|
-
* 工作区塌缩 `null`
|
|
22
|
+
* 工作区塌缩 `null`(**回落发生在调用方 `writeRoot`,非本函数**)⇒ 写 / 建两动作**不得**直用。
|
|
23
23
|
*
|
|
24
24
|
* 写路径复用(零新写路径):落盘恒经 `writeManifest` / `initManifest`——`writer` 恒显式 `'main'`
|
|
25
25
|
* (缺省 `'subagent'` = 写门 fail-closed 缺省,KD-M1-3 / AC-M1-5 零改)。
|
|
@@ -87,7 +87,7 @@ export function resolveReviewDocPaths(documents, cwd) {
|
|
|
87
87
|
if (!abs && view.state !== "ok") { invalid.push({ doc, resolved: null, reason: "no-project-root" }); continue }
|
|
88
88
|
const p = abs ? doc : resolve(view.root, doc)
|
|
89
89
|
if (!abs && !isInside(p, view.root)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
|
|
90
|
-
if (abs && !owningProject(p)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
|
|
90
|
+
if (abs && !owningProject(p, cwd)) { invalid.push({ doc, resolved: p, reason: "outside-project" }); continue }
|
|
91
91
|
if (!isDocFile(p)) { invalid.push({ doc, resolved: p, reason: "not-doc-file" }); continue }
|
|
92
92
|
resolved.set(doc, p)
|
|
93
93
|
}
|
package/ledger-cmd.mjs
CHANGED
|
@@ -40,9 +40,9 @@ function isRowAged(row, now) {
|
|
|
40
40
|
* `batch` 同形同口径)。 */
|
|
41
41
|
const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
|
|
42
42
|
|
|
43
|
-
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd)?.root
|
|
44
|
-
* ?? resolve(cwd ?? ".")
|
|
45
|
-
const baseOf = (cwd) => resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
|
|
43
|
+
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd, boundary)?.root
|
|
44
|
+
* ?? resolve(cwd ?? ".")`。`boundary` = 会话锚(与 `openLedger` 同一枚上界——同一笔动作的判据不能两界)。 */
|
|
45
|
+
const baseOf = (cwd, boundary) => resolveLedgerRoot(cwd, boundary)?.root ?? resolve(cwd ?? ".")
|
|
46
46
|
|
|
47
47
|
/** `evidence` 超限读数(设计档 §5 P4——**读数非闸门**):缺 / 空 ⇒ false;`query` 逐行携(含归档行——
|
|
48
48
|
* 行值事实);`count` 的统计集 = 未决四态(§7.1)。 */
|
|
@@ -92,8 +92,8 @@ export function overdueOf(row, { now = Date.now(), base = process.cwd() } = {})
|
|
|
92
92
|
* (§3.7 出口态字串 ∥ null——机判单源 `overdueOf`);`task_book` = 第 5 枚等值过滤(**原值等值**——列串直比,与 §3.3 选行的
|
|
93
93
|
* 「文件部分解析同址」两径口径不同、各自单源);`trigger` 过滤仅三枚举等值(`NULL` 不过滤);
|
|
94
94
|
* `now` = 注入缝(确定性用例)。 */
|
|
95
|
-
export function ledgerQuery({ cwd, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
|
|
96
|
-
const db = openLedger(cwd, { readOnly: true })
|
|
95
|
+
export function ledgerQuery({ cwd, boundary, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
|
|
96
|
+
const db = openLedger(cwd, { readOnly: true, boundary })
|
|
97
97
|
if (!db) return []
|
|
98
98
|
try {
|
|
99
99
|
const where = [], args = []
|
|
@@ -103,7 +103,7 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
|
|
|
103
103
|
if (trigger) { where.push("trigger = ?"); args.push(trigger) }
|
|
104
104
|
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
105
105
|
const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
|
|
106
|
-
const base = baseOf(cwd)
|
|
106
|
+
const base = baseOf(cwd, boundary)
|
|
107
107
|
return rows.map((r) => ({ ...r, aged: isRowAged(r, now), evidenceOversize: isEvidenceOversize(r), taskBookOnDisk: pointerOnDisk(r, base), overdue: overdueOf(r, { now, base }) }))
|
|
108
108
|
} finally { db.close() }
|
|
109
109
|
}
|
|
@@ -113,14 +113,14 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
|
|
|
113
113
|
* 机判单源);`evidenceOversize` = 其中 `evidence` 超限行数(§5)。**一次 SELECT + 逐行判**——
|
|
114
114
|
* `evidenceOversize` 与 `overdue` 同一次遍历(零额外 IO;成本模型 = §7.2);不设缓存(读数须反映
|
|
115
115
|
* 即时盘面)。库不在 = 三零。 */
|
|
116
|
-
export function ledgerCount({ cwd, task_book = null } = {}) {
|
|
117
|
-
const db = openLedger(cwd, { readOnly: true })
|
|
116
|
+
export function ledgerCount({ cwd, boundary, task_book = null } = {}) {
|
|
117
|
+
const db = openLedger(cwd, { readOnly: true, boundary })
|
|
118
118
|
if (!db) return { count: 0, overdue: 0, evidenceOversize: 0 }
|
|
119
119
|
try {
|
|
120
120
|
const where = [`status IN (${PENDING_STATUSES.map(() => "?").join(",")})`], args = [...PENDING_STATUSES]
|
|
121
121
|
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
122
122
|
const rows = db.prepare(`SELECT * FROM items WHERE ${where.join(" AND ")}`).all(...args)
|
|
123
|
-
const now = Date.now(), base = baseOf(cwd)
|
|
123
|
+
const now = Date.now(), base = baseOf(cwd, boundary)
|
|
124
124
|
let overdue = 0, oversize = 0
|
|
125
125
|
for (const r of rows) {
|
|
126
126
|
if (overdueOf(r, { now, base }) !== null) overdue++
|
|
@@ -132,8 +132,8 @@ export function ledgerCount({ cwd, task_book = null } = {}) {
|
|
|
132
132
|
|
|
133
133
|
/** `evidence` 超限行数(设计档 §7.1 导出面——`count` 回报读数**同一单源**:本函数为其薄委托,
|
|
134
134
|
* 不另立第二判定面)。 */
|
|
135
|
-
export function ledgerEvidenceOversize({ cwd, task_book = null } = {}) {
|
|
136
|
-
return ledgerCount({ cwd, task_book }).evidenceOversize
|
|
135
|
+
export function ledgerEvidenceOversize({ cwd, boundary, task_book = null } = {}) {
|
|
136
|
+
return ledgerCount({ cwd, boundary, task_book }).evidenceOversize
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
@@ -149,7 +149,7 @@ const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
|
149
149
|
* 判位 = 与迁移表判同层(落盘前);文案前缀 = 调用函数名;基准 = §6.1 口径 2(根推导单源同子表达式)。
|
|
150
150
|
* @param {{status:string, trigger:string|null, taskBook:string|null, wroteTaskBook:boolean, wroteBatchTrigger:boolean}} face 本笔写入面
|
|
151
151
|
*/
|
|
152
|
-
function assertTaskBookGate(cwd, fnName, face) {
|
|
152
|
+
function assertTaskBookGate(cwd, fnName, face, boundary) {
|
|
153
153
|
const { status, trigger, taskBook, wroteTaskBook, wroteBatchTrigger } = face
|
|
154
154
|
if ((status === "在途" || status === "待核销" || trigger === "归批") && !namesPointer(taskBook)) {
|
|
155
155
|
throw new Error(`${fnName}:task_book 必填(在途 / 待核销 / 归批 须携任务书指针)`)
|
|
@@ -157,14 +157,14 @@ function assertTaskBookGate(cwd, fnName, face) {
|
|
|
157
157
|
if (!wroteTaskBook && !wroteBatchTrigger) return // 臂② 之外零判:旧指针所指档已清不阻任何此类写入
|
|
158
158
|
const part = filePartOf(taskBook)
|
|
159
159
|
if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
|
|
160
|
-
const base = baseOf(cwd) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
|
|
160
|
+
const base = baseOf(cwd, boundary) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
|
|
161
161
|
const { ok, abs } = resolveDeclaredRef(base, part)
|
|
162
162
|
if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。`row.resume_when` 可空(§3.4)。 */
|
|
166
|
-
export function ledgerAdd({ cwd, row }) {
|
|
167
|
-
const db = openLedger(cwd, { create: true })
|
|
166
|
+
export function ledgerAdd({ cwd, row, boundary }) {
|
|
167
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
168
168
|
try {
|
|
169
169
|
// 结果行状态 = INSERT 硬编码「待讨论」⇒ 臂① 的 `status` 半幅当下不触;`trigger=归批` 半幅与臂② 照判
|
|
170
170
|
assertTaskBookGate(cwd, "ledgerAdd", {
|
|
@@ -173,7 +173,7 @@ export function ledgerAdd({ cwd, row }) {
|
|
|
173
173
|
taskBook: row.task_book ?? null,
|
|
174
174
|
wroteTaskBook: namesPointer(row.task_book),
|
|
175
175
|
wroteBatchTrigger: row.trigger === "归批",
|
|
176
|
-
})
|
|
176
|
+
}, boundary)
|
|
177
177
|
const now = nowIso()
|
|
178
178
|
const r = db.prepare(`INSERT INTO items (kind, status, title, board, req_doc, task_book, evidence, trigger, resume_when, created_at, updated_at) VALUES (?, '待讨论', ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
179
179
|
.run(row.kind, row.title, row.board ?? null, row.req_doc ?? null, row.task_book ?? null, row.evidence ?? null, row.trigger ?? null, row.resume_when ?? null, now, now)
|
|
@@ -218,7 +218,7 @@ function assertEvidenceValue(fnName, evidence) {
|
|
|
218
218
|
* (含两拒面)→ 写门 → UPDATE(判序 = §13.9 逐字未变)。`to` = `patch.status ?? 行现态`
|
|
219
219
|
* ——批量形纯字段补丁(不携 `status`)时 `to === row.status` ⇒ 迁移表不判、`status` 零写(§3.3)。
|
|
220
220
|
* `resume_when` = 普通可空字段补丁(`patch.resume_when ?? 行值`——§3.4)。 */
|
|
221
|
-
function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
221
|
+
function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary) {
|
|
222
222
|
const to = patch.status ?? row.status
|
|
223
223
|
if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
|
|
224
224
|
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
@@ -237,7 +237,7 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
|
237
237
|
taskBook: nextTaskBook,
|
|
238
238
|
wroteTaskBook: namesPointer(patch.task_book),
|
|
239
239
|
wroteBatchTrigger: patch.trigger === "归批",
|
|
240
|
-
}) // → 写门 → UPDATE
|
|
240
|
+
}, boundary) // → 写门 → UPDATE
|
|
241
241
|
const now = nowIso()
|
|
242
242
|
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, resume_when = ?, executor = ?, updated_at = ? WHERE id = ?`)
|
|
243
243
|
.run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, nextEvidence, patch.trigger ?? row.trigger, patch.resume_when ?? row.resume_when, nextExecutor, now, row.id)
|
|
@@ -249,12 +249,12 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
|
249
249
|
* 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
|
|
250
250
|
* `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
|
|
251
251
|
* / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。**单行形**(`id`)——逐行落笔 = `applyRowUpdate`。 */
|
|
252
|
-
export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
|
|
253
|
-
const db = openLedger(cwd, { create: true })
|
|
252
|
+
export function ledgerUpdate({ cwd, id, patch, executorSessionId, boundary }) {
|
|
253
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
254
254
|
try {
|
|
255
255
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
256
256
|
if (!row) throw new Error(`ledgerUpdate:行 ${id} 不存在`)
|
|
257
|
-
const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId)
|
|
257
|
+
const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary)
|
|
258
258
|
return { id: rowId }
|
|
259
259
|
} finally { db.close() }
|
|
260
260
|
}
|
|
@@ -286,9 +286,9 @@ function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
|
286
286
|
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 → 追认门(原样——判**结果值**缺 /
|
|
287
287
|
* 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3 收口两源)。**单行形**
|
|
288
288
|
* (`id`)——逐行落笔 = `applyRowClose`。 */
|
|
289
|
-
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
|
|
289
|
+
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, boundary }) {
|
|
290
290
|
if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
|
|
291
|
-
const db = openLedger(cwd, { create: true })
|
|
291
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
292
292
|
try {
|
|
293
293
|
db.exec("BEGIN")
|
|
294
294
|
try {
|
|
@@ -330,16 +330,16 @@ function resolveBatchRows(db, base, abs) {
|
|
|
330
330
|
* 并抛(零行改)——故「部分落地」只由逐行门拒产生,「中途失败」不留半批。
|
|
331
331
|
* `BEGIN IMMEDIATE`:写锁一次先把(逐行不升级锁 ⇒ 事务级失败在首行之前即失败)。
|
|
332
332
|
* @returns {{batch: string, status: string|null, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
333
|
-
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId }) {
|
|
334
|
-
const db = openLedger(cwd, { create: true })
|
|
333
|
+
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, boundary }) {
|
|
334
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
335
335
|
try {
|
|
336
|
-
const base = baseOf(cwd)
|
|
336
|
+
const base = baseOf(cwd, boundary)
|
|
337
337
|
const abs = assertBatchArg("ledgerUpdate", base, batch)
|
|
338
338
|
db.exec("BEGIN IMMEDIATE")
|
|
339
339
|
try {
|
|
340
340
|
const applied = [], skipped = []
|
|
341
341
|
for (const row of resolveBatchRows(db, base, abs)) {
|
|
342
|
-
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId).id) }
|
|
342
|
+
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary).id) }
|
|
343
343
|
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
344
344
|
}
|
|
345
345
|
db.exec("COMMIT")
|
|
@@ -355,11 +355,11 @@ export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId })
|
|
|
355
355
|
* (`evidenceValue` 同源)。回执 `status` 恒 `已核销`(批量形缺省 = 勾销单值;携则守卫已收窄为
|
|
356
356
|
* `{已核销}`——§3.2 `whenPresent`)。事务语义同 `ledgerUpdateBatch`。
|
|
357
357
|
* @returns {{batch: string, status: string, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
358
|
-
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace }) {
|
|
358
|
+
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, boundary }) {
|
|
359
359
|
const status = "已核销"
|
|
360
|
-
const db = openLedger(cwd, { create: true })
|
|
360
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
361
361
|
try {
|
|
362
|
-
const base = baseOf(cwd)
|
|
362
|
+
const base = baseOf(cwd, boundary)
|
|
363
363
|
const abs = assertBatchArg("ledgerClose", base, batch)
|
|
364
364
|
db.exec("BEGIN IMMEDIATE")
|
|
365
365
|
try {
|
|
@@ -387,13 +387,14 @@ const REANCHOR_STATUSES = ["待讨论", "待设计", "在途"]
|
|
|
387
387
|
|
|
388
388
|
/** 本批行集(只读,任意状态——设计档 §7.1):批身份单源 = `resolveBatchRows` 解析同址(零二写);
|
|
389
389
|
* 逐行携读数 `taskBookOnDisk`(不入库、不入 `DATA_COLUMNS`)。`batch` 不可解析 ∥ 无根 ⇒ 抛(同族
|
|
390
|
-
* `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd`
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
* `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd` 基准——不用会话锚);
|
|
391
|
+
* `boundary` = 上溯上界(会话锚——与工具面同一枚界;缺省 = 进程 cwd)。 */
|
|
392
|
+
export function ledgerBatchRows({ cwd, batch, boundary } = {}) {
|
|
393
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
|
|
393
394
|
if (!derived) throw new Error(`ledgerBatchRows:批档所属项目根不可推导(${cwd ?? "(缺 cwd)"})`)
|
|
394
395
|
const base = derived.root
|
|
395
396
|
const abs = assertBatchArg("ledgerBatchRows", base, batch) // 失败语义先于「库不在 ⇒ 空集」(不可解析就是不可解析)
|
|
396
|
-
const db = openLedger(base, { readOnly: true })
|
|
397
|
+
const db = openLedger(base, { readOnly: true, boundary })
|
|
397
398
|
if (!db) return []
|
|
398
399
|
try {
|
|
399
400
|
return resolveBatchRows(db, base, abs).map((r) => ({ ...r, taskBookOnDisk: pointerOnDisk(r, base) }))
|
|
@@ -406,14 +407,15 @@ export function ledgerBatchRows({ cwd, batch } = {}) {
|
|
|
406
407
|
* 复句追加 `evidence`。单事务:任一步失败 ⇒ 整集 `ROLLBACK`(零行改)。**不抛**——失败经返回形 `failed`
|
|
407
408
|
* 报出(`null` 常态);`cwd` 无根 ⇒ 不取行、不重锚(**不落兜底基准**——落了就是静默零命中)。
|
|
408
409
|
* `updated_at` 零触(机器重锚不是人工推进的痕迹——行龄读数照旧)。
|
|
410
|
+
* `boundary` = 上溯上界(会话锚——与工具面 / `ledgerBatchRows` 同一枚界;缺省 = 进程 cwd)。
|
|
409
411
|
* @returns {{batch:string, reanchored:{id:number,status:string,title:string,sentenceTo:"resume_when"|"evidence"}[], alreadyAnchored:{id:number,status:string,title:string}[], failed:string|null}} */
|
|
410
|
-
export function ledgerReanchorBatch({ cwd, batch } = {}) {
|
|
412
|
+
export function ledgerReanchorBatch({ cwd, batch, boundary } = {}) {
|
|
411
413
|
const failedResult = (why) => ({ batch, reanchored: [], alreadyAnchored: [], failed: why })
|
|
412
|
-
const derived = cwd == null ? null : resolveLedgerRoot(cwd)
|
|
414
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
|
|
413
415
|
if (!derived) return failedResult("批档所属项目根不可推导")
|
|
414
416
|
const base = derived.root
|
|
415
417
|
let db
|
|
416
|
-
try { db = openLedger(base) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
|
|
418
|
+
try { db = openLedger(base, { boundary }) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
|
|
417
419
|
if (!db) return { batch, reanchored: [], alreadyAnchored: [], failed: null } // 库不在 ⇒ 零行可锚
|
|
418
420
|
try {
|
|
419
421
|
let abs
|
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
|
|
10
|
-
*
|
|
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 {
|
|
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
|
-
/** 键控库文件路径(根 →
|
|
49
|
-
*
|
|
49
|
+
/** 键控库文件路径(根 → 库位)——**内部形**:族枚举 / 库路径解析内部构造库位时用它,**不得经
|
|
50
|
+
* `ledgerDbPath`**(互指即环,设计档 §3.8「无自指」)。库目录 = `ledgerDir`(`_setLedgerDirForTest` 覆盖 ⇒ 夹具根外零写)。 */
|
|
50
51
|
const keyedDbFile = (root) => join(ledgerDir, `${ledgerKey(root)}.db`)
|
|
51
52
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
67
|
-
* **惰性枚举 + 上限**——大目录零全量扫描)。消费面 =
|
|
68
|
-
* `ledger` =
|
|
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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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 =
|
|
234
|
-
* **根推导**(设计档 §2.1 / §3.8):项目根 = `resolveLedgerRoot(cwd)`——读写同一判据;嵌套取近者、
|
|
235
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
18
|
-
const
|
|
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
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 = "
|
|
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
|
|
262
|
-
const id = ledgerAdd({ cwd: target, row })
|
|
263
|
-
return JSON.stringify({ id, status: "待讨论"
|
|
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
|
|
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 })
|
|
276
|
-
return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId })
|
|
308
|
+
if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId, boundary: anchor }) })
|
|
309
|
+
return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId, boundary: anchor }) })
|
|
277
310
|
}
|
|
278
311
|
case "close": {
|
|
279
|
-
const target = rest.cwd
|
|
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 })
|
|
282
|
-
return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace })
|
|
314
|
+
if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
|
|
315
|
+
return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
|
|
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
|
|
288
|
-
|
|
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)
|