@thincoder/core 0.10.1 → 0.10.3
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 +46 -0
- package/advisor/citations.mjs +13 -3
- package/advisor/convergence.mjs +1 -1
- package/advisor/messages.mjs +3 -3
- package/agent/dispatch-run.mjs +29 -12
- package/agent/record-results.mjs +6 -1
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +35 -9
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/advisor-async.mjs +11 -1
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/batch-lifecycle.mjs +3 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +28 -0
- package/agent-tools/batch.mjs +5 -3
- package/agent-tools/design-token.mjs +56 -16
- package/agent-tools/subagent-async.mjs +6 -1
- package/agent-tools/subagent-panel.mjs +119 -36
- package/agent-tools/subagent.mjs +4 -2
- package/agent-tools/verify.mjs +5 -0
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +42 -8
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/ledger-cmd.mjs +13 -21
- package/ledger-db.mjs +34 -1
- package/ledger-migrate.mjs +4 -3
- package/ledger-read.mjs +94 -0
- package/ledger-surface.mjs +15 -46
- package/ledger.mjs +36 -62
- package/manifest-schema.mjs +13 -3
- package/manifest.mjs +27 -8
- package/mcp/transport-http.mjs +52 -4
- package/mcp/transport-stdio.mjs +33 -8
- package/mcp.mjs +13 -5
- package/memory/code-search.mjs +5 -3
- package/memory/core.mjs +6 -4
- package/memory/docs.mjs +5 -3
- package/memory/schema.mjs +2 -0
- package/model-ref.mjs +96 -3
- package/package.json +1 -1
- package/prompts/advisor-design.md +3 -2
- package/prompts/advisor-round1.md +1 -0
- package/prompts/advisor-round2.md +8 -3
- package/prompts/advisor-round3.md +7 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +3 -1
- package/provider/core.mjs +10 -4
- package/provider/google.mjs +18 -2
- package/provider/normalize.mjs +57 -0
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session-lifecycle.mjs +21 -12
- package/session.mjs +13 -2
- package/stream-destroy.mjs +62 -0
- package/tool-docs/verify.md +1 -1
- package/tools/edit-batch.mjs +7 -2
- package/undo-stack.mjs +23 -9
package/embedding.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { RETRYABLE_STATUS } from "./provider/index.mjs"
|
|
9
9
|
import { resolveEnvRefs } from "./env-ref.mjs"
|
|
10
|
+
// #859(MEMORY.md §6.3 ∥ §7 D-MEM31):嵌入路径发送前净化孤立 UTF-16 代理——sanitize 单源复用
|
|
11
|
+
import { sanitizeLoneSurrogates } from "./escape.mjs"
|
|
10
12
|
const MAX_RETRIES = 3
|
|
11
13
|
const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
|
|
12
14
|
|
|
@@ -48,6 +50,33 @@ export async function embed(embedder, texts, { signal } = {}) {
|
|
|
48
50
|
return vectors
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 毒行隔离消费面(MEMORY.md §6.3 ∥ §7 D-MEM31 · 台账 #859):补嵌批的容错版 `embed`。
|
|
55
|
+
* 整批失败且错误携 `httpStatus === 400`(发送前清洗后仍 400 的未知毒形)⇒ 逐条独试:
|
|
56
|
+
* 成功者照常返回、仍败者置 `null`(调用面跳过不写 + 一行可见回执);其余错误(网络 ∕ 401 等)
|
|
57
|
+
* 照旧整抛——隔离只兜 400 类。query 面(搜索侧单条嵌入)不走本函数(语义零变)。
|
|
58
|
+
* @returns {Promise<{vectors: (Float32Array|null)[], skipped: string[]}>}
|
|
59
|
+
*/
|
|
60
|
+
export async function embedTolerant(embedder, texts, { signal } = {}) {
|
|
61
|
+
try {
|
|
62
|
+
return { vectors: await embed(embedder, texts, { signal }), skipped: [] }
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err?.httpStatus !== 400) throw err
|
|
65
|
+
const vectors = []
|
|
66
|
+
const skipped = []
|
|
67
|
+
for (let i = 0; i < texts.length; i++) {
|
|
68
|
+
try {
|
|
69
|
+
const [vec] = await embed(embedder, [texts[i]], { signal })
|
|
70
|
+
vectors.push(vec)
|
|
71
|
+
} catch (e) {
|
|
72
|
+
vectors.push(null)
|
|
73
|
+
skipped.push(`input #${i}: ${e.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { vectors, skipped }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
/** Cosine similarity (inputs are normalized, dot product equals cosine) */
|
|
52
81
|
export function cosine(a, b) {
|
|
53
82
|
if (a.length !== b.length) return 0
|
|
@@ -85,7 +114,8 @@ async function requestWithRetry(embedder, input, signal) {
|
|
|
85
114
|
"Content-Type": "application/json",
|
|
86
115
|
Authorization: `Bearer ${embedder.apiKey}`,
|
|
87
116
|
},
|
|
88
|
-
|
|
117
|
+
// #859 主修:发送前逐条净化孤立代理(存量毒行 ⇒ 硅基流动 400/20015;清洗后根因消除)
|
|
118
|
+
body: JSON.stringify({ model: embedder.model, input: input.map((t) => sanitizeLoneSurrogates(t)) }),
|
|
89
119
|
signal: signal
|
|
90
120
|
? AbortSignal.any([signal, AbortSignal.timeout(60_000)])
|
|
91
121
|
: AbortSignal.timeout(60_000),
|
|
@@ -100,11 +130,13 @@ async function requestWithRetry(embedder, input, signal) {
|
|
|
100
130
|
|
|
101
131
|
const text = await response.text().catch(() => "")
|
|
102
132
|
const message = `Embedding API error ${response.status}: ${text}`
|
|
133
|
+
const err = new Error(message)
|
|
134
|
+
err.httpStatus = response.status // #859:隔离层判据(400 类 ⇒ embedTolerant 逐条独试)
|
|
103
135
|
if (RETRYABLE_STATUS.has(response.status)) {
|
|
104
|
-
lastError =
|
|
136
|
+
lastError = err
|
|
105
137
|
continue
|
|
106
138
|
}
|
|
107
|
-
throw
|
|
139
|
+
throw err
|
|
108
140
|
}
|
|
109
141
|
throw lastError
|
|
110
142
|
}
|
package/ledger-cmd.mjs
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
* 的 depthOnly 分支)——一律**动态 import** 本模块链(ledger-db.mjs 静态 import node:sqlite ⇒
|
|
11
11
|
* 消费侧静态 import 会破 W8 契约②)。
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { basename, resolve } from "node:path"
|
|
13
|
+
import { resolve } from "node:path"
|
|
15
14
|
import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES } from "./ledger-db.mjs"
|
|
16
15
|
import { resolveProjectRoot } from "./manifest.mjs"
|
|
17
|
-
import {
|
|
16
|
+
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
18
17
|
|
|
19
|
-
/** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。
|
|
18
|
+
/** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。
|
|
19
|
+
* 只读开库(readOnly 句柄 + 零 DDL/ALTER——read-data-interface 批 FR2③);库档非台账库 ⇒ 空账。 */
|
|
20
20
|
export function ledgerQuery({ cwd, status = null, kind = null, board = null } = {}) {
|
|
21
|
-
const db = openLedger(cwd)
|
|
21
|
+
const db = openLedger(cwd, { readOnly: true })
|
|
22
22
|
if (!db) return []
|
|
23
23
|
try {
|
|
24
24
|
const where = [], args = []
|
|
@@ -29,9 +29,10 @@ export function ledgerQuery({ cwd, status = null, kind = null, board = null } =
|
|
|
29
29
|
} finally { db.close() }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** 计数单源(只读,全角色):COUNT(*) WHERE 未决四态——替代 v1 md 计数面(AC-M2-3)。库不在 = 0。
|
|
32
|
+
/** 计数单源(只读,全角色):COUNT(*) WHERE 未决四态——替代 v1 md 计数面(AC-M2-3)。库不在 = 0。
|
|
33
|
+
* 只读开库(同 ledgerQuery——零 DDL/ALTER);库档非台账库 ⇒ 0。 */
|
|
33
34
|
export function ledgerCount({ cwd } = {}) {
|
|
34
|
-
const db = openLedger(cwd)
|
|
35
|
+
const db = openLedger(cwd, { readOnly: true })
|
|
35
36
|
if (!db) return 0
|
|
36
37
|
try {
|
|
37
38
|
const row = db.prepare(`SELECT COUNT(*) AS n FROM items WHERE status IN (${PENDING_STATUSES.map(() => "?").join(",")})`).get(...PENDING_STATUSES)
|
|
@@ -39,9 +40,6 @@ export function ledgerCount({ cwd } = {}) {
|
|
|
39
40
|
} finally { db.close() }
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
/** 路径 = 存在的**档**?(目录 / 缺失 → false——写门存在性判据用)。 */
|
|
43
|
-
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
44
|
-
|
|
45
43
|
/** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-10):写命令落盘前判**结果行**——`status ∈
|
|
46
44
|
* {在途, 待核销}` ⇒ 判 `task_book`:`null`(缺指针)⇒ 拒(「必填」文案);非 `null` ⇒ 文件部分
|
|
47
45
|
* (首个 `§` 前子串,trim)须经基准 `resolve(base, …)` 指向存在的档;缺文件部分(`§2` / 空串 /
|
|
@@ -50,23 +48,17 @@ const isFile = (p) => { try { return statSync(p).isFile() } catch { return false
|
|
|
50
48
|
* 基准 `base` = **与台账库关联键同源**(2026-09-18 fix 轮 · O1 收正):`resolveProjectRoot(cwd) ??
|
|
51
49
|
* resolve(cwd ?? ".")`——同表达式见 `ledger-db.mjs` `ledgerDbPath`;容器根会话(锚 cwd 下唯一注册
|
|
52
50
|
* 子仓 P)与子仓会话 ⇒ 库键与指针基准同取 P(同库同基准;原实现 `resolve(cwd, …)` = 原始 cwd 会误拒)。
|
|
53
|
-
* #832
|
|
51
|
+
* #832 ∥ #834:声明源前缀形同判——判定单源 = `declaration.mjs` `resolveDeclaredRef`(① 仓根解析 →
|
|
52
|
+
* ② 声明源前缀解析序;同名多仓 ⇒ 声明序首者)——**与迁移旗(`ledger-migrate.mjs` `writeGateFlag`)
|
|
53
|
+
* 同消费**;未声明 ∥ 声明根缺位 ∥ 目标档缺 ⇒ 照旧拒(既有拒绝面零改——判据本体 = §6.1 口径 1–4 逐字)。 */
|
|
54
54
|
function assertTaskBookGate(cwd, fnName, status, taskBook) {
|
|
55
55
|
if (status !== "在途" && status !== "待核销") return
|
|
56
56
|
if (taskBook == null) throw new Error(`${fnName}:task_book 必填(在途 / 待核销 须携任务书指针)`)
|
|
57
57
|
const part = String(taskBook).split("§")[0].trim()
|
|
58
58
|
if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
|
|
59
59
|
const base = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
|
|
60
|
-
const abs =
|
|
61
|
-
|
|
62
|
-
if (isFile(abs)) return
|
|
63
|
-
const segs = part.replace(/\\/g, "/").split("/").filter(Boolean)
|
|
64
|
-
for (const root of declaredPublicRoots(base)) {
|
|
65
|
-
if (segs[0] !== basename(root)) continue
|
|
66
|
-
if (segs.length > 1 && isFile(resolve(root, segs.slice(1).join("/")))) return
|
|
67
|
-
break
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
60
|
+
const { ok, abs } = resolveDeclaredRef(base, part)
|
|
61
|
+
if (!ok) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
/** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。 */
|
package/ledger-db.mjs
CHANGED
|
@@ -55,6 +55,10 @@ export function ledgerDbPath(cwd) {
|
|
|
55
55
|
/** 未决四态(「活条目」口径 = 计数单源 WHERE 集;已核销 / 已废弃 = 归档态)。 */
|
|
56
56
|
export const PENDING_STATUSES = ["待讨论", "待设计", "在途", "待核销"]
|
|
57
57
|
|
|
58
|
+
/** 台账库结构版本(N2 标记**单源**——`PRAGMA user_version` 落标:写面开库 `v < 本值 ⇒ 置`;
|
|
59
|
+
* 读面如实回读(旧库未标 = 0);结构变更(新列 / 新表)同步升号——2026-10-03 read-data-interface 批)。 */
|
|
60
|
+
export const LEDGER_SCHEMA_VERSION = 1
|
|
61
|
+
|
|
58
62
|
/** 六态允许迁移表(ledgerUpdate 迁移前判,不在表内 → 拒——规格 ②.2;设计档 §2.1)。 */
|
|
59
63
|
export const ALLOWED_MIGRATIONS = {
|
|
60
64
|
"待讨论": ["待设计", "已废弃"],
|
|
@@ -100,12 +104,39 @@ export function ensureExecutorColumn(db, { exists } = {}) {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/** 只读态开库(read-data-interface 批 FR2③——readOnly 句柄 + **零 DDL / 零 ALTER**;先例 = 本包
|
|
108
|
+
* `ledger-migrate.mjs` 的 `openRead`):文件必已在盘;items 表缺失探针 ⇒ 视同空账返回 null
|
|
109
|
+
* (非台账库的 SQLite 档 / 空档——观测面保持「空集」,文件零触)。不可开(坏档 / 非 SQLite)⇒
|
|
110
|
+
* 抛(调用方按面处置:单范围 = 读错;族范围 = 不可读跳过)。 */
|
|
111
|
+
function openReadOnly(file) {
|
|
112
|
+
let db
|
|
113
|
+
try {
|
|
114
|
+
db = new DatabaseSync(file, { readOnly: true })
|
|
115
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'items'").get() === undefined) {
|
|
116
|
+
db.close()
|
|
117
|
+
return null // 无 items 表 ⇒ 非台账库 ⇒ 空账(探针零写)
|
|
118
|
+
}
|
|
119
|
+
return db
|
|
120
|
+
} catch (e) {
|
|
121
|
+
try { db?.close() } catch { /* 已关 / 未开 */ }
|
|
122
|
+
throw new Error(`台账库打开失败:${file}(${e.message})`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 写面落标(N2):`user_version` < 常量 ⇒ 置;≥ 常量不动(未来版本零回退)。只在写面开库调用。 */
|
|
127
|
+
function markSchemaVersion(db) {
|
|
128
|
+
const v = Number(db.prepare("PRAGMA user_version").get().user_version ?? 0)
|
|
129
|
+
if (v < LEDGER_SCHEMA_VERSION) db.exec(`PRAGMA user_version = ${LEDGER_SCHEMA_VERSION}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
/** 开库(cwd = 项目根——仅作关联键)→ DatabaseSync 句柄 + 幂等建表 + 老库幂等迁移(executor 列)。
|
|
104
133
|
* 读面(create=false):库文件不存在 → 返回 null(空账——读面不建库、无副作用)。
|
|
105
134
|
* 写面(create=true):库文件不存在自动建(台账目录随之创建);项目根目录不存在 → 抛友好错误。
|
|
135
|
+
* **只读态**(readOnly=true——读面专用):只读句柄 + 零 DDL / ALTER + items 探针(见 openReadOnly);
|
|
136
|
+
* 如实副作用:旧库(缺 executor 列)**读**不再自动补列——补列归写面开库;读出行缺列按 null 归一。
|
|
106
137
|
* **歧义锚**(≥2 候选——`projectRootView` `ambiguous` 态)⇒ **显式拒**(列候选 + 显式项目根指引;
|
|
107
138
|
* 零写——不静默回退锚;族锚「项目不可解析」,AC-M2-17);兜底 `resolve(cwd ?? ".")` 只属 none 态。 */
|
|
108
|
-
export function openLedger(cwd, { create = false } = {}) {
|
|
139
|
+
export function openLedger(cwd, { create = false, readOnly = false } = {}) {
|
|
109
140
|
const dir = resolve(cwd ?? ".")
|
|
110
141
|
// 歧义锚 ⇒ 显式拒(零写——消「缺省空读 / 错写落锚」双面症;读 / 写五工具同经本门)。
|
|
111
142
|
const view = projectRootView(dir)
|
|
@@ -118,11 +149,13 @@ export function openLedger(cwd, { create = false } = {}) {
|
|
|
118
149
|
throw new Error(`项目目录不存在:${dir}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
|
|
119
150
|
}
|
|
120
151
|
if (!existsSync(file) && !create) return null
|
|
152
|
+
if (readOnly) return openReadOnly(file)
|
|
121
153
|
try { mkdirSync(ledgerDir, { recursive: true }) } catch { /* 已存在 / 并发建目录竞争——忽略 */ }
|
|
122
154
|
const db = new DatabaseSync(file)
|
|
123
155
|
try {
|
|
124
156
|
db.exec(DDL)
|
|
125
157
|
ensureExecutorColumn(db)
|
|
158
|
+
markSchemaVersion(db)
|
|
126
159
|
} catch (e) {
|
|
127
160
|
db.close()
|
|
128
161
|
throw new Error(`台账库打开失败:${file}(${e.message})`)
|
package/ledger-migrate.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { copyFileSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSy
|
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path"
|
|
12
12
|
import { DatabaseSync } from "node:sqlite"
|
|
13
13
|
|
|
14
|
+
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
14
15
|
import { ledgerDirPath, ledgerKey, openLedger } from "./ledger-db.mjs"
|
|
15
16
|
import { resolveProjectRoot } from "./manifest.mjs"
|
|
16
17
|
|
|
@@ -71,14 +72,14 @@ function readRowsOf(file, missing = []) {
|
|
|
71
72
|
const db = openRead(file)
|
|
72
73
|
try { return readRows(db, missing) } finally { db.close() }
|
|
73
74
|
}
|
|
74
|
-
/** 写门风险旗(§2.2 表):迁入行属 `在途 / 待核销` 且 `task_book` 不可解析 / 指向档不存在 ⇒ 该行后续更新将拒于 §6.1
|
|
75
|
+
/** 写门风险旗(§2.2 表):迁入行属 `在途 / 待核销` 且 `task_book` 不可解析 / 指向档不存在 ⇒ 该行后续更新将拒于 §6.1 写门;只出旗,不拦截(判面与写门同源·#834——共享解析单点 `resolveDeclaredRef`:① 仓根解析 → ② 声明源前缀解析序)。 */
|
|
75
76
|
function writeGateFlag(values, projectRoot) {
|
|
76
77
|
const tb = values[5]
|
|
77
78
|
if ((values[1] !== "在途" && values[1] !== "待核销") || tb == null || tb === "") return null
|
|
78
79
|
const part = String(tb).split("§")[0].trim()
|
|
79
80
|
if (!part) return `task_book 不可解析(缺文件部分):${tb}`
|
|
80
|
-
const abs =
|
|
81
|
-
return
|
|
81
|
+
const { ok, abs } = resolveDeclaredRef(projectRoot, part)
|
|
82
|
+
return ok ? null : `task_book 指向的档不存在:${tb}(解析 = ${abs})`
|
|
82
83
|
}
|
|
83
84
|
/** `--from` 指名键 = **补充源**:键形非 16 位小写十六进制 / 无对应库 / 不可开 ⇒ 拒跑(fail-closed,零写——显式指名不静默跳过;键形判防路径形 / 越目录形取值 join 出台账目录外)。 */
|
|
84
85
|
function fromKeyRefusal(fromKeys, dir) {
|
package/ledger-read.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-read.mjs — 只读数据接口(read-data-interface 批 · 设计档
|
|
3
|
+
* `docs/cli/design/READ-DATA-INTERFACE.md` §2.1 / §2.3 / §2.4 / §2.7 · 台账 #886 ∥ #887)。
|
|
4
|
+
*
|
|
5
|
+
* 面:① `ledgerExport({ cwd, family, full })` —— CLI `ledger list --json` ∥ ACP `ledger/list`
|
|
6
|
+
* 两出口共用的**统一序列化单源**(N1:零 SQL 复刻、零表名泄漏——行集直用核 `ledgerQuery`);
|
|
7
|
+
* ② `runLedgerList(args, opts)` —— CLI 命令 runner(严格解析 · fail-closed——§2.7 / §3.1)。
|
|
8
|
+
*
|
|
9
|
+
* 载荷(§3.2 冻结形):`{ projects: [{ root, name, schemaVersion, rows }] }`;行 = `id` + 12 数据列
|
|
10
|
+
* (列集单源 = `DATA_COLUMNS`——D-10 直引,不另立副本;`evidence` 仅 full 入行);缺列(旧库)按
|
|
11
|
+
* null 归一;`schemaVersion` = 库 `PRAGMA user_version` 如实回读(旧库未标 = 0);库不在 / 非台账库
|
|
12
|
+
* ⇒ 项目不出项(空集)。
|
|
13
|
+
* 范围(§2.4):单范围 = 解析后项目根一项(`resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")`——与库键
|
|
14
|
+
* 同源);`--family` = `discoverFamily` 发现序逐项(不可读跳过——既有语义)。
|
|
15
|
+
*
|
|
16
|
+
* W8 契约②:本档静态入 `ledger-db.mjs`(node:sqlite)⇒ 消费侧一律**动态 import** 本档。
|
|
17
|
+
* D-11:`ledger.mjs` 零编辑——族发现只 import `discoverFamily`。
|
|
18
|
+
*/
|
|
19
|
+
import { basename, resolve } from "node:path"
|
|
20
|
+
|
|
21
|
+
import { ledgerQuery } from "./ledger-cmd.mjs"
|
|
22
|
+
import { discoverFamily } from "./ledger.mjs"
|
|
23
|
+
import { DATA_COLUMNS } from "./ledger-migrate.mjs"
|
|
24
|
+
import { resolveProjectRoot } from "./manifest.mjs"
|
|
25
|
+
import { openLedger } from "./ledger-db.mjs"
|
|
26
|
+
|
|
27
|
+
/** 行出形(§3.2):`id` + 数据列序(`evidence` 仅 full);缺列按 null 归一。 */
|
|
28
|
+
function exportRow(r, full) {
|
|
29
|
+
const row = { id: Number(r.id) }
|
|
30
|
+
for (const c of DATA_COLUMNS) {
|
|
31
|
+
if (c === "evidence" && !full) continue
|
|
32
|
+
row[c] = r[c] ?? null
|
|
33
|
+
}
|
|
34
|
+
return row
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 单项目读(**两开均只读态**——版本探针开 + 行集开,§2.3):库不在 / 非台账库 ⇒ null(项目不出项)。
|
|
38
|
+
* 不可读(坏档)⇒ 抛(单范围 = 读错转 exit 1;族范围 = 调用侧捕获跳过)。 */
|
|
39
|
+
function readProject(root, full) {
|
|
40
|
+
const probe = openLedger(root, { readOnly: true })
|
|
41
|
+
if (!probe) return null
|
|
42
|
+
let schemaVersion
|
|
43
|
+
try { schemaVersion = Number(probe.prepare("PRAGMA user_version").get().user_version ?? 0) } finally { probe.close() }
|
|
44
|
+
const rows = ledgerQuery({ cwd: root })
|
|
45
|
+
return { root, name: basename(root), schemaVersion, rows: rows.map((r) => exportRow(r, full)) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 统一序列化单源(N1——两出口共用;载荷 = §3.2 冻结形)。
|
|
49
|
+
* 单范围 = 当前项目一项;`family=true` = 族内发现序逐项(不可读跳过——余者照常)。 */
|
|
50
|
+
export function ledgerExport({ cwd, family = false, full = false } = {}) {
|
|
51
|
+
const anchor = cwd ?? process.cwd()
|
|
52
|
+
if (!family) {
|
|
53
|
+
const root = resolveProjectRoot(anchor) ?? resolve(anchor)
|
|
54
|
+
const item = readProject(root, full)
|
|
55
|
+
return { projects: item ? [item] : [] }
|
|
56
|
+
}
|
|
57
|
+
const projects = []
|
|
58
|
+
for (const p of discoverFamily(anchor).projects) {
|
|
59
|
+
try { const item = readProject(p.root, full); if (item) projects.push(item) } catch { /* 不可读跳过 */ }
|
|
60
|
+
}
|
|
61
|
+
return { projects }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** CLI 命令 usage(冻结形——§3.1;解析错入 stderr)。 */
|
|
65
|
+
const LIST_USAGE = "Usage: thincoder ledger list --json [--full] [--family] [--cwd <dir>]"
|
|
66
|
+
|
|
67
|
+
/** CLI runner(§2.7):严格解析——未知参 / 缺 `--json` / `--cwd` 缺值 ⇒ usage + exit 1(fail-closed,
|
|
68
|
+
* 与同族 migrate / audit 宽松面刻意不同);`--cwd` 空格形(同族先例)。成功 = stdout 单段 JSON
|
|
69
|
+
* (零杂行)+ exit 0(含空集);解析类 / 读错 ⇒ stderr 一行消息(零栈泄)+ exit 1。 */
|
|
70
|
+
export function runLedgerList(args, { cwd = process.cwd(), out = console.log, err = console.error } = {}) {
|
|
71
|
+
const argv = Array.isArray(args) ? args : []
|
|
72
|
+
let json = false, full = false, family = false, dir = null
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i]
|
|
75
|
+
if (a === "--json") json = true
|
|
76
|
+
else if (a === "--full") full = true
|
|
77
|
+
else if (a === "--family") family = true
|
|
78
|
+
else if (a === "--cwd") {
|
|
79
|
+
const v = argv[i + 1]
|
|
80
|
+
if (v === undefined || v.startsWith("--")) { err(LIST_USAGE); return 1 }
|
|
81
|
+
dir = String(v)
|
|
82
|
+
i++
|
|
83
|
+
} else { err(LIST_USAGE); return 1 }
|
|
84
|
+
}
|
|
85
|
+
if (!json) { err(LIST_USAGE); return 1 }
|
|
86
|
+
try {
|
|
87
|
+
out(JSON.stringify(ledgerExport({ cwd: dir ?? cwd, family, full })))
|
|
88
|
+
return 0
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// 错误面「一行消息、零栈泄」(§3.1):多行诊断(如歧义锚候选清单)折叠单行——内容零丢。
|
|
91
|
+
err(String(e?.message ?? e).replace(/\s*\r?\n\s*/g, " "))
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
}
|
package/ledger-surface.mjs
CHANGED
|
@@ -1,76 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ledger-surface.mjs — 台账可见面(核内结构归位;CORE-UNIFICATION §2.5 #174
|
|
3
|
-
* 「融合:取一侧 + 渲染面按端注入」)。
|
|
2
|
+
* ledger-surface.mjs — 台账可见面(核内结构归位;CORE-UNIFICATION §2.5 #174「融合:取一侧」)。
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 是 TUI 专用件的颜色表 ⇒ ④ 端差,改由调用方传入 `colors`(未注入 ⇒ 空对象,输出纯文本,
|
|
8
|
-
* 核内零端名分支——契约 5 / 10)。
|
|
9
|
-
*
|
|
10
|
-
* 三面:① 启动行(首扫、**任一项目可动作**才出——明细行集每项目一行)② 变化行(老化首次
|
|
11
|
-
* 越线 / 池达阈值——一次性去重 + **送达门**:未送达不记账)③ 状态位(`state.ledger`——L1
|
|
12
|
-
* 常驻标记,渲染端消费;**不受启动行门约束**——F2 按标记态照显)。
|
|
13
|
-
* 只读台账;唯一写面 = 去重档(`~/.thincoder/ledger-notify.json`)。
|
|
4
|
+
* 面:族扫描 → 判活批量解析 → 状态位(`state.ledger`——L1 常驻标记,范围归约 `scopeMarkerOf`
|
|
5
|
+
* 单源 §7.2,渲染端消费);只读台账,本面零写。
|
|
14
6
|
*/
|
|
15
|
-
import { buildScan,
|
|
7
|
+
import { buildScan, discoverFamily, REFRESH_MS, resolveExecutorStates, scopeMarkerOf } from "./ledger.mjs"
|
|
16
8
|
|
|
17
9
|
/** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。**async**(F-LX1:await 判活解析——
|
|
18
|
-
* LEDGER.md §7.3.1 ④ 读面不阻塞事件循环)。
|
|
19
|
-
|
|
20
|
-
* `colors` = 端注入的渲染色表(`{ warn, dim }`);未注入 ⇒ `{}`(`undefined` 色值 = 纯文本)。 */
|
|
21
|
-
export async function runLedgerScan({ state, agent = null, anchor = null, notifyFile = NOTIFY_FILE, pushLine = () => {}, render = () => {}, startup = false, colors = {} } = {}) {
|
|
10
|
+
* LEDGER.md §7.3.1 ④ 读面不阻塞事件循环)。 */
|
|
11
|
+
export async function runLedgerScan({ state, agent = null, anchor = null, render = () => {} } = {}) {
|
|
22
12
|
const base = anchor ?? agent?.cwd ?? process.cwd()
|
|
23
13
|
const family = discoverFamily(base)
|
|
24
14
|
const scans = []
|
|
25
15
|
for (const p of family.projects) {
|
|
26
16
|
try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
|
|
27
17
|
}
|
|
28
|
-
const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
|
|
29
18
|
await resolveExecutorStates(scans) // 判活批量解析(一次探束 + TTL 缓存;无在途零 exec)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const rows = showStartup
|
|
35
|
-
? [...changeLines, ...detailScans(scans, current).map((s) => ({ text: formatDetailLine(s), warn: s.actionable }))]
|
|
36
|
-
: changeLines
|
|
37
|
-
let delivered = true
|
|
38
|
-
for (const line of rows) {
|
|
39
|
-
try { pushLine(line.text, line.warn ? colors.warn : colors.dim) } catch { delivered = false; break }
|
|
40
|
-
}
|
|
41
|
-
// 送达门(F5):有事件行 → 仅在送达后记账;无事件行 → 仅状态确有变化时静默同步(跌回 false 即重置)
|
|
42
|
-
const changed = plans.some(({ s, plan }) => {
|
|
43
|
-
const prev = notify.ledgers[notifyKey(s.ledger)]
|
|
44
|
-
return !prev || JSON.stringify(prev.aged ?? []) !== JSON.stringify(plan.next.aged) || Boolean(prev.threshold) !== plan.next.threshold
|
|
45
|
-
})
|
|
46
|
-
if (changeLines.length ? delivered : changed) {
|
|
47
|
-
const ledgers = { ...notify.ledgers }
|
|
48
|
-
for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
|
|
49
|
-
saveNotifyState(notifyFile, { version: 1, ledgers })
|
|
50
|
-
}
|
|
51
|
-
// warn 判位钉在判活解析之后(裁定 #7):属主已死也是可动作态
|
|
52
|
-
state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && (current.aged > 0 || current.deadExecutors > 0)), scannedAt: Date.now() }
|
|
19
|
+
// 范围归约 = `scopeMarkerOf` 单源(§7.2——容器根锚求和 ∥ 具体项目锚只显自身);warn 判位钉在
|
|
20
|
+
// 判活解析之后(裁定 #7):属主已死也是可动作态
|
|
21
|
+
const { marker, warn } = scopeMarkerOf(scans, family)
|
|
22
|
+
state.ledger = { marker, warn, scannedAt: Date.now() }
|
|
53
23
|
render()
|
|
54
24
|
}
|
|
55
25
|
|
|
56
26
|
/** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。
|
|
57
|
-
* 端注入的渲染面(`ctx.colors`)与推送面(`ctx.pushLine` / `ctx.render`)照传。
|
|
58
27
|
* `tick` 异步化(F-LX1——runLedgerScan async):await 且 catch 不崩(N1)。重叠防衛:
|
|
59
28
|
* 判活探测秒级可达,上一轮未落定时跳过本轮(防探束堆积)。 */
|
|
60
29
|
export function startLedgerSurface(ctx = {}) {
|
|
61
30
|
let disposed = false
|
|
62
31
|
let timer = null
|
|
63
32
|
let inflight = false
|
|
64
|
-
const tick = async (
|
|
33
|
+
const tick = async () => {
|
|
65
34
|
if (disposed || inflight) return
|
|
66
|
-
if (
|
|
35
|
+
if (ctx.state?.processing) return // 扫描不打断回合帧
|
|
67
36
|
inflight = true
|
|
68
|
-
try { await runLedgerScan(
|
|
37
|
+
try { await runLedgerScan(ctx) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
|
|
69
38
|
}
|
|
70
39
|
setImmediate(() => {
|
|
71
40
|
if (disposed) return
|
|
72
|
-
tick(
|
|
73
|
-
timer = setInterval(() => tick(
|
|
41
|
+
tick()
|
|
42
|
+
timer = setInterval(() => tick(), REFRESH_MS)
|
|
74
43
|
timer.unref?.()
|
|
75
44
|
})
|
|
76
45
|
return {
|
package/ledger.mjs
CHANGED
|
@@ -9,20 +9,18 @@
|
|
|
9
9
|
* 两源,接线见 tools/index.mjs 与 agent/family-tools.mjs)
|
|
10
10
|
* ② 族发现(findProject / discoverFamily——标记 = ledger.db)③ scan 组装(buildScan——行集 + ledgerCount
|
|
11
11
|
* → scan 对象,形状契约 = pool/tech/aged/thresholdReached/actionable/root/name/ledger)
|
|
12
|
-
* ④
|
|
12
|
+
* ④ 格式 helper(L1 标记 ∥ L2 明细行逐字契约——含标记范围归约 `scopeMarkerOf`,§7.2)。
|
|
13
13
|
* 判活展示 = 拆分件 `ledger-executors.mjs`(在途 executor 解析 + 三态尾段——本档 re-export,
|
|
14
14
|
* 消费面仍只认本档)。
|
|
15
15
|
*
|
|
16
16
|
* W8 契约②:ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧一律**动态 import** 本档(禁止静态 import)。
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { opendirSync, statSync } from "node:fs"
|
|
19
19
|
import { basename, dirname, join, resolve } from "node:path"
|
|
20
|
-
import {
|
|
20
|
+
import { projectRootView } from "./manifest.mjs"
|
|
21
21
|
import { ledgerDbPath, PENDING_STATUSES } from "./ledger-db.mjs"
|
|
22
22
|
import { ledgerQuery } from "./ledger-cmd.mjs"
|
|
23
23
|
import { executorTail } from "./ledger-executors.mjs"
|
|
24
|
-
// 归一步单源(#585——盘符大写契约直引 `session-slots.mjs`;先例 = `ledger-db.mjs:23`)。
|
|
25
|
-
import { normalizeCwd } from "./session-slots.mjs"
|
|
26
24
|
|
|
27
25
|
/** 老化阈值(天——口径 = 需求档;`days` 参数可覆盖)。 */
|
|
28
26
|
export const AGING_DAYS = 30
|
|
@@ -32,18 +30,16 @@ export const THRESHOLD_BOARD = 2
|
|
|
32
30
|
export const THRESHOLD_POOL = 3
|
|
33
31
|
/** 刷新周期(N2 实现常量;VSC 端另有换项目事件触发)。 */
|
|
34
32
|
export const REFRESH_MS = 120000
|
|
35
|
-
/**
|
|
36
|
-
export const NOTIFY_FILE = join(configDir, "ledger-notify.json")
|
|
37
|
-
/** 空族提示(仅命令面——运行时面静默,N1 / 设计档 §2.30.3.3)。 */
|
|
33
|
+
/** 空族提示(仅命令面——运行时面静默,N1 / 归档档 ENGINEERING-MODE.md §2.30.3.3——现行承接 = `docs/core/design/LEDGER.md`)。 */
|
|
38
34
|
export const EMPTY_FAMILY_LINE = "台账:未发现台账。"
|
|
39
35
|
|
|
40
36
|
/** 条目归一化文本 = 条目键(去 `- [ ] ` 前缀 + 去首尾空白 + 连续空白折叠单空格)。
|
|
41
|
-
* 位置无关(位移 / 他条编辑稳定);自身文本变更 =
|
|
37
|
+
* 位置无关(位移 / 他条编辑稳定);自身文本变更 = 键变(最坏一次重报)。归档档 ENGINEERING-MODE.md §2.30.3.2。 */
|
|
42
38
|
export function normalizeEntry(text) {
|
|
43
39
|
return text.replace(/^- \[[ x]\]\s+/, "").trim().replace(/\s+/g, " ")
|
|
44
40
|
}
|
|
45
41
|
|
|
46
|
-
/** 条目标题:首个 `**…**` 段;无粗体段 → 归一化文本前 20 字(超出加
|
|
42
|
+
/** 条目标题:首个 `**…**` 段;无粗体段 → 归一化文本前 20 字(超出加 `…`)。归档档 ENGINEERING-MODE.md §2.30.3.3 L3。 */
|
|
47
43
|
export function entryTitle(text) {
|
|
48
44
|
const m = /\*\*(.+?)\*\*/.exec(text)
|
|
49
45
|
if (m) return m[1]
|
|
@@ -91,9 +87,14 @@ function ledgerChildren(dir) {
|
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
/** 项目族发现:{current, projects}——projects = current + 其同级含台账库目录(**发现序:current 在前**,
|
|
94
|
-
* 其余按目录名升序);current 缺(容器目录,K6)→ 上下文目录向上最近「含台账子目录」者取其子目录。
|
|
90
|
+
* 其余按目录名升序);current 缺(容器目录,K6)→ 上下文目录向上最近「含台账子目录」者取其子目录。
|
|
91
|
+
* **歧义根不充当项目**(轻通道轮 · 2026-10-04):命中锚若项目解析 `ambiguous`(≥2 候选——`projectRootView`)
|
|
92
|
+
* ⇒ 该锚不可扫描(`openLedger` 歧义拒)——其键控库(先于 #828 写门的存量空壳 / 幽灵)不得遮蔽族发现;
|
|
93
|
+
* 按容器形落子目录族(需求:打开容器根 ⇒ 族内合计——标记范围批 §1 原话)。 */
|
|
95
94
|
export function discoverFamily(anchor) {
|
|
96
|
-
const
|
|
95
|
+
const hit = findProject(anchor)
|
|
96
|
+
// 歧义根跳过(键控库存在 ≠ 可作项目——扫描面 `buildScan` 对歧义锚必拒;遮蔽则标记静默缺席)。
|
|
97
|
+
const current = hit && projectRootView(hit.root).state === "ambiguous" ? null : hit
|
|
97
98
|
if (current) {
|
|
98
99
|
const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
|
|
99
100
|
return { current, projects: [current, ...siblings] }
|
|
@@ -143,27 +144,39 @@ export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
/** L1
|
|
147
|
+
/** L1 状态标记(逐字——归档档 ENGINEERING-MODE.md §2.30.3.3)。 */
|
|
147
148
|
export function formatMarker(scan) {
|
|
148
149
|
return scan ? `台账 ${scan.pool}·${scan.tech}` : null
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
/** 标记范围归约(L1 取值单源——LEDGER.md §7.2「标记范围」):`current` 在场 ⇒ 范围 = `scans` 中
|
|
153
|
+
* `root` = `family.current.root` 的 scan(命中但不在 `scans`——不可读 ⇒ 空范围,不掺兄弟);
|
|
154
|
+
* `current` 缺席(容器根锚)⇒ 范围 = `scans` 全体(族内已读项目——构建期不可读已跳过)。
|
|
155
|
+
* 空范围 ⇒ `{ marker: null, warn: false }`(不落 `0·0`);范围非空 ⇒ `marker` = `formatMarker`
|
|
156
|
+
* 逐字(两池分列求和——F1);`warn` = 范围内任一项 `aged>0 ∨ deadExecutors>0`(端零重算)。
|
|
157
|
+
* 消费面 = 核拍面(`ledger-surface.mjs`)∥ VSC item——端零自算单源。 */
|
|
158
|
+
export function scopeMarkerOf(scans, family) {
|
|
159
|
+
const list = Array.isArray(scans) ? scans : []
|
|
160
|
+
const currentRoot = family?.current?.root ?? null
|
|
161
|
+
const range = currentRoot === null ? list : list.filter((s) => s?.root === currentRoot)
|
|
162
|
+
if (range.length === 0) return { marker: null, warn: false }
|
|
163
|
+
let pool = 0
|
|
164
|
+
let tech = 0
|
|
165
|
+
let warn = false
|
|
166
|
+
for (const s of range) {
|
|
167
|
+
pool += s.pool
|
|
168
|
+
tech += s.tech
|
|
169
|
+
if (s.aged > 0 || s.deadExecutors > 0) warn = true
|
|
170
|
+
}
|
|
171
|
+
return { marker: formatMarker({ pool, tech }), warn }
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
/** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`;判活尾段 = executorTail 三态——
|
|
152
175
|
* 在途 executor 缺席时尾段空串,既有逐字断言零破)。 */
|
|
153
176
|
export function formatDetailLine(scan) {
|
|
154
177
|
return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}${executorTail(scan)}`
|
|
155
178
|
}
|
|
156
179
|
|
|
157
|
-
/** L3 变化行·老化(titles = 新增老化条目标题;> 3 条时第三项后接 `;…`)。 */
|
|
158
|
-
export function formatAgingLine(scan, titles) {
|
|
159
|
-
return `台账变化:${scan.name} 老化首次越线 ${titles.length} 条(超 30 天未处置):${titles.slice(0, 3).join(";")}${titles.length > 3 ? ";…" : ""}`
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** L4 变化行·阈值。 */
|
|
163
|
-
export function formatThresholdLine(scan) {
|
|
164
|
-
return `台账变化:${scan.name} 需求池达阈值(${scan.pool} 条)— 可开批`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
180
|
/** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
|
|
168
181
|
export function detailScans(scans, current) {
|
|
169
182
|
const out = []
|
|
@@ -172,45 +185,6 @@ export function detailScans(scans, current) {
|
|
|
172
185
|
return out
|
|
173
186
|
}
|
|
174
187
|
|
|
175
|
-
/** 变化行规划(纯函数):{lines:[{text,warn}], next:{aged,threshold}}(去重口径 §2.30.3.2)。 */
|
|
176
|
-
export function planChangeLines(prev, scan) {
|
|
177
|
-
const p = prev && typeof prev === "object" ? prev : {}
|
|
178
|
-
const before = new Set(Array.isArray(p.aged) ? p.aged : [])
|
|
179
|
-
const freshTitles = scan.agedKeys.map((k, i) => (before.has(k) ? null : scan.agedTitles[i])).filter((t) => t != null)
|
|
180
|
-
const lines = []
|
|
181
|
-
if (freshTitles.length) lines.push({ text: formatAgingLine(scan, freshTitles), warn: true })
|
|
182
|
-
if (scan.thresholdReached && !p.threshold) lines.push({ text: formatThresholdLine(scan), warn: true })
|
|
183
|
-
return { lines, next: { aged: scan.agedKeys, threshold: scan.thresholdReached } }
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** 去重档键 = 台账库绝对路径·正斜杠 + **盘符大写**(跨端同规则——CLI 的 `process.cwd()` 盘符大写、
|
|
187
|
-
* VSC 的 `uri.fsPath` 小写(`session-slots.mjs` `normalizeCwd` 同一契约——session / checkpoint /
|
|
188
|
-
* trace 跨端共享即赖此);盘符步 = `normalizeCwd` 直调(单源——#585;先例 = `ledger-db.mjs:23`)、
|
|
189
|
-
* 分隔符折叠自持;CLI 写的键 VSC 须逐字认得)。 */
|
|
190
|
-
export function notifyKey(ledger) {
|
|
191
|
-
return normalizeCwd(resolve(ledger).replace(/\\/g, "/"))
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** 去重档读(缺失 / 坏 JSON → 空态——N1 降级不崩)。 */
|
|
195
|
-
export function loadNotifyState(file = NOTIFY_FILE) {
|
|
196
|
-
try {
|
|
197
|
-
const j = JSON.parse(readFileSync(file, "utf8"))
|
|
198
|
-
if (j && typeof j === "object" && j.ledgers && typeof j.ledgers === "object") return { version: 1, ledgers: j.ledgers }
|
|
199
|
-
} catch { /* 缺失 / 坏档 → 空态 */ }
|
|
200
|
-
return { version: 1, ledgers: {} }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** 去重档写(temp + rename 防撕裂;写失败静默返回 false——唯一写面,仅送达后调)。 */
|
|
204
|
-
export function saveNotifyState(file, state) {
|
|
205
|
-
try {
|
|
206
|
-
mkdirSync(dirname(file), { recursive: true })
|
|
207
|
-
const tmp = `${file}.tmp-${process.pid}`
|
|
208
|
-
writeFileSync(tmp, JSON.stringify(state), "utf8")
|
|
209
|
-
renameSync(tmp, file)
|
|
210
|
-
return true
|
|
211
|
-
} catch { return false }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
188
|
// ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3) ──
|
|
215
189
|
export { ALLOWED_MIGRATIONS, ledgerDbPath, ledgerKey, ledgerDirPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest, ensureExecutorColumn } from "./ledger-db.mjs"
|
|
216
190
|
export { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
|
package/manifest-schema.mjs
CHANGED
|
@@ -80,14 +80,24 @@ function isLineCountsValue(v) {
|
|
|
80
80
|
* 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
|
|
81
81
|
* 指针腿已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
|
|
82
82
|
* 缺键 = 便利 fallback(补默认值),不是错(与整档缺失两分——KD-M1-2)。
|
|
83
|
+
* 未知键(#802 / KD-M1-36):顶层 + 四嵌套层产 `unknownKeys`(非拒——ok 定性零变;产线恰 =
|
|
84
|
+
* fillDefaults 丢弃面;嵌套键用点形路径——同 missingKeys 口径)。
|
|
83
85
|
* @param {object} obj 待校验 manifest 对象
|
|
84
|
-
* @returns {{ok:boolean, errors:string[], missingKeys:string[]}}
|
|
86
|
+
* @returns {{ok:boolean, errors:string[], missingKeys:string[], unknownKeys:string[]}}
|
|
85
87
|
*/
|
|
86
88
|
export function validateManifest(obj) {
|
|
87
89
|
const errors = []
|
|
88
90
|
const missingKeys = []
|
|
89
91
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
90
|
-
return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys }
|
|
92
|
+
return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys, unknownKeys: [] }
|
|
93
|
+
}
|
|
94
|
+
// #802(KD-M1-36——未知键可见化):顶层 + 四嵌套层逐键对照(非拒——ok 定性零变;嵌套键点形)。
|
|
95
|
+
const unknownKeys = []
|
|
96
|
+
for (const key of Object.keys(obj)) if (!MANIFEST_SCHEMA.keys.includes(key)) unknownKeys.push(key)
|
|
97
|
+
for (const [nested, subkeys] of Object.entries(MANIFEST_SCHEMA.nestedKeys)) {
|
|
98
|
+
const value = obj[nested]
|
|
99
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue
|
|
100
|
+
for (const sub of Object.keys(value)) if (!subkeys.includes(sub)) unknownKeys.push(`${nested}.${sub}`)
|
|
91
101
|
}
|
|
92
102
|
for (const key of MANIFEST_SCHEMA.keys) {
|
|
93
103
|
if (!(key in obj)) missingKeys.push(key)
|
|
@@ -138,7 +148,7 @@ export function validateManifest(obj) {
|
|
|
138
148
|
if (obj.version !== undefined && (typeof obj.version !== "number" || !Number.isFinite(obj.version))) {
|
|
139
149
|
errors.push(`version 非法:${JSON.stringify(obj.version)}(须为数值)`)
|
|
140
150
|
}
|
|
141
|
-
return { ok: errors.length === 0, errors, missingKeys }
|
|
151
|
+
return { ok: errors.length === 0, errors, missingKeys, unknownKeys }
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/** 缺键补默认值(module 设计 §2.2 管线):顶层缺键补默认、嵌套键(docRoot/checkConfig/index/advisor)
|