@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.
- package/CHANGELOG.md +21 -0
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent/run-stages.mjs +33 -29
- package/agent/setup.mjs +3 -0
- package/agent/turn-loop.mjs +3 -0
- package/agent-tools/batch-gate.mjs +65 -49
- package/agent-tools/batch-lifecycle.mjs +82 -12
- package/agent-tools/batch-paths.mjs +10 -8
- package/agent-tools/batch-skeleton.mjs +9 -1
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- 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/agent.mjs +1 -0
- package/client-identity.mjs +54 -0
- package/config.mjs +19 -13
- package/context.mjs +5 -5
- package/doc-check/index.mjs +28 -15
- package/doc-check/paragraph.mjs +6 -3
- package/embedding.mjs +2 -0
- package/generate-title.mjs +3 -2
- package/ledger-close-proof.mjs +80 -0
- package/ledger-cmd.mjs +64 -53
- 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 +17 -18
- 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 +23 -11
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +19 -3
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +10 -4
- package/provider/list-models.mjs +4 -3
- package/provider/responses.mjs +17 -7
- package/session-lifecycle.mjs +3 -0
- package/session-slot-write.mjs +21 -1
- package/session-slots-manifest.mjs +21 -4
- package/token-ttl.mjs +68 -4
- package/token-window.mjs +23 -2
- package/tool-docs/wait_for.md +15 -6
package/doc-check/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* doc-check/index.mjs — 文档纪律机检 · 引擎入口(两条判据 + 报面行)。
|
|
3
3
|
*
|
|
4
4
|
* 判据两条(起步 · 闭集——口径单源 = `docs/core/design/DOC-DISCIPLINE.md` §3.20 条目 S):
|
|
5
|
-
* ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;②
|
|
5
|
+
* ① 锚(档缺 / 节缺)——实现 = `./anchors.mjs`;② 段判据(文字太长,难于阅读 ∥ 行太长)——口径 = §3.18 条目 Q,实现 = `./paragraph.mjs`。
|
|
6
6
|
* 判据参数 = **工具内置常量** `DOC_CHECK_BUILTINS`:不读任何项目的声明面,因而没有「缺参数 ⇒ 拒跑」这一层,
|
|
7
7
|
* 也没有常量校验面——改判据 = 改代码、走评审。
|
|
8
8
|
* 判定范围 = **调用方传入的文件清单**(判定范围 = 解析范围 = 传入的那几档;无目录遍历、无全域兜底):
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { statSync } from "node:fs"
|
|
17
17
|
import { relative, resolve } from "node:path"
|
|
18
|
-
import { checkParagraphs } from "./paragraph.mjs"
|
|
18
|
+
import { checkParagraphs, DOC_CLASS } from "./paragraph.mjs"
|
|
19
19
|
import { checkAnchors } from "./anchors.mjs"
|
|
20
20
|
|
|
21
21
|
/** 判据参数(内置常量 · 冻结):阈值 300 字符(一段 ∥ 一行同值——§3.18 ④);两项空列表 = 区带类 / 豁免类默认值且恒空
|
|
@@ -62,20 +62,32 @@ function resolveRange(base, docs) {
|
|
|
62
62
|
return { check: check.sort(), undecidable, missing, notMarkdown, invalid }
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** 段判据命中行(报面单源——两类命中同形,按 `clazz`
|
|
66
|
-
const paragraphHitLine = (base, h) => `✗ ${h.clazz} ${rel(base, h.file)}:${h.line}
|
|
65
|
+
/** 段判据命中行(报面单源——两类命中同形,按 `clazz` 分词)。**不报字符数**:用户 2026-10-26 11:16「不能表述成超长多少个字符,要表述成为文字太长难于阅读」。 */
|
|
66
|
+
const paragraphHitLine = (base, h) => `✗ ${h.clazz} ${rel(base, h.file)}:${h.line}`
|
|
67
67
|
|
|
68
68
|
/** gate 报面(固定句形——§3.20「报面(固定句形 · 交付门 gate)」):首行 verdict = 唯一机判串。 */
|
|
69
|
-
function gateReport({ base, docsSource, checkFiles, skipped, anchors, paragraphs
|
|
69
|
+
function gateReport({ base, docsSource, checkFiles, skipped, anchors, paragraphs }) {
|
|
70
70
|
const total = anchors.hits.length + paragraphs.length
|
|
71
71
|
const src = docsSource === "session" ? "本会话写过的文档" : "显式 docs"
|
|
72
|
+
// 首行:锚与段分列(用户 11:16:段的问题叫「文字太长,难于阅读」,不是「命中 N 条」也不是字符数)
|
|
73
|
+
const verdict = () => {
|
|
74
|
+
if (total === 0) return checkFiles.length ? "DOC-CHECK GATE: PASS" : "DOC-CHECK GATE: PASS(0 档)"
|
|
75
|
+
const parts = []
|
|
76
|
+
if (anchors.hits.length) parts.push(`锚 ${anchors.hits.length} 条`)
|
|
77
|
+
// 段与行分列——两类命中不得合并(提示词面一行 = 一个块,属「行太长」,不是「一段文字太长」)
|
|
78
|
+
const paraCount = paragraphs.filter((h) => h.clazz === DOC_CLASS.para).length
|
|
79
|
+
const lineCount = paragraphs.filter((h) => h.clazz === DOC_CLASS.line).length
|
|
80
|
+
if (paraCount) parts.push(`${paraCount} 段${DOC_CLASS.para}`)
|
|
81
|
+
if (lineCount) parts.push(`${lineCount} ${DOC_CLASS.line}`)
|
|
82
|
+
return `DOC-CHECK GATE: REFUSED(${parts.join(" · ")})`
|
|
83
|
+
}
|
|
72
84
|
const out = [
|
|
73
|
-
|
|
85
|
+
verdict(),
|
|
74
86
|
"文档纪律机检(交付门 · gate)",
|
|
75
87
|
checkFiles.length
|
|
76
|
-
? `范围:${src} ${checkFiles.length}
|
|
77
|
-
: `范围:${src} —— **0
|
|
78
|
-
"
|
|
88
|
+
? `范围:${src} ${checkFiles.length} 档`
|
|
89
|
+
: `范围:${src} —— **0 档受检**(空清单:本次没有判任何档,不得读作「已检」)`,
|
|
90
|
+
"判据:逐段检查——" + DOC_CLASS.para,
|
|
79
91
|
]
|
|
80
92
|
if (skipped.missing.length) out.push(`跳过 ${skipped.missing.length} 档(已不在盘,未检):${skipped.missing.map((p) => rel(base, p)).join(" ∥ ")}`)
|
|
81
93
|
if (skipped.notMarkdown.length) out.push(`跳过 ${skipped.notMarkdown.length} 档(非 markdown 档,未检):${skipped.notMarkdown.map((p) => rel(base, p)).join(" ∥ ")}`)
|
|
@@ -91,7 +103,7 @@ function gateReport({ base, docsSource, checkFiles, skipped, anchors, paragraphs
|
|
|
91
103
|
? "范围为空:显式 docs 里没有可判档——见上方跳过行"
|
|
92
104
|
: "范围为空:显式 docs 是空清单——列出要判的档后复跑"))
|
|
93
105
|
} else if (total === 0) {
|
|
94
|
-
out.push(`结论:0
|
|
106
|
+
out.push(`结论:0 段有问题(${checkFiles.length} 档全清)`)
|
|
95
107
|
} else {
|
|
96
108
|
for (const h of anchors.hits) {
|
|
97
109
|
out.push(h.clazz === "档缺"
|
|
@@ -99,7 +111,8 @@ function gateReport({ base, docsSource, checkFiles, skipped, anchors, paragraphs
|
|
|
99
111
|
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
100
112
|
}
|
|
101
113
|
for (const w of paragraphs) out.push(paragraphHitLine(base, w))
|
|
102
|
-
|
|
114
|
+
// 处置句只跟「段」命中走:仅有行太长时不得叫人对段落重写
|
|
115
|
+
if (paragraphs.some((h) => h.clazz === DOC_CLASS.para)) out.push("处置:上面这些段落,重新组织语言重写;同轮修正后复跑;不转台账")
|
|
103
116
|
}
|
|
104
117
|
return out.join("\n")
|
|
105
118
|
}
|
|
@@ -154,15 +167,15 @@ export function runDocCheck(root, { docs, mode = "report", docsSource } = {}) {
|
|
|
154
167
|
skipped: { missing: range.missing, notMarkdown: range.notMarkdown },
|
|
155
168
|
}
|
|
156
169
|
if (gate) {
|
|
157
|
-
return { ...shared, report: gateReport({ base, docsSource: source, checkFiles, skipped: shared.skipped, anchors, paragraphs
|
|
170
|
+
return { ...shared, report: gateReport({ base, docsSource: source, checkFiles, skipped: shared.skipped, anchors, paragraphs }) }
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
const out = [
|
|
161
174
|
"文档纪律机检(报告态 · 非门禁)",
|
|
162
|
-
`射程:${base} · 传入清单 ${checkFiles.length} 档 ·
|
|
175
|
+
`射程:${base} · 传入清单 ${checkFiles.length} 档 · 区带豁免:${paragraphExemptZones.length ? paragraphExemptZones.join(" ∥ ") : "无"}`,
|
|
163
176
|
"声明:判据参数 = 工具内置常量;范围 = 传入文件(判定范围 = 解析范围)",
|
|
164
177
|
`范围:传入清单 ${checkFiles.length} 档(整档入判 · 判定范围 = 解析范围)`,
|
|
165
|
-
`命中 ${hits.total} 条(锚 ${anchors.hits.length} ·
|
|
178
|
+
`命中 ${hits.total} 条(锚 ${anchors.hits.length} · ${DOC_CLASS.para} ${paragraphs.filter((h) => h.clazz === DOC_CLASS.para).length} · ${DOC_CLASS.line} ${paragraphs.filter((h) => h.clazz === DOC_CLASS.line).length})`,
|
|
166
179
|
]
|
|
167
180
|
for (const h of anchors.hits) {
|
|
168
181
|
out.push(h.clazz === "档缺"
|
|
@@ -170,7 +183,7 @@ export function runDocCheck(root, { docs, mode = "report", docsSource } = {}) {
|
|
|
170
183
|
: `✗ 锚·节缺 ${rel(base, h.file)}:${h.line} \`${h.token}\`(目标 ${rel(base, h.target)} 无 §${h.section})`)
|
|
171
184
|
}
|
|
172
185
|
for (const w of paragraphs) out.push(paragraphHitLine(base, w))
|
|
173
|
-
out.push(hits.total ? `结论:${hits.total} 条命中——报告态,不作拒执行的理由` :
|
|
186
|
+
out.push(hits.total ? `结论:${hits.total} 条命中——报告态,不作拒执行的理由` : `结论:0 条命中(锚 ∥ ${DOC_CLASS.para} ∥ ${DOC_CLASS.line} 均清)`)
|
|
174
187
|
|
|
175
188
|
return { ...shared, report: out.join("\n") }
|
|
176
189
|
}
|
package/doc-check/paragraph.mjs
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync } from "node:fs"
|
|
17
17
|
|
|
18
|
+
/** 段判据类名单源(报面与统计同取此名——改报面忘改过滤的字面漂移,2026-10-26 实发过一次)。 */
|
|
19
|
+
export const DOC_CLASS = Object.freeze({ para: "文字太长,难于阅读", line: "行太长" })
|
|
20
|
+
|
|
18
21
|
/** 表格行谓词(markdown 表格行结构性不可折行——不计入段,与锚检查同源)。 */
|
|
19
22
|
export const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(String(l))
|
|
20
23
|
|
|
@@ -62,7 +65,7 @@ export function inCodeSpan(line, idx) {
|
|
|
62
65
|
|
|
63
66
|
/** 段判据(阈值 = 调用方内置常量 `paragraphMax` ∥ `lineMax`;口径逐字 = §3.18 条目 Q):
|
|
64
67
|
* · 文档面:相邻非空正文行合并成一段,段长 = 组成行 `[...line].length` 之和 > `paragraphMax` ⇔ 命中
|
|
65
|
-
* (`clazz: "
|
|
68
|
+
* (`clazz: "文字太长,难于阅读"`,定位 = 段首行号)。正文行 = 非空 ∧ 非表格行 ∧ 非标题 ∧ 非列表项 ∧ 非引用行
|
|
66
69
|
* ∧ 非围栏(标记行与块内整行)∧ 非区带内;其余行类不计入、且把段断开。
|
|
67
70
|
* · 提示词面(`isPromptFace`):逐行裸判——`[...line].length` > `lineMax` ⇔ 命中(`clazz: "行太长"`,
|
|
68
71
|
* 无结构豁免)。
|
|
@@ -79,7 +82,7 @@ export function checkParagraphs({ paragraphMax, lineMax, exemptZones = [], files
|
|
|
79
82
|
if (isPromptFace(f)) {
|
|
80
83
|
text.split("\n").forEach((l, i) => {
|
|
81
84
|
const len = [...l].length
|
|
82
|
-
if (len > lineMax) hits.push({ file: f, line: i + 1, len, clazz:
|
|
85
|
+
if (len > lineMax) hits.push({ file: f, line: i + 1, len, clazz: DOC_CLASS.line })
|
|
83
86
|
})
|
|
84
87
|
continue
|
|
85
88
|
}
|
|
@@ -88,7 +91,7 @@ export function checkParagraphs({ paragraphMax, lineMax, exemptZones = [], files
|
|
|
88
91
|
let para = null // 在写的段 —— { start, len };非正文行 ⇒ 断开
|
|
89
92
|
const lines = text.split("\n")
|
|
90
93
|
const flush = () => {
|
|
91
|
-
if (para !== null && para.len > paragraphMax) hits.push({ file: f, line: para.start, len: para.len, clazz:
|
|
94
|
+
if (para !== null && para.len > paragraphMax) hits.push({ file: f, line: para.start, len: para.len, clazz: DOC_CLASS.para })
|
|
92
95
|
para = null
|
|
93
96
|
}
|
|
94
97
|
lines.forEach((l, i) => {
|
package/embedding.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { RETRYABLE_STATUS } from "./provider/index.mjs"
|
|
9
9
|
import { resolveEnvRefs } from "./env-ref.mjs"
|
|
10
|
+
import { withUserAgent } from "./client-identity.mjs"
|
|
10
11
|
// #859(MEMORY.md §6.3 ∥ §7 D-MEM31):嵌入路径发送前净化孤立 UTF-16 代理——sanitize 单源复用
|
|
11
12
|
import { sanitizeLoneSurrogates } from "./escape.mjs"
|
|
12
13
|
const MAX_RETRIES = 3
|
|
@@ -111,6 +112,7 @@ async function requestWithRetry(embedder, input, signal) {
|
|
|
111
112
|
response = await fetch(`${embedder.baseURL}/embeddings`, {
|
|
112
113
|
method: "POST",
|
|
113
114
|
headers: {
|
|
115
|
+
...withUserAgent(), // 产品标识头(embedding 无定制头字段——PROVIDER.md §6.17)
|
|
114
116
|
"Content-Type": "application/json",
|
|
115
117
|
Authorization: `Bearer ${embedder.apiKey}`,
|
|
116
118
|
},
|
package/generate-title.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { proxyFetch } from "./proxy.mjs"
|
|
12
|
+
import { withUserAgent } from "./client-identity.mjs"
|
|
12
13
|
import { resolveProviderSecrets } from "./env-ref.mjs"
|
|
13
14
|
// D7(2026-09-21 块标题行对齐批——标题链单源):首条真实 user 消息谓词收核单源
|
|
14
15
|
// (`isRealUserMsg`——纯函数零依赖,无环)。
|
|
@@ -97,8 +98,8 @@ export async function generateTitle(userContent, provider) {
|
|
|
97
98
|
body = JSON.stringify(bodyObj)
|
|
98
99
|
extract = (data) => data.choices?.[0]?.message?.content
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
-
const headers = { ...(provider.headers
|
|
101
|
+
// 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17)——定制头在前、内置头在后:内置头胜出;三格式各自内置头。
|
|
102
|
+
const headers = { ...withUserAgent(provider.headers), "Content-Type": "application/json" }
|
|
102
103
|
if (provider.format === "anthropic") {
|
|
103
104
|
headers["x-api-key"] = provider.apiKey
|
|
104
105
|
headers["anthropic-version"] = "2023-06-01"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-close-proof.mjs — 台账核销机检单源(F-LX4 · 2026-10-26 批 §2;台账 #1807)。
|
|
3
|
+
*
|
|
4
|
+
* 判据(需求档 §② 功能点 11 / AC-M2-34):凡写 `已核销` 的语句,写库前判三件——① 行的 `task_book`
|
|
5
|
+
* 所指批档在盘 ② 该档 §1 状态行 = `已收口` ③ 该批档在 design token 槽的批次归属里有记录;缺一即拒、
|
|
6
|
+
* **一次报全缺项**、行零改。**判据只有目标值一条**——闸挂在写这个值的两处逐行共函数
|
|
7
|
+
* (`ledger-cmd.mjs` 的 `applyRowUpdate` / `applyRowClose`)之前,不判来源态。
|
|
8
|
+
*
|
|
9
|
+
* 住本档的理由(拆分件——先例 = `ledger-tools.mjs` 自 `ledger-cmd.mjs` 拆出):`ledger-cmd.mjs` 收尾
|
|
10
|
+
* 446 行,机检本体 + 三形拒因入册即越单档 500 顾问线(设计 §五「实落超 500 ⇒ 主动拆分」)。两枚指针
|
|
11
|
+
* 值谓词(`filePartOf`——设计档 §6.1 口径 1 单源 ∥ `namesPointer`)随迁本档,`ledger-cmd` 反向 import
|
|
12
|
+
* 取用——方向单向、无静态环。
|
|
13
|
+
*
|
|
14
|
+
* 只读:③ 只读归属旁列(designId → 批档路径),**从不读 token 值那一面**(`_engDesignTokens` /
|
|
15
|
+
* `readEngTokensFromSlot` 不入本档)——拒因文案里只有行 id ∥ `task_book` 值 ∥ 批档路径。
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs"
|
|
18
|
+
|
|
19
|
+
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
20
|
+
import { batchPathKey, unionBatches } from "./agent-tools/design-slots.mjs"
|
|
21
|
+
import { isBatchRecord, readBatchStatusLine } from "./agent-tools/batch-skeleton.mjs"
|
|
22
|
+
import { loadManifest } from "./session-slots.mjs"
|
|
23
|
+
|
|
24
|
+
/** 指针 ∥ 批档值 → 文件部分(首个 `§` 前子串,trim——设计档 §6.1 口径 1 单源;`task_book` 与
|
|
25
|
+
* `batch` 同形同口径)。 */
|
|
26
|
+
export const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
|
|
27
|
+
|
|
28
|
+
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
29
|
+
export const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
30
|
+
|
|
31
|
+
/** 会话槽号(与 `token-ttl.mjs` `currentSlotNoReadOnly` 同一取值式:`_slot` 优先、未钉槽读 manifest
|
|
32
|
+
* `active`)——只用于 ③ 拒因分辨两种成因,不读任何 token 值面。 */
|
|
33
|
+
function slotNoOf(agent) {
|
|
34
|
+
if (Number.isInteger(agent?._slot)) return agent._slot
|
|
35
|
+
const active = loadManifest(agent?.cwd)?.active
|
|
36
|
+
return Number.isInteger(active) ? active : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** ③ 归属并集一次读入(严格形):`fnName` 在场 ⇒ 读失败即抛(必写 `已核销` 的批量径 ⇒ BEGIN 之前整批拒、
|
|
40
|
+
* 不进 skipped);缺省 ⇒ 返回错误对象(单行径 ∥ 纯字段补丁逐行报 ③)。 */
|
|
41
|
+
export function readCloseOutBatches(agent, fnName) {
|
|
42
|
+
try { return unionBatches(agent, { strict: true }) } catch (e) {
|
|
43
|
+
if (!fnName) return e
|
|
44
|
+
throw new Error(`${fnName}:核销机检未过(整批零行改——会话级读失败,与行无关)\n ③ 读不到 token 槽归属(${e?.message ?? e}):核销被拒、行零改;修好槽文件或权限后重试`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 核销机检(F-LX4 · 需求档 §② 功能点 11 / AC-M2-34):写 `已核销` 前判三件——① `task_book` 所指档在盘
|
|
49
|
+
* ② 该档 §1 = `已收口` ③ 该批档在 design token 槽的批次归属里有记录;**判据只有目标值一条**(两处写
|
|
50
|
+
* 这个值的 SQL 之前同挂),三件各判、**一次报全缺项**、行零改(只判不写)。③ = 值扫(`batchPathKey`
|
|
51
|
+
* 归一),只读归属旁列、从不读 token 值面。`batches` = 批量径预读(`Error` = 预读失败);缺省 ⇒ 此处读。 */
|
|
52
|
+
export function assertCloseOutProof(fnName, { agent, base, row, batches }) {
|
|
53
|
+
const missing = [] // 每项 = 一条拒因行(首字 = 缺项号)
|
|
54
|
+
let abs = null
|
|
55
|
+
if (!namesPointer(row?.task_book)) {
|
|
56
|
+
missing.push(" ① 行无 task_book 指针:追认核销须先挂批档(update 写 task_book),再跑全链")
|
|
57
|
+
} else {
|
|
58
|
+
const hit = resolveDeclaredRef(base, filePartOf(row.task_book))
|
|
59
|
+
if (!hit.ok) missing.push(` ① 批档不在盘(task_book = ${row.task_book}):先补指针,或恢复该档`)
|
|
60
|
+
else {
|
|
61
|
+
abs = hit.abs
|
|
62
|
+
const src = readFileSync(abs, "utf8")
|
|
63
|
+
if (!isBatchRecord(src)) missing.push(` ② 指针所指档不是批档(${abs} 无 §1 段):先把 task_book 改指批档`)
|
|
64
|
+
else {
|
|
65
|
+
const state = readBatchStatusLine(src)
|
|
66
|
+
if (state !== "closed") missing.push(` ② 批档未收口(${abs} §1 = ${state}):该批须先跑全链收口`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (abs !== null) {
|
|
71
|
+
let map = batches instanceof Map || batches instanceof Error ? batches : readCloseOutBatches(agent)
|
|
72
|
+
if (map instanceof Error) missing.push(` ③ 读不到 token 槽归属(${map?.message ?? map}):核销被拒、行零改;修好槽文件或权限后重试`)
|
|
73
|
+
else if (![...map].some(([, b]) => typeof b === "string" && batchPathKey(b) === batchPathKey(abs))) {
|
|
74
|
+
missing.push(slotNoOf(agent) == null
|
|
75
|
+
? " ③ 本会话无槽号(manifest 无 active):核销须在持有该批归属的会话里跑"
|
|
76
|
+
: " ③ 本会话槽内无该批档的归属:先确认是否换了会话——在跑该批设计评审的会话里核销;\n 确无归属 ⇒ 那次评审的 documents 未带该批档,须重跑")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (missing.length > 0) throw new Error(`${fnName}:核销机检未过(行 ${row?.id} 零改)——缺:${missing.map((t) => t.trim()[0]).join(" ")}\n${missing.join("\n")}`)
|
|
80
|
+
}
|
package/ledger-cmd.mjs
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* 核销标记门(LIGHT-CHANNEL.md §2.5 · 台账 #1780):写入 `已核销` 的三径(close 单行 ∥ close 批量 ∥
|
|
16
16
|
* update 迁移——均经两逐行共函数)判**结果行标题**携 `ROUND_MARKER` ⇒ 拒;判据只认标题标记
|
|
17
17
|
* (不查批档 ∥ 不查评审实跑),撤回(→ 已废弃)不拦。
|
|
18
|
+
* 核销机检(F-LX4 · 2026-10-26 批 §2):同一两逐行共函数在写 `已核销` 前判三件(批档在盘 ∥ §1 =
|
|
19
|
+
* 已收口 ∥ 该批档在 design token 槽的批次归属有记录),缺一即拒、一次报全缺项、行零改;判据只有
|
|
20
|
+
* 目标值一条。本体住拆分件 `ledger-close-proof.mjs`(同 `ledger-tools.mjs` 先例);标记门在前
|
|
21
|
+
* (零成本字符串判先报),两门互不替代。
|
|
18
22
|
*
|
|
19
23
|
* 接线(设计档 §2.2 命令面接线):族出口 = `ledger.mjs`(re-export 两源——KD-M2-3);查询命令 ←
|
|
20
24
|
* tools/index.mjs(assembleBuiltinTools 全角色面);写命令 ← agent/family-tools.mjs(assembleFamilyTools
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
import { resolve } from "node:path"
|
|
25
29
|
import { AGING_DAYS, ALLOWED_MIGRATIONS, EVIDENCE_SOFT_LIMIT, nowIso, openLedger, PENDING_STATUSES, resolveLedgerRoot, ROUND_MARKER } from "./ledger-db.mjs"
|
|
26
30
|
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
31
|
+
import { assertCloseOutProof, filePartOf, namesPointer, readCloseOutBatches } from "./ledger-close-proof.mjs"
|
|
27
32
|
|
|
28
33
|
/** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
|
|
29
34
|
* 行龄 > `AGING_DAYS` 天;行龄源 = `updated_at ?? created_at`(§5 同源);时间戳缺 / 不可解析
|
|
@@ -36,13 +41,9 @@ function isRowAged(row, now) {
|
|
|
36
41
|
return Number.isFinite(ms) && (now - ms) / 86400000 > AGING_DAYS
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
/**
|
|
40
|
-
* `
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd)?.root
|
|
44
|
-
* ?? resolve(cwd ?? ".")`。 */
|
|
45
|
-
const baseOf = (cwd) => resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
|
|
44
|
+
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd, boundary)?.root
|
|
45
|
+
* ?? resolve(cwd ?? ".")`。`boundary` = 会话锚(与 `openLedger` 同一枚上界——同一笔动作的判据不能两界)。 */
|
|
46
|
+
const baseOf = (cwd, boundary) => resolveLedgerRoot(cwd, boundary)?.root ?? resolve(cwd ?? ".")
|
|
46
47
|
|
|
47
48
|
/** `evidence` 超限读数(设计档 §5 P4——**读数非闸门**):缺 / 空 ⇒ false;`query` 逐行携(含归档行——
|
|
48
49
|
* 行值事实);`count` 的统计集 = 未决四态(§7.1)。 */
|
|
@@ -92,8 +93,8 @@ export function overdueOf(row, { now = Date.now(), base = process.cwd() } = {})
|
|
|
92
93
|
* (§3.7 出口态字串 ∥ null——机判单源 `overdueOf`);`task_book` = 第 5 枚等值过滤(**原值等值**——列串直比,与 §3.3 选行的
|
|
93
94
|
* 「文件部分解析同址」两径口径不同、各自单源);`trigger` 过滤仅三枚举等值(`NULL` 不过滤);
|
|
94
95
|
* `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 })
|
|
96
|
+
export function ledgerQuery({ cwd, boundary, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
|
|
97
|
+
const db = openLedger(cwd, { readOnly: true, boundary })
|
|
97
98
|
if (!db) return []
|
|
98
99
|
try {
|
|
99
100
|
const where = [], args = []
|
|
@@ -103,7 +104,7 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
|
|
|
103
104
|
if (trigger) { where.push("trigger = ?"); args.push(trigger) }
|
|
104
105
|
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
105
106
|
const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
|
|
106
|
-
const base = baseOf(cwd)
|
|
107
|
+
const base = baseOf(cwd, boundary)
|
|
107
108
|
return rows.map((r) => ({ ...r, aged: isRowAged(r, now), evidenceOversize: isEvidenceOversize(r), taskBookOnDisk: pointerOnDisk(r, base), overdue: overdueOf(r, { now, base }) }))
|
|
108
109
|
} finally { db.close() }
|
|
109
110
|
}
|
|
@@ -113,14 +114,14 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
|
|
|
113
114
|
* 机判单源);`evidenceOversize` = 其中 `evidence` 超限行数(§5)。**一次 SELECT + 逐行判**——
|
|
114
115
|
* `evidenceOversize` 与 `overdue` 同一次遍历(零额外 IO;成本模型 = §7.2);不设缓存(读数须反映
|
|
115
116
|
* 即时盘面)。库不在 = 三零。 */
|
|
116
|
-
export function ledgerCount({ cwd, task_book = null } = {}) {
|
|
117
|
-
const db = openLedger(cwd, { readOnly: true })
|
|
117
|
+
export function ledgerCount({ cwd, boundary, task_book = null } = {}) {
|
|
118
|
+
const db = openLedger(cwd, { readOnly: true, boundary })
|
|
118
119
|
if (!db) return { count: 0, overdue: 0, evidenceOversize: 0 }
|
|
119
120
|
try {
|
|
120
121
|
const where = [`status IN (${PENDING_STATUSES.map(() => "?").join(",")})`], args = [...PENDING_STATUSES]
|
|
121
122
|
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
122
123
|
const rows = db.prepare(`SELECT * FROM items WHERE ${where.join(" AND ")}`).all(...args)
|
|
123
|
-
const now = Date.now(), base = baseOf(cwd)
|
|
124
|
+
const now = Date.now(), base = baseOf(cwd, boundary)
|
|
124
125
|
let overdue = 0, oversize = 0
|
|
125
126
|
for (const r of rows) {
|
|
126
127
|
if (overdueOf(r, { now, base }) !== null) overdue++
|
|
@@ -132,13 +133,10 @@ export function ledgerCount({ cwd, task_book = null } = {}) {
|
|
|
132
133
|
|
|
133
134
|
/** `evidence` 超限行数(设计档 §7.1 导出面——`count` 回报读数**同一单源**:本函数为其薄委托,
|
|
134
135
|
* 不另立第二判定面)。 */
|
|
135
|
-
export function ledgerEvidenceOversize({ cwd, task_book = null } = {}) {
|
|
136
|
-
return ledgerCount({ cwd, task_book }).evidenceOversize
|
|
136
|
+
export function ledgerEvidenceOversize({ cwd, boundary, task_book = null } = {}) {
|
|
137
|
+
return ledgerCount({ cwd, boundary, task_book }).evidenceOversize
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
140
|
-
const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
141
|
-
|
|
142
140
|
/** 写门·指针点名(设计档 §6.1 两臂 · AC-M2-10 ∥ AC-M2-29):判**本笔写入面**——两臂之外零判。
|
|
143
141
|
* 臂①(点名非空):结果行 `status ∈ {在途, 待核销}` ∨ 结果行 `trigger = 归批` ⇒ `task_book` 非空
|
|
144
142
|
* (缺 / 空串 / 全空白 ⇒ 拒「必填」);**不判在盘**(列值事实,不随盘面翻转——DDL 咬合 CHECK 为该半幅防线)。
|
|
@@ -149,7 +147,7 @@ const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
|
149
147
|
* 判位 = 与迁移表判同层(落盘前);文案前缀 = 调用函数名;基准 = §6.1 口径 2(根推导单源同子表达式)。
|
|
150
148
|
* @param {{status:string, trigger:string|null, taskBook:string|null, wroteTaskBook:boolean, wroteBatchTrigger:boolean}} face 本笔写入面
|
|
151
149
|
*/
|
|
152
|
-
function assertTaskBookGate(cwd, fnName, face) {
|
|
150
|
+
function assertTaskBookGate(cwd, fnName, face, boundary) {
|
|
153
151
|
const { status, trigger, taskBook, wroteTaskBook, wroteBatchTrigger } = face
|
|
154
152
|
if ((status === "在途" || status === "待核销" || trigger === "归批") && !namesPointer(taskBook)) {
|
|
155
153
|
throw new Error(`${fnName}:task_book 必填(在途 / 待核销 / 归批 须携任务书指针)`)
|
|
@@ -157,14 +155,14 @@ function assertTaskBookGate(cwd, fnName, face) {
|
|
|
157
155
|
if (!wroteTaskBook && !wroteBatchTrigger) return // 臂② 之外零判:旧指针所指档已清不阻任何此类写入
|
|
158
156
|
const part = filePartOf(taskBook)
|
|
159
157
|
if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
|
|
160
|
-
const base = baseOf(cwd) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
|
|
158
|
+
const base = baseOf(cwd, boundary) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
|
|
161
159
|
const { ok, abs } = resolveDeclaredRef(base, part)
|
|
162
160
|
if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
/** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。`row.resume_when` 可空(§3.4)。 */
|
|
166
|
-
export function ledgerAdd({ cwd, row }) {
|
|
167
|
-
const db = openLedger(cwd, { create: true })
|
|
164
|
+
export function ledgerAdd({ cwd, row, boundary }) {
|
|
165
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
168
166
|
try {
|
|
169
167
|
// 结果行状态 = INSERT 硬编码「待讨论」⇒ 臂① 的 `status` 半幅当下不触;`trigger=归批` 半幅与臂② 照判
|
|
170
168
|
assertTaskBookGate(cwd, "ledgerAdd", {
|
|
@@ -173,7 +171,7 @@ export function ledgerAdd({ cwd, row }) {
|
|
|
173
171
|
taskBook: row.task_book ?? null,
|
|
174
172
|
wroteTaskBook: namesPointer(row.task_book),
|
|
175
173
|
wroteBatchTrigger: row.trigger === "归批",
|
|
176
|
-
})
|
|
174
|
+
}, boundary)
|
|
177
175
|
const now = nowIso()
|
|
178
176
|
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
177
|
.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)
|
|
@@ -214,11 +212,11 @@ function assertEvidenceValue(fnName, evidence) {
|
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
/** 单行落笔·update(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):迁移表判 → 核销标记门
|
|
217
|
-
* (目标 = `已核销` ∧ 结果标题携标记 ⇒ 拒——判序 = LIGHT-CHANNEL.md §2.5)→
|
|
218
|
-
*
|
|
215
|
+
* (目标 = `已核销` ∧ 结果标题携标记 ⇒ 拒——判序 = LIGHT-CHANNEL.md §2.5)→ **核销机检**(目标 = `已核销`
|
|
216
|
+
* ⇒ `assertCloseOutProof`)→ executor / evidence 值计算(含两拒面)→ 写门 → UPDATE(判序 = §13.9 逐字未变)。`to` = `patch.status ?? 行现态`
|
|
219
217
|
* ——批量形纯字段补丁(不携 `status`)时 `to === row.status` ⇒ 迁移表不判、`status` 零写(§3.3)。
|
|
220
218
|
* `resume_when` = 普通可空字段补丁(`patch.resume_when ?? 行值`——§3.4)。 */
|
|
221
|
-
function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
219
|
+
function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary, proof = {}) {
|
|
222
220
|
const to = patch.status ?? row.status
|
|
223
221
|
if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
|
|
224
222
|
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
@@ -227,6 +225,8 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
|
227
225
|
if (to === "已核销" && String(patch.title ?? row.title ?? "").includes(ROUND_MARKER)) {
|
|
228
226
|
throw new Error(`ledgerUpdate:行标题仍携「${ROUND_MARKER}」——收尾链未跑完,不许核销(先跑收尾全链,再由父侧清标记)`)
|
|
229
227
|
}
|
|
228
|
+
// 核销机检(F-LX4):目标值 = `已核销` ⇒ 写库前三件校验(批档在盘 ∥ §1 已收口 ∥ 槽内有归属)
|
|
229
|
+
if (to === "已核销") assertCloseOutProof("ledgerUpdate", { agent: proof.agent, base: proof.base ?? baseOf(cwd, boundary), row, batches: proof.batches })
|
|
230
230
|
const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
|
|
231
231
|
const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
|
|
232
232
|
assertEvidenceValue("ledgerUpdate", patch.evidence)
|
|
@@ -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,27 +249,30 @@ 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, agent }) {
|
|
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, { agent, base: baseOf(cwd, boundary) })
|
|
258
258
|
return { id: rowId }
|
|
259
259
|
} finally { db.close() }
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/** 单行落笔·close(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):源态判(核销仅三源——
|
|
263
263
|
* 在途不可跳)→ 核销标记门(LIGHT-CHANNEL.md §2.5——`已核销` ∧ 标题携标记 ⇒ 拒;撤回不拦)→
|
|
264
|
-
* 值计算(拒面① 旗独传判于此)→ 追认口 `evidence`
|
|
264
|
+
* **核销机检**(`已核销` ⇒ `assertCloseOutProof`)→ 值计算(拒面① 旗独传判于此)→ 追认口 `evidence`
|
|
265
|
+
* 门 → 拒面② → UPDATE。
|
|
265
266
|
* 撤回(任意态 → 已废弃——含在途直撤)同步 `executor = NULL`(LEDGER.md §3.1 ④);核销两源(勾销 /
|
|
266
267
|
* 追认)`executor` 零触碰(非在途出边)。 */
|
|
267
|
-
function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
268
|
+
function applyRowClose(db, row, status, evidence, evidenceReplace, proof = {}) {
|
|
268
269
|
if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
|
|
269
270
|
// 核销标记门(LIGHT-CHANNEL.md §2.5):勾销 ∥ 追认同判;撤回(→ 已废弃)不入本门
|
|
270
271
|
if (status === "已核销" && String(row.title ?? "").includes(ROUND_MARKER)) {
|
|
271
272
|
throw new Error(`ledgerClose:行标题仍携「${ROUND_MARKER}」——收尾链未跑完,不许核销(先跑收尾全链,再由父侧清标记)`)
|
|
272
273
|
}
|
|
274
|
+
// 核销机检(F-LX4):判序 = 标记门之后(零成本字符串判先报)、值计算之前
|
|
275
|
+
if (status === "已核销") assertCloseOutProof("ledgerClose", { agent: proof.agent, base: proof.base, row, batches: proof.batches })
|
|
273
276
|
const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
|
|
274
277
|
if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
|
|
275
278
|
assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
|
|
@@ -283,18 +286,18 @@ function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
|
283
286
|
* 可选 `evidence` 参:传值 = 追加缺省(§13.9——旧空直写 / 旧非空拼 `旧|新`)、`evidenceReplace: true`
|
|
284
287
|
* 同传 = 整段替换;勾销 / 追认 / 撤回三径同携(写值落行,内容不判);两拒面(旗独传 @值计算 ∥
|
|
285
288
|
* 新值空白 @追认门后)见 `evidenceValue` / `assertEvidenceValue`。
|
|
286
|
-
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 →
|
|
287
|
-
* 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3
|
|
288
|
-
*
|
|
289
|
-
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
|
|
289
|
+
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → **核销机检**(`已核销` 才触)→ 值计算 →
|
|
290
|
+
* 追认门(原样——判**结果值**缺 / 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3
|
|
291
|
+
* 收口两源)。**单行形**(`id`)——逐行落笔 = `applyRowClose`。 */
|
|
292
|
+
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, boundary, agent }) {
|
|
290
293
|
if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
|
|
291
|
-
const db = openLedger(cwd, { create: true })
|
|
294
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
292
295
|
try {
|
|
293
296
|
db.exec("BEGIN")
|
|
294
297
|
try {
|
|
295
298
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
296
299
|
if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
|
|
297
|
-
const out = applyRowClose(db, row, status, evidence, evidenceReplace)
|
|
300
|
+
const out = applyRowClose(db, row, status, evidence, evidenceReplace, { agent, base: baseOf(cwd, boundary) })
|
|
298
301
|
db.exec("COMMIT")
|
|
299
302
|
return out
|
|
300
303
|
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
@@ -330,16 +333,20 @@ function resolveBatchRows(db, base, abs) {
|
|
|
330
333
|
* 并抛(零行改)——故「部分落地」只由逐行门拒产生,「中途失败」不留半批。
|
|
331
334
|
* `BEGIN IMMEDIATE`:写锁一次先把(逐行不升级锁 ⇒ 事务级失败在首行之前即失败)。
|
|
332
335
|
* @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 })
|
|
336
|
+
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, boundary, agent }) {
|
|
337
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
335
338
|
try {
|
|
336
|
-
const base = baseOf(cwd)
|
|
339
|
+
const base = baseOf(cwd, boundary)
|
|
337
340
|
const abs = assertBatchArg("ledgerUpdate", base, batch)
|
|
341
|
+
// 核销机检 ③:迁移径(结果态 = `已核销`)在 BEGIN 之前一次读入、行间复用(严格读抛 = 会话级 ⇒ 整批拒);
|
|
342
|
+
// 纯字段补丁径也读一次(不抛):闸只对归档行触(存量已知接受),读失败向每行报 ③ 形——不改业务结果
|
|
343
|
+
// (那些行本就被拦),但会以逐行拒因呈现环境问题(父侧已知项,待裁)。
|
|
344
|
+
const proof = { agent, base, batches: patch.status === "已核销" ? readCloseOutBatches(agent, "ledgerUpdate") : patch.status == null ? readCloseOutBatches(agent) : undefined }
|
|
338
345
|
db.exec("BEGIN IMMEDIATE")
|
|
339
346
|
try {
|
|
340
347
|
const applied = [], skipped = []
|
|
341
348
|
for (const row of resolveBatchRows(db, base, abs)) {
|
|
342
|
-
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId).id) }
|
|
349
|
+
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary, proof).id) }
|
|
343
350
|
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
344
351
|
}
|
|
345
352
|
db.exec("COMMIT")
|
|
@@ -355,12 +362,14 @@ export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId })
|
|
|
355
362
|
* (`evidenceValue` 同源)。回执 `status` 恒 `已核销`(批量形缺省 = 勾销单值;携则守卫已收窄为
|
|
356
363
|
* `{已核销}`——§3.2 `whenPresent`)。事务语义同 `ledgerUpdateBatch`。
|
|
357
364
|
* @returns {{batch: string, status: string, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
358
|
-
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace }) {
|
|
365
|
+
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, boundary, agent }) {
|
|
359
366
|
const status = "已核销"
|
|
360
|
-
const db = openLedger(cwd, { create: true })
|
|
367
|
+
const db = openLedger(cwd, { create: true, boundary })
|
|
361
368
|
try {
|
|
362
|
-
const base = baseOf(cwd)
|
|
369
|
+
const base = baseOf(cwd, boundary)
|
|
363
370
|
const abs = assertBatchArg("ledgerClose", base, batch)
|
|
371
|
+
// 核销机检 ③:本径恒写 `已核销` ⇒ 归属并集在 BEGIN 之前一次读入(严格读抛 = 会话级 ⇒ 整批拒、不进 skipped)
|
|
372
|
+
const proof = { agent, base, batches: readCloseOutBatches(agent, "ledgerClose") }
|
|
364
373
|
db.exec("BEGIN IMMEDIATE")
|
|
365
374
|
try {
|
|
366
375
|
const applied = [], skipped = []
|
|
@@ -371,7 +380,7 @@ export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace }) {
|
|
|
371
380
|
skipped.push({ id: Number(row.id), from: row.status, why })
|
|
372
381
|
continue
|
|
373
382
|
}
|
|
374
|
-
try { applied.push(applyRowClose(db, row, status, evidence, evidenceReplace).id) }
|
|
383
|
+
try { applied.push(applyRowClose(db, row, status, evidence, evidenceReplace, proof).id) }
|
|
375
384
|
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
376
385
|
}
|
|
377
386
|
db.exec("COMMIT")
|
|
@@ -387,13 +396,14 @@ const REANCHOR_STATUSES = ["待讨论", "待设计", "在途"]
|
|
|
387
396
|
|
|
388
397
|
/** 本批行集(只读,任意状态——设计档 §7.1):批身份单源 = `resolveBatchRows` 解析同址(零二写);
|
|
389
398
|
* 逐行携读数 `taskBookOnDisk`(不入库、不入 `DATA_COLUMNS`)。`batch` 不可解析 ∥ 无根 ⇒ 抛(同族
|
|
390
|
-
* `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd`
|
|
391
|
-
|
|
392
|
-
|
|
399
|
+
* `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd` 基准——不用会话锚);
|
|
400
|
+
* `boundary` = 上溯上界(会话锚——与工具面同一枚界;缺省 = 进程 cwd)。 */
|
|
401
|
+
export function ledgerBatchRows({ cwd, batch, boundary } = {}) {
|
|
402
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
|
|
393
403
|
if (!derived) throw new Error(`ledgerBatchRows:批档所属项目根不可推导(${cwd ?? "(缺 cwd)"})`)
|
|
394
404
|
const base = derived.root
|
|
395
405
|
const abs = assertBatchArg("ledgerBatchRows", base, batch) // 失败语义先于「库不在 ⇒ 空集」(不可解析就是不可解析)
|
|
396
|
-
const db = openLedger(base, { readOnly: true })
|
|
406
|
+
const db = openLedger(base, { readOnly: true, boundary })
|
|
397
407
|
if (!db) return []
|
|
398
408
|
try {
|
|
399
409
|
return resolveBatchRows(db, base, abs).map((r) => ({ ...r, taskBookOnDisk: pointerOnDisk(r, base) }))
|
|
@@ -406,14 +416,15 @@ export function ledgerBatchRows({ cwd, batch } = {}) {
|
|
|
406
416
|
* 复句追加 `evidence`。单事务:任一步失败 ⇒ 整集 `ROLLBACK`(零行改)。**不抛**——失败经返回形 `failed`
|
|
407
417
|
* 报出(`null` 常态);`cwd` 无根 ⇒ 不取行、不重锚(**不落兜底基准**——落了就是静默零命中)。
|
|
408
418
|
* `updated_at` 零触(机器重锚不是人工推进的痕迹——行龄读数照旧)。
|
|
419
|
+
* `boundary` = 上溯上界(会话锚——与工具面 / `ledgerBatchRows` 同一枚界;缺省 = 进程 cwd)。
|
|
409
420
|
* @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 } = {}) {
|
|
421
|
+
export function ledgerReanchorBatch({ cwd, batch, boundary } = {}) {
|
|
411
422
|
const failedResult = (why) => ({ batch, reanchored: [], alreadyAnchored: [], failed: why })
|
|
412
|
-
const derived = cwd == null ? null : resolveLedgerRoot(cwd)
|
|
423
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd, boundary)
|
|
413
424
|
if (!derived) return failedResult("批档所属项目根不可推导")
|
|
414
425
|
const base = derived.root
|
|
415
426
|
let db
|
|
416
|
-
try { db = openLedger(base) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
|
|
427
|
+
try { db = openLedger(base, { boundary }) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
|
|
417
428
|
if (!db) return { batch, reanchored: [], alreadyAnchored: [], failed: null } // 库不在 ⇒ 零行可锚
|
|
418
429
|
try {
|
|
419
430
|
let abs
|