@thincoder/core 0.10.8 → 0.10.9
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 +6 -0
- package/agent/family-tools.mjs +2 -2
- package/agent/helpers.mjs +6 -6
- package/agent/post-turn.mjs +34 -0
- package/agent/run-stages.mjs +43 -9
- package/agent/setup.mjs +10 -2
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/audit-block.mjs +4 -2
- package/agent-tools/batch-gate.mjs +198 -0
- package/agent-tools/batch-lifecycle.mjs +216 -36
- package/agent-tools/batch-skeleton.mjs +87 -0
- package/agent-tools/batch.mjs +11 -3
- package/agent-tools/context.mjs +28 -5
- package/agent-tools/design-slots.mjs +7 -33
- package/agent-tools/doc-check.mjs +44 -6
- package/agent-tools/spawn-gates.mjs +100 -41
- package/agent-tools/ssh-session.mjs +108 -61
- package/agent-tools/ssh-transport.mjs +17 -4
- package/agent-tools/ssh-vault.mjs +291 -0
- package/agent-tools/subagent-spawn.mjs +18 -11
- package/agent-tools/subagent.mjs +13 -7
- package/agent-tools/timer.mjs +2 -2
- package/config-defaults.mjs +92 -0
- package/config-normalize.mjs +109 -0
- package/config.mjs +88 -197
- package/doc-check/anchors.mjs +54 -12
- package/doc-check/index.mjs +161 -12
- package/doc-check/version.mjs +84 -0
- package/doc-check/width.mjs +5 -4
- package/i18n.mjs +2 -0
- package/ledger-cmd.mjs +314 -61
- package/ledger-db.mjs +22 -10
- package/ledger-migrate.mjs +15 -9
- package/ledger-tools.mjs +115 -69
- package/ledger.mjs +13 -10
- package/log.mjs +2 -2
- package/manifest-schema.mjs +15 -1
- package/package.json +1 -1
- package/permission.mjs +7 -1
- package/prompts/advisor-design.md +21 -21
- package/prompts/advisor-round1.md +18 -18
- package/prompts/advisor-round2.md +22 -22
- package/prompts/advisor-round3.md +24 -24
- package/prompts/common.md +108 -93
- package/prompts/consult-base.md +8 -8
- package/prompts/discipline-engineering.md +68 -65
- package/prompts/discipline-normal.md +69 -68
- package/prompts/persona-coder.md +6 -6
- package/prompts/persona-eng-coder.md +19 -18
- package/prompts/persona-eng-designer.md +36 -32
- package/prompts/persona-engineering.md +114 -98
- package/prompts/persona-explore.md +2 -2
- package/prompts/persona-normal.md +16 -16
- package/prompts/persona-plan.md +7 -7
- package/prompts/subagent-base.md +4 -4
- package/provider/anthropic.mjs +4 -2
- package/provider/core.mjs +4 -2
- package/provider/google.mjs +5 -1
- package/provider/responses-request.mjs +3 -2
- package/session-lifecycle.mjs +6 -6
- package/team.mjs +2 -1
- package/token-window.mjs +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/doc_check.md +30 -7
- package/tool-docs/fetch.md +3 -2
- package/tool-docs/ledger.md +7 -5
- package/tool-docs/ssh.md +23 -6
- package/tool-docs/subagent.md +6 -11
- package/tool-docs/websearch.md +3 -1
- package/tools/ssh.mjs +24 -11
- package/tools/web-deepseek.mjs +172 -0
- package/tools/web.mjs +29 -4
package/ledger-cmd.mjs
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* ledger-cmd.mjs — 台账核心函数档(查询 = 只读全角色 / 写 = 仅主 agent 装配)。
|
|
3
3
|
* 工具定义 + 工具层参数守卫 = 拆分件 `thincoder-core/ledger-tools.mjs`(2026-09-28 守卫族微修批 ·
|
|
4
4
|
* 台账 #473——纯搬移、语义零变;拆分触发 = 本档 298 行贴 300 顾问线,本批读面守卫增量必越线);
|
|
5
|
-
* 核函数(`ledgerQuery` / `ledgerCount` / `ledgerAdd` / `ledgerUpdate` / `ledgerClose
|
|
6
|
-
*
|
|
5
|
+
* 核函数(`ledgerQuery` / `ledgerCount` / `ledgerAdd` / `ledgerUpdate` / `ledgerClose` + 批级批量
|
|
6
|
+
* `ledgerUpdateBatch` / `ledgerCloseBatch`)留本档,工具档消费之。
|
|
7
|
+
*
|
|
8
|
+
* 读数面(逐行携、不入库、不入 `DATA_COLUMNS`——先例 = `aged`):`evidenceOversize`(§5 P4)∥
|
|
9
|
+
* `overdue`(§3.7 出口态——机判单源 `overdueOf`)∥ `taskBookOnDisk`(§7.1 B——指针在盘读数)。
|
|
10
|
+
* 三枚皆**读数非闸门**(不拒任何写入)。
|
|
11
|
+
* 指针判据重锚面(§6.1 两臂写门 ∥ §6.2 收口重锚 · 台账 #1550):写门判**本笔写入面**、只判本笔新指名;
|
|
12
|
+
* 「档此刻在不在盘」降为读数——读面导出 `ledgerBatchRows`、收口重锚单源 `ledgerReanchorBatch`。
|
|
13
|
+
* 批量面(§3.3 P1):选行单源 `resolveBatchRows` + 逐行落笔 `applyRowUpdate` / `applyRowClose`
|
|
14
|
+
* (与单行形共函数——迁移表判 / 写门 / evidence 门逐行照判)。
|
|
7
15
|
*
|
|
8
16
|
* 接线(设计档 §2.2 命令面接线):族出口 = `ledger.mjs`(re-export 两源——KD-M2-3);查询命令 ←
|
|
9
17
|
* tools/index.mjs(assembleBuiltinTools 全角色面);写命令 ← agent/family-tools.mjs(assembleFamilyTools
|
|
@@ -11,7 +19,7 @@
|
|
|
11
19
|
* 消费侧静态 import 会破 W8 契约②)。
|
|
12
20
|
*/
|
|
13
21
|
import { resolve } from "node:path"
|
|
14
|
-
import { AGING_DAYS, ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES, resolveLedgerRoot } from "./ledger-db.mjs"
|
|
22
|
+
import { AGING_DAYS, ALLOWED_MIGRATIONS, EVIDENCE_SOFT_LIMIT, nowIso, openLedger, PENDING_STATUSES, resolveLedgerRoot } from "./ledger-db.mjs"
|
|
15
23
|
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
16
24
|
|
|
17
25
|
/** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
|
|
@@ -25,11 +33,63 @@ function isRowAged(row, now) {
|
|
|
25
33
|
return Number.isFinite(ms) && (now - ms) / 86400000 > AGING_DAYS
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
/** 指针 ∥ 批档值 → 文件部分(首个 `§` 前子串,trim——设计档 §6.1 口径 1 单源;`task_book` 与
|
|
37
|
+
* `batch` 同形同口径)。 */
|
|
38
|
+
const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
|
|
39
|
+
|
|
40
|
+
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd)?.root
|
|
41
|
+
* ?? resolve(cwd ?? ".")`。 */
|
|
42
|
+
const baseOf = (cwd) => resolveLedgerRoot(cwd)?.root ?? resolve(cwd ?? ".")
|
|
43
|
+
|
|
44
|
+
/** `evidence` 超限读数(设计档 §5 P4——**读数非闸门**):缺 / 空 ⇒ false;`query` 逐行携(含归档行——
|
|
45
|
+
* 行值事实);`count` 的统计集 = 未决四态(§7.1)。 */
|
|
46
|
+
const isEvidenceOversize = (row) => row?.evidence != null && String(row.evidence).length > EVIDENCE_SOFT_LIMIT
|
|
47
|
+
|
|
48
|
+
/** 本地日界(`YYYY-MM-DD`)——`resume_due` 比较粒度(设计档 §3.7:本地日界,当日锚当日即到期)。 */
|
|
49
|
+
const localDay = (ms) => { const d = new Date(ms); const p = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` }
|
|
50
|
+
|
|
51
|
+
/** 复启锚判定(设计档 §3.4「锚判定文法」单源):从左到右取首个 `@` 起始 token——
|
|
52
|
+
* ① 形合 `^\d{4}-\d{2}-\d{2}$` ∧ 真日历日(回写一致)⇒ 日期锚;② 含 `@` 而非日期锚 ⇒ 事件锚
|
|
53
|
+
* (fail-safe:机器不判 due,零假阳);③ 无 `@` token ⇒ `free`。 */
|
|
54
|
+
function resumeAnchor(text) {
|
|
55
|
+
const m = /@(\S+)/.exec(String(text ?? "")) // token = 「`@` 后非空白连续串」(零长 `@` 不算 token ⇒ free)
|
|
56
|
+
if (!m) return { kind: "free", day: null }
|
|
57
|
+
const tok = m[1]
|
|
58
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(tok)) {
|
|
59
|
+
const ms = Date.parse(`${tok}T00:00:00`)
|
|
60
|
+
if (Number.isFinite(ms) && localDay(ms) === tok) return { kind: "date", day: tok }
|
|
61
|
+
}
|
|
62
|
+
return { kind: "event", day: null }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 指针在盘判(设计档 §7.1 B 判定本体):`query` 逐行读数键 `taskBookOnDisk` 与出口态 `dangling`
|
|
66
|
+
* **共用同一判据**——解析成立(= 档在盘)⇒ true;缺指针 / 不可解析 / 档不在盘 ⇒ false。**读数非闸门**。 */
|
|
67
|
+
const pointerOnDisk = (row, base) => { const part = filePartOf(row?.task_book); return part !== "" && resolveDeclaredRef(base, part).ok }
|
|
68
|
+
|
|
69
|
+
/** 出口面机判单源(设计档 §3.7 ①——只由行数据 + 盘面事实判;射程 = 未决四态,归档态恒 `null`):
|
|
70
|
+
* 四类出口态**首中优先**(序 = `resume_due` → `dangling` → `unlabeled` → `aged`);返回出口态字串
|
|
71
|
+
* ∥ `null`。`unlabeled`(`trigger IS NULL` ∧ 超龄)⊆ `aged` ⇒ 必先于 `aged` 判(否则永不返回)。
|
|
72
|
+
* `resume_due` 比较粒度 = 本地日界(锚日 ≤ 当地日 ⇒ 到期;不按时刻比)。 */
|
|
73
|
+
export function overdueOf(row, { now = Date.now(), base = process.cwd() } = {}) {
|
|
74
|
+
if (!PENDING_STATUSES.includes(row?.status)) return null
|
|
75
|
+
if (row.trigger === "条件") {
|
|
76
|
+
const anchor = resumeAnchor(row.resume_when)
|
|
77
|
+
if (anchor.day !== null && anchor.day <= localDay(now)) return "resume_due"
|
|
78
|
+
}
|
|
79
|
+
if (row.trigger === "归批" && !pointerOnDisk(row, base)) return "dangling"
|
|
80
|
+
if (row.trigger == null && isRowAged(row, now)) return "unlabeled"
|
|
81
|
+
if (isRowAged(row, now)) return "aged"
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 查询(只读,全角色):SELECT 行集(status / kind / board / trigger / task_book 等值过滤并取,缺省 =
|
|
86
|
+
* 全部行;id 升序)。库不在 = 空账。只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批
|
|
87
|
+
* FR2③);库档非台账库 ⇒ 空账。逐行携计算字段(均不入库、不入导出白名单 `DATA_COLUMNS`):
|
|
88
|
+
* `aged`(§13.4)∥ `evidenceOversize`(§5 P4 超限读数)∥ `taskBookOnDisk`(§7.1 B 在盘读数)∥ `overdue`
|
|
89
|
+
* (§3.7 出口态字串 ∥ null——机判单源 `overdueOf`);`task_book` = 第 5 枚等值过滤(**原值等值**——列串直比,与 §3.3 选行的
|
|
90
|
+
* 「文件部分解析同址」两径口径不同、各自单源);`trigger` 过滤仅三枚举等值(`NULL` 不过滤);
|
|
91
|
+
* `now` = 注入缝(确定性用例)。 */
|
|
92
|
+
export function ledgerQuery({ cwd, status = null, kind = null, board = null, trigger = null, task_book = null, now = Date.now() } = {}) {
|
|
33
93
|
const db = openLedger(cwd, { readOnly: true })
|
|
34
94
|
if (!db) return []
|
|
35
95
|
try {
|
|
@@ -38,52 +98,82 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null, tri
|
|
|
38
98
|
if (kind) { where.push("kind = ?"); args.push(kind) }
|
|
39
99
|
if (board) { where.push("board = ?"); args.push(board) }
|
|
40
100
|
if (trigger) { where.push("trigger = ?"); args.push(trigger) }
|
|
101
|
+
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
41
102
|
const rows = db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
|
|
42
|
-
|
|
103
|
+
const base = baseOf(cwd)
|
|
104
|
+
return rows.map((r) => ({ ...r, aged: isRowAged(r, now), evidenceOversize: isEvidenceOversize(r), taskBookOnDisk: pointerOnDisk(r, base), overdue: overdueOf(r, { now, base }) }))
|
|
43
105
|
} finally { db.close() }
|
|
44
106
|
}
|
|
45
107
|
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
|
|
108
|
+
/** 计数单源(只读,全角色):回报 `{ count, overdue, evidenceOversize }`(设计档 §7.1——原裸数出参
|
|
109
|
+
* 改对象形,消费面同批改)。`count` = 未决四态行数;`overdue` = 其中命中出口态的行数(`overdueOf`
|
|
110
|
+
* 机判单源);`evidenceOversize` = 其中 `evidence` 超限行数(§5)。**一次 SELECT + 逐行判**——
|
|
111
|
+
* `evidenceOversize` 与 `overdue` 同一次遍历(零额外 IO;成本模型 = §7.2);不设缓存(读数须反映
|
|
112
|
+
* 即时盘面)。库不在 = 三零。 */
|
|
113
|
+
export function ledgerCount({ cwd, task_book = null } = {}) {
|
|
49
114
|
const db = openLedger(cwd, { readOnly: true })
|
|
50
|
-
if (!db) return 0
|
|
115
|
+
if (!db) return { count: 0, overdue: 0, evidenceOversize: 0 }
|
|
51
116
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
117
|
+
const where = [`status IN (${PENDING_STATUSES.map(() => "?").join(",")})`], args = [...PENDING_STATUSES]
|
|
118
|
+
if (task_book) { where.push("task_book = ?"); args.push(task_book) }
|
|
119
|
+
const rows = db.prepare(`SELECT * FROM items WHERE ${where.join(" AND ")}`).all(...args)
|
|
120
|
+
const now = Date.now(), base = baseOf(cwd)
|
|
121
|
+
let overdue = 0, oversize = 0
|
|
122
|
+
for (const r of rows) {
|
|
123
|
+
if (overdueOf(r, { now, base }) !== null) overdue++
|
|
124
|
+
if (isEvidenceOversize(r)) oversize++
|
|
125
|
+
}
|
|
126
|
+
return { count: rows.length, overdue, evidenceOversize: oversize }
|
|
54
127
|
} finally { db.close() }
|
|
55
128
|
}
|
|
56
129
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
/** `evidence` 超限行数(设计档 §7.1 导出面——`count` 回报读数**同一单源**:本函数为其薄委托,
|
|
131
|
+
* 不另立第二判定面)。 */
|
|
132
|
+
export function ledgerEvidenceOversize({ cwd, task_book = null } = {}) {
|
|
133
|
+
return ledgerCount({ cwd, task_book }).evidenceOversize
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
137
|
+
const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
138
|
+
|
|
139
|
+
/** 写门·指针点名(设计档 §6.1 两臂 · AC-M2-10 ∥ AC-M2-29):判**本笔写入面**——两臂之外零判。
|
|
140
|
+
* 臂①(点名非空):结果行 `status ∈ {在途, 待核销}` ∨ 结果行 `trigger = 归批` ⇒ `task_book` 非空
|
|
141
|
+
* (缺 / 空串 / 全空白 ⇒ 拒「必填」);**不判在盘**(列值事实,不随盘面翻转——DDL 咬合 CHECK 为该半幅防线)。
|
|
142
|
+
* 臂②(本笔新指名):本笔新写 `task_book`(非空值)∥ 本笔新写 `trigger = 归批` ⇒ 绑定指针非空 ∧ 文件部分
|
|
143
|
+
* 可解析 ∧ 解析出的档**在盘**(判定单源 = `resolveDeclaredRef`,零二写)。
|
|
144
|
+
* **「档此刻在不在盘」不入臂①**:批档可随时整体清理 ⇒ 当结果行判据则合法账务被拒、而出路正是被拒字段
|
|
145
|
+
* (死锁);存量指针失效只体现为读数 `taskBookOnDisk = false`(§7.1)。
|
|
146
|
+
* 判位 = 与迁移表判同层(落盘前);文案前缀 = 调用函数名;基准 = §6.1 口径 2(根推导单源同子表达式)。
|
|
147
|
+
* @param {{status:string, trigger:string|null, taskBook:string|null, wroteTaskBook:boolean, wroteBatchTrigger:boolean}} face 本笔写入面
|
|
148
|
+
*/
|
|
149
|
+
function assertTaskBookGate(cwd, fnName, face) {
|
|
150
|
+
const { status, trigger, taskBook, wroteTaskBook, wroteBatchTrigger } = face
|
|
151
|
+
if ((status === "在途" || status === "待核销" || trigger === "归批") && !namesPointer(taskBook)) {
|
|
152
|
+
throw new Error(`${fnName}:task_book 必填(在途 / 待核销 / 归批 须携任务书指针)`)
|
|
153
|
+
}
|
|
154
|
+
if (!wroteTaskBook && !wroteBatchTrigger) return // 臂② 之外零判:旧指针所指档已清不阻任何此类写入
|
|
155
|
+
const part = filePartOf(taskBook)
|
|
72
156
|
if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
|
|
73
|
-
const base =
|
|
157
|
+
const base = baseOf(cwd) // §6.1 口径 2 同一子表达式(单源 = 本档 `baseOf`)
|
|
74
158
|
const { ok, abs } = resolveDeclaredRef(base, part)
|
|
75
159
|
if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
76
160
|
}
|
|
77
161
|
|
|
78
|
-
/** 新增(写命令,仅主 agent):INSERT
|
|
162
|
+
/** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。`row.resume_when` 可空(§3.4)。 */
|
|
79
163
|
export function ledgerAdd({ cwd, row }) {
|
|
80
164
|
const db = openLedger(cwd, { create: true })
|
|
81
165
|
try {
|
|
82
|
-
// 结果行状态 = INSERT 硬编码「待讨论」⇒
|
|
83
|
-
assertTaskBookGate(cwd, "ledgerAdd",
|
|
166
|
+
// 结果行状态 = INSERT 硬编码「待讨论」⇒ 臂① 的 `status` 半幅当下不触;`trigger=归批` 半幅与臂② 照判
|
|
167
|
+
assertTaskBookGate(cwd, "ledgerAdd", {
|
|
168
|
+
status: "待讨论",
|
|
169
|
+
trigger: row.trigger ?? null,
|
|
170
|
+
taskBook: row.task_book ?? null,
|
|
171
|
+
wroteTaskBook: namesPointer(row.task_book),
|
|
172
|
+
wroteBatchTrigger: row.trigger === "归批",
|
|
173
|
+
})
|
|
84
174
|
const now = nowIso()
|
|
85
|
-
const r = db.prepare(`INSERT INTO items (kind, status, title, board, req_doc, task_book, evidence, trigger, created_at, updated_at) VALUES (?, '待讨论', ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
86
|
-
.run(row.kind, row.title, row.board ?? null, row.req_doc ?? null, row.task_book ?? null, row.evidence ?? null, row.trigger ?? null, now, now)
|
|
175
|
+
const r = db.prepare(`INSERT INTO items (kind, status, title, board, req_doc, task_book, evidence, trigger, resume_when, created_at, updated_at) VALUES (?, '待讨论', ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
176
|
+
.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)
|
|
87
177
|
return Number(r.lastInsertRowid)
|
|
88
178
|
} finally { db.close() }
|
|
89
179
|
}
|
|
@@ -120,40 +210,69 @@ function assertEvidenceValue(fnName, evidence) {
|
|
|
120
210
|
if (evidence != null && String(evidence).trim() === "") throw new Error(`${fnName}:evidence 为空(非空字符串)`)
|
|
121
211
|
}
|
|
122
212
|
|
|
213
|
+
/** 单行落笔·update(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):迁移表判 → executor /
|
|
214
|
+
* evidence 值计算(含两拒面)→ 写门 → UPDATE(判序 = §13.9 逐字未变)。`to` = `patch.status ?? 行现态`
|
|
215
|
+
* ——批量形纯字段补丁(不携 `status`)时 `to === row.status` ⇒ 迁移表不判、`status` 零写(§3.3)。
|
|
216
|
+
* `resume_when` = 普通可空字段补丁(`patch.resume_when ?? 行值`——§3.4)。 */
|
|
217
|
+
function applyRowUpdate(db, cwd, row, patch, executorSessionId) {
|
|
218
|
+
const to = patch.status ?? row.status
|
|
219
|
+
if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
|
|
220
|
+
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
221
|
+
}
|
|
222
|
+
const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
|
|
223
|
+
const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
|
|
224
|
+
assertEvidenceValue("ledgerUpdate", patch.evidence)
|
|
225
|
+
const nextTaskBook = patch.task_book ?? row.task_book
|
|
226
|
+
assertTaskBookGate(cwd, "ledgerUpdate", {
|
|
227
|
+
status: to,
|
|
228
|
+
trigger: patch.trigger ?? row.trigger,
|
|
229
|
+
taskBook: nextTaskBook,
|
|
230
|
+
wroteTaskBook: namesPointer(patch.task_book),
|
|
231
|
+
wroteBatchTrigger: patch.trigger === "归批",
|
|
232
|
+
}) // → 写门 → UPDATE
|
|
233
|
+
const now = nowIso()
|
|
234
|
+
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, resume_when = ?, executor = ?, updated_at = ? WHERE id = ?`)
|
|
235
|
+
.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)
|
|
236
|
+
return { id: Number(row.id), status: to }
|
|
237
|
+
}
|
|
238
|
+
|
|
123
239
|
/** 更新(写命令,仅主 agent):UPDATE——状态迁移前判允许迁移表(不在表内 → 拒,行不变)。
|
|
124
240
|
* executor 目标值随同一 UPDATE 落列(LEDGER.md §3.1);executorSessionId = 调用会话(函数参数注入——K-LX1
|
|
125
241
|
* 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
|
|
126
242
|
* `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
|
|
127
|
-
* / `assertEvidenceValue`;判位 = 本语义段,随 executor
|
|
243
|
+
* / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。**单行形**(`id`)——逐行落笔 = `applyRowUpdate`。 */
|
|
128
244
|
export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
|
|
129
245
|
const db = openLedger(cwd, { create: true })
|
|
130
246
|
try {
|
|
131
247
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
132
248
|
if (!row) throw new Error(`ledgerUpdate:行 ${id} 不存在`)
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
136
|
-
}
|
|
137
|
-
// 判序(§13.9):迁移表判 → 本语义(executor 目标值 + evidence 值计算——含两拒面)→ 写门 → UPDATE
|
|
138
|
-
const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
|
|
139
|
-
const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
|
|
140
|
-
assertEvidenceValue("ledgerUpdate", patch.evidence)
|
|
141
|
-
const nextTaskBook = patch.task_book ?? row.task_book
|
|
142
|
-
assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // → 写门 → UPDATE
|
|
143
|
-
const now = nowIso()
|
|
144
|
-
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, executor = ?, updated_at = ? WHERE id = ?`)
|
|
145
|
-
.run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, nextEvidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
|
|
146
|
-
return { id }
|
|
249
|
+
const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId)
|
|
250
|
+
return { id: rowId }
|
|
147
251
|
} finally { db.close() }
|
|
148
252
|
}
|
|
149
253
|
|
|
254
|
+
/** 单行落笔·close(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):源态判(核销仅三源——
|
|
255
|
+
* 在途不可跳)→ 值计算(拒面① 旗独传判于此)→ 追认口 `evidence` 门 → 拒面② → UPDATE。
|
|
256
|
+
* 撤回(任意态 → 已废弃——含在途直撤)同步 `executor = NULL`(LEDGER.md §3.1 ④);核销两源(勾销 /
|
|
257
|
+
* 追认)`executor` 零触碰(非在途出边)。 */
|
|
258
|
+
function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
259
|
+
if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
|
|
260
|
+
const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
|
|
261
|
+
if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
|
|
262
|
+
assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
|
|
263
|
+
const now = nowIso()
|
|
264
|
+
db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ?, evidence = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, nextEvidence, row.id)
|
|
265
|
+
return { id: Number(row.id), status }
|
|
266
|
+
}
|
|
267
|
+
|
|
150
268
|
/** 收口(写命令,仅主 agent):核销两源(勾销:待核销 → 已核销;追认核销:待讨论 / 待设计 → 已核销,
|
|
151
269
|
* 行 `evidence` 非空,缺 / 全空白 ⇒ 拒)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。
|
|
152
270
|
* 可选 `evidence` 参:传值 = 追加缺省(§13.9——旧空直写 / 旧非空拼 `旧|新`)、`evidenceReplace: true`
|
|
153
271
|
* 同传 = 整段替换;勾销 / 追认 / 撤回三径同携(写值落行,内容不判);两拒面(旗独传 @值计算 ∥
|
|
154
272
|
* 新值空白 @追认门后)见 `evidenceValue` / `assertEvidenceValue`。
|
|
155
273
|
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 → 追认门(原样——判**结果值**缺 /
|
|
156
|
-
* 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3
|
|
274
|
+
* 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3 收口两源)。**单行形**
|
|
275
|
+
* (`id`)——逐行落笔 = `applyRowClose`。 */
|
|
157
276
|
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
|
|
158
277
|
if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
|
|
159
278
|
const db = openLedger(cwd, { create: true })
|
|
@@ -162,16 +281,150 @@ export function ledgerClose({ cwd, id, status, evidence, evidenceReplace }) {
|
|
|
162
281
|
try {
|
|
163
282
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
164
283
|
if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
|
|
165
|
-
|
|
166
|
-
if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
|
|
167
|
-
const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
|
|
168
|
-
if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
|
|
169
|
-
assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
|
|
170
|
-
const now = nowIso()
|
|
171
|
-
// 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);核销两源(勾销 / 追认)executor 零触碰(非在途出边)
|
|
172
|
-
db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ?, evidence = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, nextEvidence, id)
|
|
284
|
+
const out = applyRowClose(db, row, status, evidence, evidenceReplace)
|
|
173
285
|
db.exec("COMMIT")
|
|
174
|
-
return
|
|
286
|
+
return out
|
|
175
287
|
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
176
288
|
} finally { db.close() }
|
|
177
289
|
}
|
|
290
|
+
|
|
291
|
+
// ── 批级批量迁移与结算(设计档 §3.3 · P1 · AC-M2-26 ∥ AC-M2-27)────────────────
|
|
292
|
+
|
|
293
|
+
/** `batch` 入参自身门(设计档 §3.3 · fail-closed):值自身须**可解析**(含非空文件部分)∧ 解析后批档
|
|
294
|
+
* **在盘** ⇒ 否则拒(零写)。判的是**本笔指名**的那一笔——与 §6.1 臂② 同一律、不同字段(§6.2 归批
|
|
295
|
+
* 门判结果行存在性 + 本笔新指名,本门判 `batch` 入参自身)。无匹配行 ⇒ 不拒(`applied = []`)。
|
|
296
|
+
* @returns {string} 批档绝对路径(解析基准 = `base`)
|
|
297
|
+
*/
|
|
298
|
+
function assertBatchArg(fnName, base, batch) {
|
|
299
|
+
const part = filePartOf(batch)
|
|
300
|
+
const hit = part === "" ? { ok: false, abs: "" } : resolveDeclaredRef(base, part)
|
|
301
|
+
if (!hit.ok) throw new Error(`${fnName}:batch 不可解析或不在盘(${batch})`)
|
|
302
|
+
return hit.abs
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 批 → 行集(选行**单源** · 设计档 §3.3):`task_book` 文件部分经 `resolveDeclaredRef` 解析为与批档
|
|
306
|
+
* **同一绝对路径**的行(任意状态、含归档态)。判据 = 解析同址而非串等值(同批档三种指针写法并存——
|
|
307
|
+
* 无后缀 / `§2` / `§1.5(括注)`,串等值会漏行)。 */
|
|
308
|
+
function resolveBatchRows(db, base, abs) {
|
|
309
|
+
return db.prepare("SELECT * FROM items ORDER BY id").all()
|
|
310
|
+
.filter((r) => { const part = filePartOf(r.task_book); return part !== "" && resolveDeclaredRef(base, part).abs === abs })
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 批级批量迁移(设计档 §3.3):`patch.status` 在场 ⇒ 逐行按既有迁移表重判;不在场 ⇒ **纯字段补丁**
|
|
314
|
+
* (`status` 零写、迁移表不判、`executor` 普通补丁语义——回执 `status` 键 = `null`)。
|
|
315
|
+
* **门拒 ≠ 事务失败**:逐行门(迁移表 / §6.1 写门 / evidence 门)抛错 ⇒ 本行捕获计入 `skipped`
|
|
316
|
+
* (`why` = 该门拒因文案原文),其余行照落;**事务级失败**(取写锁 / `COMMIT` 层)⇒ 整批 `ROLLBACK`
|
|
317
|
+
* 并抛(零行改)——故「部分落地」只由逐行门拒产生,「中途失败」不留半批。
|
|
318
|
+
* `BEGIN IMMEDIATE`:写锁一次先把(逐行不升级锁 ⇒ 事务级失败在首行之前即失败)。
|
|
319
|
+
* @returns {{batch: string, status: string|null, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
320
|
+
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId }) {
|
|
321
|
+
const db = openLedger(cwd, { create: true })
|
|
322
|
+
try {
|
|
323
|
+
const base = baseOf(cwd)
|
|
324
|
+
const abs = assertBatchArg("ledgerUpdate", base, batch)
|
|
325
|
+
db.exec("BEGIN IMMEDIATE")
|
|
326
|
+
try {
|
|
327
|
+
const applied = [], skipped = []
|
|
328
|
+
for (const row of resolveBatchRows(db, base, abs)) {
|
|
329
|
+
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId).id) }
|
|
330
|
+
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
331
|
+
}
|
|
332
|
+
db.exec("COMMIT")
|
|
333
|
+
return { batch, status: patch.status ?? null, applied, skipped }
|
|
334
|
+
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
335
|
+
} finally { db.close() }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 批级批量结算(设计档 §3.3):**只销已到 `待核销` 的行**(勾销径);`待讨论` / `待设计` ⇒ `skipped`
|
|
339
|
+
* 逐行点名(**追认核销不入批量面**——追认须逐行结账依据,统一文本会造假账;转单行面),`在途` ⇒
|
|
340
|
+
* `skipped` 逐行点名(不可跳)。**归档态(`已核销` / `已废弃`)行 ⇒ 静默不计**(不入 `applied`、
|
|
341
|
+
* 不入 `skipped`——归档行本非结算对象,点名会让每批重跑刷屏)。`evidence` 逐行追加同一文本
|
|
342
|
+
* (`evidenceValue` 同源)。回执 `status` 恒 `已核销`(批量形缺省 = 勾销单值;携则守卫已收窄为
|
|
343
|
+
* `{已核销}`——§3.2 `whenPresent`)。事务语义同 `ledgerUpdateBatch`。
|
|
344
|
+
* @returns {{batch: string, status: string, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
345
|
+
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace }) {
|
|
346
|
+
const status = "已核销"
|
|
347
|
+
const db = openLedger(cwd, { create: true })
|
|
348
|
+
try {
|
|
349
|
+
const base = baseOf(cwd)
|
|
350
|
+
const abs = assertBatchArg("ledgerClose", base, batch)
|
|
351
|
+
db.exec("BEGIN IMMEDIATE")
|
|
352
|
+
try {
|
|
353
|
+
const applied = [], skipped = []
|
|
354
|
+
for (const row of resolveBatchRows(db, base, abs)) {
|
|
355
|
+
if (row.status === "已核销" || row.status === "已废弃") continue // 归档态静默不计
|
|
356
|
+
if (row.status !== "待核销") {
|
|
357
|
+
const why = row.status === "在途" ? "ledgerClose:在途不可跳(须先 待核销)" : "ledgerClose:追认核销不入批量面(转单行 close)"
|
|
358
|
+
skipped.push({ id: Number(row.id), from: row.status, why })
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
try { applied.push(applyRowClose(db, row, status, evidence, evidenceReplace).id) }
|
|
362
|
+
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
363
|
+
}
|
|
364
|
+
db.exec("COMMIT")
|
|
365
|
+
return { batch, status, applied, skipped }
|
|
366
|
+
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
367
|
+
} finally { db.close() }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── 指针判据重锚面(设计档 §7.1 · B 读数 ∥ C 收口重锚 · 台账 #1550)────────────────
|
|
371
|
+
|
|
372
|
+
/** 重锚取行集(设计档 §6.2——`待核销` 属已落只待结账,不取:对它写「须另起批次接手」失实,且核销零触指针)。 */
|
|
373
|
+
const REANCHOR_STATUSES = ["待讨论", "待设计", "在途"]
|
|
374
|
+
|
|
375
|
+
/** 本批行集(只读,任意状态——设计档 §7.1):批身份单源 = `resolveBatchRows` 解析同址(零二写);
|
|
376
|
+
* 逐行携读数 `taskBookOnDisk`(不入库、不入 `DATA_COLUMNS`)。`batch` 不可解析 ∥ 无根 ⇒ 抛(同族
|
|
377
|
+
* `assertBatchArg` 形);库不在 ⇒ 空集。`cwd` 口径 = 批档所属项目根(§6.2 `cwd` 基准——不用会话锚)。 */
|
|
378
|
+
export function ledgerBatchRows({ cwd, batch } = {}) {
|
|
379
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd)
|
|
380
|
+
if (!derived) throw new Error(`ledgerBatchRows:批档所属项目根不可推导(${cwd ?? "(缺 cwd)"})`)
|
|
381
|
+
const base = derived.root
|
|
382
|
+
const abs = assertBatchArg("ledgerBatchRows", base, batch) // 失败语义先于「库不在 ⇒ 空集」(不可解析就是不可解析)
|
|
383
|
+
const db = openLedger(base, { readOnly: true })
|
|
384
|
+
if (!db) return []
|
|
385
|
+
try {
|
|
386
|
+
return resolveBatchRows(db, base, abs).map((r) => ({ ...r, taskBookOnDisk: pointerOnDisk(r, base) }))
|
|
387
|
+
} finally { db.close() }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 收口重锚单源(写命令,主 agent 面——设计档 §6.2 · C · AC-M2-30):取本批未落行(`REANCHOR_STATUSES`)→
|
|
391
|
+
* 现值已 `条件` ⇒ 零写跳过(幂等);`认账不排期` 零触(政策判断归行主人)→ 逐行改 `trigger = 条件` + 复启句
|
|
392
|
+
* (`原批 <批名> 已收口,须另起批次接手`)落 `resume_when`;该列现值非空(行主条件句在册)⇒ **不覆盖**、
|
|
393
|
+
* 复句追加 `evidence`。单事务:任一步失败 ⇒ 整集 `ROLLBACK`(零行改)。**不抛**——失败经返回形 `failed`
|
|
394
|
+
* 报出(`null` 常态);`cwd` 无根 ⇒ 不取行、不重锚(**不落兜底基准**——落了就是静默零命中)。
|
|
395
|
+
* `updated_at` 零触(机器重锚不是人工推进的痕迹——行龄读数照旧)。
|
|
396
|
+
* @returns {{batch:string, reanchored:{id:number,status:string,title:string,sentenceTo:"resume_when"|"evidence"}[], alreadyAnchored:{id:number,status:string,title:string}[], failed:string|null}} */
|
|
397
|
+
export function ledgerReanchorBatch({ cwd, batch } = {}) {
|
|
398
|
+
const failedResult = (why) => ({ batch, reanchored: [], alreadyAnchored: [], failed: why })
|
|
399
|
+
const derived = cwd == null ? null : resolveLedgerRoot(cwd)
|
|
400
|
+
if (!derived) return failedResult("批档所属项目根不可推导")
|
|
401
|
+
const base = derived.root
|
|
402
|
+
let db
|
|
403
|
+
try { db = openLedger(base) } catch (e) { return failedResult(String(e?.message ?? e)) } // 库不可开 ⇒ 零写报因
|
|
404
|
+
if (!db) return { batch, reanchored: [], alreadyAnchored: [], failed: null } // 库不在 ⇒ 零行可锚
|
|
405
|
+
try {
|
|
406
|
+
let abs
|
|
407
|
+
try { abs = assertBatchArg("ledgerReanchorBatch", base, batch) } catch (e) { return failedResult(String(e?.message ?? e)) }
|
|
408
|
+
const sentence = `原批 ${filePartOf(batch)} 已收口,须另起批次接手`
|
|
409
|
+
try {
|
|
410
|
+
db.exec("BEGIN IMMEDIATE")
|
|
411
|
+
const reanchored = [], alreadyAnchored = []
|
|
412
|
+
for (const row of resolveBatchRows(db, base, abs)) {
|
|
413
|
+
if (!REANCHOR_STATUSES.includes(row.status)) continue
|
|
414
|
+
if (row.trigger === "认账不排期") continue
|
|
415
|
+
const face = { id: Number(row.id), status: row.status, title: row.title }
|
|
416
|
+
if (row.trigger === "条件") { alreadyAnchored.push(face); continue }
|
|
417
|
+
const kept = row.resume_when != null && String(row.resume_when).trim() !== ""
|
|
418
|
+
const nextEvidence = kept ? evidenceValue("ledgerReanchorBatch", row.evidence, sentence, false) : row.evidence
|
|
419
|
+
db.prepare("UPDATE items SET trigger = ?, resume_when = ?, evidence = ? WHERE id = ?")
|
|
420
|
+
.run("条件", kept ? row.resume_when : sentence, nextEvidence, row.id)
|
|
421
|
+
reanchored.push({ ...face, sentenceTo: kept ? "evidence" : "resume_when" })
|
|
422
|
+
}
|
|
423
|
+
db.exec("COMMIT")
|
|
424
|
+
return { batch, reanchored, alreadyAnchored, failed: null }
|
|
425
|
+
} catch (e) {
|
|
426
|
+
try { db.exec("ROLLBACK") } catch { /* 事务未开 / 已结 */ }
|
|
427
|
+
return failedResult(String(e?.message ?? e))
|
|
428
|
+
}
|
|
429
|
+
} finally { db.close() }
|
|
430
|
+
}
|
package/ledger-db.mjs
CHANGED
|
@@ -139,9 +139,15 @@ export const PENDING_STATUSES = ["待讨论", "待设计", "在途", "待核销"
|
|
|
139
139
|
* 保公共面零变——LEDGER.md §13.4)。 */
|
|
140
140
|
export const AGING_DAYS = 30
|
|
141
141
|
|
|
142
|
+
/** `evidence` 软上限(字——设计档 `LEDGER.md` §5 P4 读数面单源):超限**只出读数,不拒写**
|
|
143
|
+
* (软上限非闸门——用户裁定「无限机检反感」;读数面 = `query` 逐行 `evidenceOversize` ∥
|
|
144
|
+
* `count` 回报)。 */
|
|
145
|
+
export const EVIDENCE_SOFT_LIMIT = 300
|
|
146
|
+
|
|
142
147
|
/** 台账库结构版本(N2 标记**单源**——`PRAGMA user_version` 落标:写面开库 `v < 本值 ⇒ 置`;
|
|
143
|
-
* 读面如实回读(旧库未标 = 0);结构变更(新列 / 新表)同步升号——2026-10-03 read-data-interface
|
|
144
|
-
|
|
148
|
+
* 读面如实回读(旧库未标 = 0);结构变更(新列 / 新表)同步升号——2026-10-03 read-data-interface 批。
|
|
149
|
+
* 2 = `resume_when` 可空列(设计档 §3.4 P5)。 */
|
|
150
|
+
export const LEDGER_SCHEMA_VERSION = 2
|
|
145
151
|
|
|
146
152
|
/** 六态允许迁移表(ledgerUpdate 迁移前判,不在表内 → 拒——规格 ②.2;设计档 §2.1)。 */
|
|
147
153
|
export const ALLOWED_MIGRATIONS = {
|
|
@@ -169,25 +175,30 @@ CREATE TABLE IF NOT EXISTS items (
|
|
|
169
175
|
created_at TEXT,
|
|
170
176
|
updated_at TEXT,
|
|
171
177
|
closed_at TEXT,
|
|
178
|
+
resume_when TEXT,
|
|
172
179
|
CHECK (status NOT IN ('在途','待核销') OR (task_book IS NOT NULL AND task_book <> ''))
|
|
173
180
|
)`
|
|
174
181
|
|
|
175
|
-
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
|
|
182
|
+
/** 老库幂等迁移共件:items 表缺该可空列 ⇒ ALTER 补列(`executor` ∥ `resume_when` 两列共用一份
|
|
183
|
+
* 「复判列在否」逻辑——设计档 §3.4)。判据 = PRAGMA table_info 实查(不猜版本号);ALTER
|
|
184
|
+
* duplicate column 竞争 → 复核列在吞 / 不在抛真错。列存在性谓词抽成单元(param exists),
|
|
185
|
+
* 便于并发竞争拍直测。K-LX5:老库零删改、零重建。 */
|
|
186
|
+
export function ensureColumn(db, name, { exists } = {}) {
|
|
179
187
|
const columnExists = exists
|
|
180
|
-
?? (() => db.prepare("SELECT 1 FROM pragma_table_info('items') WHERE name =
|
|
188
|
+
?? (() => db.prepare("SELECT 1 FROM pragma_table_info('items') WHERE name = ?").get(name) !== undefined)
|
|
181
189
|
if (columnExists()) return false
|
|
182
190
|
try {
|
|
183
|
-
db.exec(
|
|
191
|
+
db.exec(`ALTER TABLE items ADD COLUMN ${name} TEXT`)
|
|
184
192
|
return true
|
|
185
193
|
} catch (e) {
|
|
186
194
|
if (columnExists()) return false // 并发竞争——另一连接已加列:吞
|
|
187
|
-
throw new Error(`台账库
|
|
195
|
+
throw new Error(`台账库 ${name} 列迁移失败:${e.message}`)
|
|
188
196
|
}
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
/** `executor` 列补列(F-LX1——共件别名,公共面形零改)。 */
|
|
200
|
+
export const ensureExecutorColumn = (db, opts) => ensureColumn(db, "executor", opts)
|
|
201
|
+
|
|
191
202
|
/** 只读态开库(read-data-interface 批 FR2③——readOnly 句柄 + **零 DDL / 零 ALTER**;先例 = 本包
|
|
192
203
|
* `ledger-migrate.mjs` 的 `openRead`):文件必已在盘;items 表缺失探针 ⇒ 视同空账返回 null
|
|
193
204
|
* (非台账库的 SQLite 档 / 空档——观测面保持「空集」,文件零触)。不可开(坏档 / 非 SQLite)⇒
|
|
@@ -239,7 +250,8 @@ export function openLedger(cwd, { create = false, readOnly = false } = {}) {
|
|
|
239
250
|
const db = new DatabaseSync(file)
|
|
240
251
|
try {
|
|
241
252
|
db.exec(DDL)
|
|
242
|
-
|
|
253
|
+
ensureColumn(db, "executor")
|
|
254
|
+
ensureColumn(db, "resume_when")
|
|
243
255
|
markSchemaVersion(db)
|
|
244
256
|
} catch (e) {
|
|
245
257
|
db.close()
|
package/ledger-migrate.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* ② `ledger migrate --dry-run | --confirm`:变体键库一次性合并——三件套 = dry-run 零写 / 先备份(拷贝 + 读回同计数)/ 幂等(全字段等值);
|
|
4
4
|
* 六步 = 备份 → 源就绪与列集判 → 单事务迁入(事务内读回核验)→ 回收源(rename 进 `ledger-trash`,不 unlink)→ 报告 → 汇总。执行主体 = 父侧 ops(KD-LN6)。
|
|
5
5
|
* ③ `ledger audit`:逐 `*.db` 定性(目标库 / 变体源 / 空库 / 不可归因(有行)/ 不可读(坏档);stat 失败 ⇒ 读取失败态)+ 候选根集合归因;只报告零动作。
|
|
6
|
-
* 铁律:**
|
|
6
|
+
* 铁律:**13 数据列逐字复制(不含 id)+ id 重发**(两库 id 空间重叠;列集单源 = `DATA_COLUMNS`——`resume_when`
|
|
7
|
+
* 落地后 12 ⇒ 13,设计档 §3.4);备份 / 回收目录 = `ledgerDirPath()` 的**兄弟位**(§2.11#6 ⇒ 夹具根外零写);
|
|
7
8
|
* 迁移走直接 SQL(§6.1 写门不拦迁移本身 ⇒ 风险行出旗不拦截);本档静态 import node:sqlite ⇒ 消费侧一律动态 import(W8 契约②——同 ledger-db.mjs)。
|
|
8
9
|
*/
|
|
9
10
|
import { createHash } from "node:crypto"
|
|
@@ -15,11 +16,14 @@ import { resolveDeclaredRef } from "./declaration.mjs"
|
|
|
15
16
|
import { ledgerDirPath, ledgerKey, openLedger, resolveLedgerRoot } from "./ledger-db.mjs"
|
|
16
17
|
import { resolveProjectRoot } from "./manifest.mjs"
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
+
/** 13 数据列(**等值比对列集**——不含 id:id 重发,§2.2 语义保全 / AC-M2-12;`resume_when` 落地后\n * 12 ⇒ 13——设计档 §3.4)。 */
|
|
19
20
|
export const DATA_COLUMNS = [
|
|
20
21
|
"kind", "status", "title", "board", "req_doc", "task_book",
|
|
21
|
-
"evidence", "trigger", "executor", "created_at", "updated_at", "closed_at",
|
|
22
|
+
"evidence", "trigger", "executor", "created_at", "updated_at", "closed_at", "resume_when",
|
|
22
23
|
]
|
|
24
|
+
/** 旧库可缺列容忍集(缺则按 NULL 映射照迁):`executor`(F-LX1 前的旧 DDL)∥ `resume_when`
|
|
25
|
+
* (P5 落地前的旧 DDL——设计档 §3.4)。其余任一数据列缺 ⇒ 列集不可归因,拒(fail-closed)。 */
|
|
26
|
+
const TOLERATED_MISSING = ["executor", "resume_when"]
|
|
23
27
|
/** 备份面根 / 回收面根(`ledgerDirPath()` 的**兄弟位**——同派生于可覆盖基,§2.11#6)。 */
|
|
24
28
|
export function ledgerBackupRoot() { return join(dirname(ledgerDirPath()), "ledger-backup") }
|
|
25
29
|
export function ledgerTrashRoot() { return join(dirname(ledgerDirPath()), "ledger-trash") }
|
|
@@ -48,7 +52,7 @@ function batchStamp(date) {
|
|
|
48
52
|
const p = (n) => String(n).padStart(2, "0")
|
|
49
53
|
return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`
|
|
50
54
|
}
|
|
51
|
-
/** 行集读取(id +
|
|
55
|
+
/** 行集读取(id + `DATA_COLUMNS` 全列;缺失列按 NULL 映射——旧 DDL 源的 `executor` / `resume_when` 正常态,KD-LN10)。 */
|
|
52
56
|
function readRows(db, missing = []) {
|
|
53
57
|
const cols = DATA_COLUMNS.filter((c) => !missing.includes(c))
|
|
54
58
|
return db.prepare(`SELECT id, ${cols.join(", ")} FROM items ORDER BY id`).all()
|
|
@@ -62,7 +66,7 @@ function describeSource(file) {
|
|
|
62
66
|
const cols = itemColumns(db)
|
|
63
67
|
if (!cols.length) return { rows: 0, missing: [], refusal: "不可开为台账库(无 items 表)" }
|
|
64
68
|
const missing = DATA_COLUMNS.filter((c) => !cols.includes(c))
|
|
65
|
-
const bad = missing.filter((c) => c
|
|
69
|
+
const bad = missing.filter((c) => !TOLERATED_MISSING.includes(c))
|
|
66
70
|
const rows = readRows(db, missing)
|
|
67
71
|
return { rows: rows.length, missing, dist: byStatus(rows), refusal: bad.length ? `缺数据列(列集不可归因):${bad.join(" / ")}` : null }
|
|
68
72
|
} catch (e) { return { error: e.message, refusal: `不可读(${e.message})` } } finally { db.close() }
|
|
@@ -72,7 +76,9 @@ function readRowsOf(file, missing = []) {
|
|
|
72
76
|
const db = openRead(file)
|
|
73
77
|
try { return readRows(db, missing) } finally { db.close() }
|
|
74
78
|
}
|
|
75
|
-
/**
|
|
79
|
+
/** 指针读数旗(§2.2 表 · §6.1 口径 5):迁入行属 `在途 / 待核销` 且 `task_book` 不可解析 / 指向档不存在 ⇒ 出旗;
|
|
80
|
+
* **读数非门**——不拦任何写入(该行迁入照旧、后续更新按 §6.1 本笔写入面判)。判面与写门同源·#834
|
|
81
|
+
* (共享解析单点 `resolveDeclaredRef`:① 仓根解析 → ② 声明源前缀解析序)。 */
|
|
76
82
|
function writeGateFlag(values, projectRoot) {
|
|
77
83
|
const tb = values[5]
|
|
78
84
|
if ((values[1] !== "在途" && values[1] !== "待核销") || tb == null || tb === "") return null
|
|
@@ -116,7 +122,7 @@ export function planLedgerMigration(cwd, { fromKeys = [] } = {}) {
|
|
|
116
122
|
return { projectRoot, targetKey, targetPath, targetDesc, sources, flags, migrate }
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
/** dry-run 计划行(零写报告:目标 / 变体源候选 / 计划动作 /
|
|
125
|
+
/** dry-run 计划行(零写报告:目标 / 变体源候选 / 计划动作 / 指针读数旗(非门))。 */
|
|
120
126
|
function printPlan(plan, out) {
|
|
121
127
|
const t = plan.targetDesc
|
|
122
128
|
const dist = (d) => (d.dist ? Object.entries(d.dist).map(([k, v]) => `${k} ${v}`).join(" · ") : `unreadable: ${d.error}`)
|
|
@@ -127,13 +133,13 @@ function printPlan(plan, out) {
|
|
|
127
133
|
`Variant sources (${plan.sources.length}):${plan.sources.length ? "" : " none(无待迁源)"}`,
|
|
128
134
|
...plan.sources.map((s) => ` ${s.key} ${s.path} rows ${s.desc.rows ?? "?"} mtime ${s.mtime}\n ${dist(s.desc)}${s.refusal ? ` ⚠ ${s.refusal}(confirm 面将拒跑)` : ""}`),
|
|
129
135
|
`Plan: backup → ${join(ledgerBackupRoot(), "<批次>")}/ ; migrate ${plan.migrate} rows → target (post-migration count = ${t.error ? "?" : (t.rows ?? 0) + plan.migrate})`,
|
|
130
|
-
`
|
|
136
|
+
`Pointer reading flags: ${plan.flags.length}${plan.flags.length ? "(读数非门——不拦任何写入:行照迁,后续更新按本笔写入面判)" : ""}`,
|
|
131
137
|
...plan.flags.map((f) => ` ⚠ ${f.source} #${f.id}: ${f.flag}`),
|
|
132
138
|
'Run "thincoder ledger migrate --confirm" to execute (backup first, then single-transaction import, then sources are recycled into ledger-trash).',
|
|
133
139
|
].join("\n"))
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
/** 事务内读回核验(§2.2 步 3):逐插入行按新 id 回读
|
|
142
|
+
/** 事务内读回核验(§2.2 步 3):逐插入行按新 id 回读 `DATA_COLUMNS` 全列与源等值 ∧ 目标既有行零动;不等 ⇒ throw(ROLLBACK ⇒ 目标零行变、源零动——新建档可能残留)。 */
|
|
137
143
|
function verifyImported(db, inserted, existingById) {
|
|
138
144
|
const get = db.prepare(`SELECT ${DATA_COLUMNS.join(", ")} FROM items WHERE id = ?`)
|
|
139
145
|
for (const { key, id, values } of inserted) {
|