@thincoder/core 0.10.2 → 0.10.4
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 +65 -0
- package/advisor/citations.mjs +40 -4
- package/advisor/compaction.mjs +21 -2
- package/advisor/convergence.mjs +1 -1
- package/advisor/history.mjs +8 -6
- package/advisor/loop.mjs +29 -27
- package/advisor/messages.mjs +5 -4
- package/advisor/run.mjs +5 -1
- package/advisor/timeline.mjs +26 -0
- package/advisor.mjs +4 -2
- package/agent/completion.mjs +3 -1
- package/agent/digest-account.mjs +188 -0
- package/agent/dispatch-run.mjs +29 -12
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +8 -0
- package/agent/record-results.mjs +10 -1
- package/agent/run-stages.mjs +27 -4
- package/agent/run-start.mjs +49 -3
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +60 -12
- package/agent/turn-loop.mjs +31 -2
- package/agent/write-gate.mjs +13 -25
- package/agent-tools/advisor-async.mjs +25 -7
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/advisor.mjs +34 -33
- package/agent-tools/async-settle.mjs +29 -5
- package/agent-tools/batch-lifecycle.mjs +29 -7
- package/agent-tools/batch-paths.mjs +22 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +59 -33
- package/agent-tools/batch.mjs +16 -6
- package/agent-tools/design-token.mjs +76 -16
- package/agent-tools/escalate-async.mjs +4 -2
- package/agent-tools/review-facts.mjs +143 -16
- package/agent-tools/subagent-actions-query.mjs +13 -6
- package/agent-tools/subagent-async.mjs +16 -4
- package/agent-tools/subagent-panel.mjs +121 -37
- package/agent-tools/subagent-spawn.mjs +1 -1
- package/agent-tools/subagent.mjs +5 -3
- package/agent-tools/verify.mjs +5 -0
- package/agent-tools/zero-write-watch.mjs +77 -0
- package/agent.mjs +1 -1
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +30 -0
- package/conventions.mjs +26 -6
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/escape.mjs +1 -1
- package/i18n.mjs +5 -0
- 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-tools.mjs +148 -85
- package/ledger-variant-notice.mjs +47 -0
- package/ledger.mjs +38 -64
- package/manifest-schema.mjs +19 -7
- package/manifest.mjs +32 -11
- 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/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/common.md +2 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +7 -4
- package/provider/anthropic.mjs +14 -2
- package/provider/core.mjs +10 -4
- package/provider/errors.mjs +23 -0
- package/provider/google.mjs +31 -4
- package/provider/normalize.mjs +57 -0
- package/provider/responses-request.mjs +3 -1
- package/provider/responses.mjs +38 -4
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session.mjs +13 -2
- package/shell-identity.mjs +107 -0
- package/stream-destroy.mjs +62 -0
- package/tool-docs/advisor.md +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/delete.md +7 -5
- package/tool-docs/ledger.md +6 -0
- package/tool-docs/subagent.md +10 -0
- package/tool-docs/verify.md +1 -1
- package/tools/bash.mjs +9 -4
- package/tools/edit-batch.mjs +7 -2
- package/tools/index.mjs +1 -4
- package/tools/patch.mjs +8 -2
- package/tools/write-path.mjs +6 -4
- package/undo-stack.mjs +23 -9
- package/tool-docs/ledger_add.md +0 -1
- package/tool-docs/ledger_close.md +0 -1
- package/tool-docs/ledger_count.md +0 -1
- package/tool-docs/ledger_query.md +0 -1
- package/tool-docs/ledger_update.md +0 -1
package/ledger-tools.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ledger-tools.mjs —
|
|
3
|
-
* 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* ledger-tools.mjs — 台账**统一入口** `ledger`(action 分派)+ 五旧名弃用壳 + 工具层参数守卫。
|
|
3
|
+
* 自 `ledger-cmd.mjs` 拆出(2026-09-28 守卫族微修批 · 台账 #473);2026-10-05 统一入口批
|
|
4
|
+
* (台账 #923)合五工具为一:模型面恰一名 `ledger`(两变体同名单对象、schema 窄化——
|
|
5
|
+
* 全量 = depth-0 五 action ∥ 只读 = depth>0 二 action);旧五名 = code 面弃用壳(不入任何装配面)。
|
|
6
|
+
* 守卫 = **声明派生**(设计档 §3.2 / §11.3:判序① 入参归一 → ② 入口级 action 判(消费 action)→
|
|
7
|
+
* ③ 余键喂该 action 伪工具对象走 P2–P6——声明源 = 前身旧工具 `parameters` 逐字;helper `assertToolArgs`
|
|
8
|
+
* 零改);判于核函数之前 ⇒ 非法入参零写、零库动作、零 SQLite 原文外泄。
|
|
9
|
+
* 接线:族出口 = `ledger.mjs`(re-export——消费面 agent/family-tools.mjs;旧读二装配面已清零);
|
|
10
|
+
* 消费侧一律动态 import(`ledger-db.mjs` 静态 import node:sqlite ⇒ W8 契约②)。
|
|
9
11
|
*/
|
|
10
12
|
import { ledgerAdd, ledgerClose, ledgerCount, ledgerQuery, ledgerUpdate } from "./ledger-cmd.mjs"
|
|
11
|
-
import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/
|
|
13
|
+
import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/ledger.md(DESC 单解析面)
|
|
12
14
|
|
|
13
15
|
/** 工具 cwd 供值面(缺省 → 当前项目根)。 */
|
|
14
16
|
const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
|
|
@@ -68,51 +70,22 @@ function assertToolArgs(tool, args) {
|
|
|
68
70
|
return args
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
// ──
|
|
73
|
+
// ── 统一入口(设计档 §11.2 / §11.3) ─────────────────────────────────────────
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
parameters: {
|
|
77
|
-
type: "object",
|
|
78
|
-
properties: {
|
|
79
|
-
cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
|
|
80
|
-
status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
|
|
81
|
-
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
|
|
82
|
-
board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
|
|
83
|
-
},
|
|
84
|
-
additionalProperties: false,
|
|
85
|
-
},
|
|
86
|
-
readonly: true,
|
|
87
|
-
async execute(args, ctx) {
|
|
88
|
-
args = assertToolArgs(ledgerQueryTool, args)
|
|
89
|
-
return JSON.stringify(ledgerQuery({ cwd: args.cwd ?? cwdOf(ctx), status: args.status, kind: args.kind, board: args.board }), null, 2)
|
|
90
|
-
},
|
|
91
|
-
}
|
|
75
|
+
/** action 枚举(全量 / 只读两变体;顺序 = 文档与按钮序)。 */
|
|
76
|
+
const FULL_ACTIONS = ["add", "update", "close", "query", "count"]
|
|
77
|
+
const READ_ACTIONS = ["query", "count"]
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
description: DESC("ledger_count"), // #15 外置:文本单点 = tool-docs/ledger_count.md
|
|
96
|
-
parameters: {
|
|
97
|
-
type: "object",
|
|
98
|
-
properties: { cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" } },
|
|
99
|
-
additionalProperties: false,
|
|
100
|
-
},
|
|
101
|
-
readonly: true,
|
|
102
|
-
async execute(args, ctx) {
|
|
103
|
-
args = assertToolArgs(ledgerCountTool, args)
|
|
104
|
-
return JSON.stringify({ count: ledgerCount({ cwd: args.cwd ?? cwdOf(ctx) }) })
|
|
105
|
-
},
|
|
106
|
-
}
|
|
79
|
+
/** cwd 参数描述(五前身声明同文——文本单点)。 */
|
|
80
|
+
const CWD_DESC = "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根"
|
|
107
81
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
parameters: {
|
|
82
|
+
/** 各 action 声明条目(= 前身旧工具 `parameters` 逐字;伪工具对象守卫 + 弃用壳共用单源)。 */
|
|
83
|
+
const ACTION_SPECS = {
|
|
84
|
+
add: {
|
|
112
85
|
type: "object",
|
|
113
86
|
required: ["kind", "title"],
|
|
114
87
|
properties: {
|
|
115
|
-
cwd: { type: "string", description:
|
|
88
|
+
cwd: { type: "string", description: CWD_DESC },
|
|
116
89
|
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
|
|
117
90
|
title: { type: "string", description: "条目标题" },
|
|
118
91
|
board: { type: "string", description: "归属板块(需求档文档名,可空)" },
|
|
@@ -123,23 +96,11 @@ export const ledgerAddTool = {
|
|
|
123
96
|
},
|
|
124
97
|
additionalProperties: false,
|
|
125
98
|
},
|
|
126
|
-
|
|
127
|
-
async execute(args, ctx) {
|
|
128
|
-
args = assertToolArgs(ledgerAddTool, args)
|
|
129
|
-
const { cwd, ...row } = args
|
|
130
|
-
const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
|
|
131
|
-
return JSON.stringify({ id, status: "待讨论" })
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const ledgerUpdateTool = {
|
|
136
|
-
name: "ledger_update",
|
|
137
|
-
description: DESC("ledger_update"), // #15 外置:文本单点 = tool-docs/ledger_update.md
|
|
138
|
-
parameters: {
|
|
99
|
+
update: {
|
|
139
100
|
type: "object",
|
|
140
101
|
required: ["id"],
|
|
141
102
|
properties: {
|
|
142
|
-
cwd: { type: "string", description:
|
|
103
|
+
cwd: { type: "string", description: CWD_DESC },
|
|
143
104
|
id: { type: "number", description: "条目 id" },
|
|
144
105
|
status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "目标状态(六态之一,缺省 = 不变)" },
|
|
145
106
|
title: { type: "string", description: "标题(缺省 = 不变)" },
|
|
@@ -152,37 +113,139 @@ export const ledgerUpdateTool = {
|
|
|
152
113
|
},
|
|
153
114
|
additionalProperties: false,
|
|
154
115
|
},
|
|
155
|
-
|
|
156
|
-
async execute(args, ctx) {
|
|
157
|
-
args = assertToolArgs(ledgerUpdateTool, args)
|
|
158
|
-
const { cwd, id, ...patch } = args
|
|
159
|
-
let executorSessionId
|
|
160
|
-
if (patch.executor == null) {
|
|
161
|
-
// 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1);
|
|
162
|
-
// 工具层供值,核心函数保持参数注入纯函数
|
|
163
|
-
const { getSessionId } = await import("./session-slots.mjs")
|
|
164
|
-
executorSessionId = getSessionId()
|
|
165
|
-
}
|
|
166
|
-
return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export const ledgerCloseTool = {
|
|
171
|
-
name: "ledger_close",
|
|
172
|
-
description: DESC("ledger_close"), // #15 外置:文本单点 = tool-docs/ledger_close.md
|
|
173
|
-
parameters: {
|
|
116
|
+
close: {
|
|
174
117
|
type: "object",
|
|
175
118
|
required: ["id", "status"],
|
|
176
119
|
properties: {
|
|
177
|
-
cwd: { type: "string", description:
|
|
120
|
+
cwd: { type: "string", description: CWD_DESC },
|
|
178
121
|
id: { type: "number", description: "条目 id" },
|
|
179
122
|
status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 追认核销 / 撤回)" },
|
|
180
123
|
},
|
|
181
124
|
additionalProperties: false,
|
|
182
125
|
},
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
126
|
+
query: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {
|
|
129
|
+
cwd: { type: "string", description: CWD_DESC },
|
|
130
|
+
status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
|
|
131
|
+
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
|
|
132
|
+
board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
|
|
133
|
+
},
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
},
|
|
136
|
+
count: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: { cwd: { type: "string", description: CWD_DESC } },
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 并集 schema(§11.2——`action` 枚举 + 五 action 参数并集;per-action 必填由运行时守卫裁)。 */
|
|
144
|
+
const UNION_PARAMETERS = {
|
|
145
|
+
type: "object",
|
|
146
|
+
required: ["action"],
|
|
147
|
+
properties: {
|
|
148
|
+
action: { type: "string", enum: FULL_ACTIONS, description: "" }, // 变体构造时填描述
|
|
149
|
+
cwd: { type: "string", description: CWD_DESC },
|
|
150
|
+
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别(add 必填 / query 过滤;缺省 = 不过滤)" },
|
|
151
|
+
title: { type: "string", description: "条目标题(add 必填;update 可改——空串 / 全空白 ⇒ 拒)" },
|
|
152
|
+
board: { type: "string", description: "归属板块(需求档文档名;add 可空 / update 缺省 = 不变 / query 过滤)" },
|
|
153
|
+
req_doc: { type: "string", description: "需求档指针(add 可空 / update 缺省 = 不变)" },
|
|
154
|
+
task_book: { type: "string", description: "任务书指针(add 可空 / update 缺省 = 不变;在途 / 待核销必填——咬合 CHECK)" },
|
|
155
|
+
evidence: { type: "string", description: "最小证据行(add 可空 / update 缺省 = 不变;追认核销须非空)" },
|
|
156
|
+
trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(add / update 可空)" },
|
|
157
|
+
id: { type: "number", description: "条目 id(update / close 必填)" },
|
|
158
|
+
status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "状态(close 必填 ∈ {已核销, 已废弃};update 迁移 / query 过滤——六态之一)" },
|
|
159
|
+
executor: { type: "string", description: "执行者 sessionId(update 可选——接手改写归属;缺省 = 按状态迁移语义)" },
|
|
187
160
|
},
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 变体构造(同名单对象、schema 窄化、两形永不同装配——先例 = `subagent` 工具 depth 面变体):
|
|
165
|
+
* 全量(depth-0 五 action;`writeFace:true` —— `readonly:false` + 动作级分类钩子,写面保持侧效门,
|
|
166
|
+
* query / count 归只读类 = 旧读二 `readonly:true` 同行为:planMode 放行 / 免审批)∥ 只读(depth>0
|
|
167
|
+
* 二 action;`readonly:true`,写三 schema 级不可达)。形态显式声明(不按 actions 长度推导——增量
|
|
168
|
+
* 变体不改错标文案)。 */
|
|
169
|
+
function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
|
|
170
|
+
const entryTool = { name: "ledger", parameters: { type: "object", required: ["action"], properties: { action: { type: "string", enum: actions } } } }
|
|
171
|
+
return {
|
|
172
|
+
name: "ledger",
|
|
173
|
+
description: DESC("ledger"), // 文本单点 = tool-docs/ledger.md(五档要点合一)
|
|
174
|
+
parameters: {
|
|
175
|
+
...UNION_PARAMETERS,
|
|
176
|
+
properties: {
|
|
177
|
+
...UNION_PARAMETERS.properties,
|
|
178
|
+
action: {
|
|
179
|
+
...UNION_PARAMETERS.properties.action,
|
|
180
|
+
enum: actions,
|
|
181
|
+
description: writeFace
|
|
182
|
+
? "操作(五值):add 新增 / update 更新 / close 收口 / query 查询 / count 计数——写三仅主 agent 装配面"
|
|
183
|
+
: "操作(只读二值):query 查询 / count 计数",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
readonly,
|
|
188
|
+
isReadonlyAction(args) { return READ_ACTIONS.includes(args?.action) },
|
|
189
|
+
async execute(args, ctx) {
|
|
190
|
+
// ① 入参归一(判序①——P1 同式;缺省 / null ⇒ {})
|
|
191
|
+
if (args == null) args = {}
|
|
192
|
+
else if (typeof args !== "object" || Array.isArray(args)) throw new Error(`ledger:参数须为对象(收到 ${argPreview(args)})`)
|
|
193
|
+
// ② 入口级 action 判(P3 缺失 / P5 非法——消费 action;取值域 = 本装配面枚举)
|
|
194
|
+
const { action, ...rest0 } = args
|
|
195
|
+
assertToolArgs(entryTool, { action })
|
|
196
|
+
// ③ 余键喂该 action 伪工具对象(声明源 = 前身工具 parameters 逐字——`action` 键不入 P2 域)
|
|
197
|
+
const rest = assertToolArgs({ name: `ledger(${action})`, parameters: ACTION_SPECS[action] }, rest0)
|
|
198
|
+
// ④ 路由既有核函数(包装细节携自原工具 execute 逐字保留:cwd 缺省 / JSON 缩进 / getSessionId 动态 import)
|
|
199
|
+
switch (action) {
|
|
200
|
+
case "add": {
|
|
201
|
+
const { cwd, ...row } = rest
|
|
202
|
+
const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
|
|
203
|
+
return JSON.stringify({ id, status: "待讨论" })
|
|
204
|
+
}
|
|
205
|
+
case "update": {
|
|
206
|
+
const { cwd, id, ...patch } = rest
|
|
207
|
+
let executorSessionId
|
|
208
|
+
if (patch.executor == null) {
|
|
209
|
+
// 显式 `null` ≡ 略去(§3.1 `null` 口径 · #474——与缺省同分支);动态 import——零新静态边(K-LX1)
|
|
210
|
+
const { getSessionId } = await import("./session-slots.mjs")
|
|
211
|
+
executorSessionId = getSessionId()
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
|
|
214
|
+
}
|
|
215
|
+
case "close":
|
|
216
|
+
return JSON.stringify(ledgerClose({ cwd: rest.cwd ?? cwdOf(ctx), id: rest.id, status: rest.status }))
|
|
217
|
+
case "query":
|
|
218
|
+
return JSON.stringify(ledgerQuery({ cwd: rest.cwd ?? cwdOf(ctx), status: rest.status, kind: rest.kind, board: rest.board }), null, 2)
|
|
219
|
+
case "count":
|
|
220
|
+
return JSON.stringify({ count: ledgerCount({ cwd: rest.cwd ?? cwdOf(ctx) }) })
|
|
221
|
+
default:
|
|
222
|
+
// 不可达(② 枚举守门已先拒)——fail-loud 兜底(防未来变体增量 drift)
|
|
223
|
+
throw new Error(`ledger:action 非法:${argPreview(action)}(取值 ∈ ${enumText(actions)})`)
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
}
|
|
188
227
|
}
|
|
228
|
+
|
|
229
|
+
/** 统一入口两变体(同名 `ledger`——depth-0 装配全量 ∥ depth>0 各角色段装配只读)。 */
|
|
230
|
+
export const ledgerTool = makeLedgerTool(FULL_ACTIONS, { writeFace: true })
|
|
231
|
+
export const ledgerReadTool = makeLedgerTool(READ_ACTIONS, { readonly: true })
|
|
232
|
+
|
|
233
|
+
// ── 旧五名弃用壳(设计档 §11.4——code 面保留、不入任何装配面) ────────────────
|
|
234
|
+
|
|
235
|
+
/** 弃用文案(含旧名 + 统一入口 + 对应 action 指引;execute 抛它 ⇒ 零库动作)。
|
|
236
|
+
* description 同文静态化——不经 `DESC`(旧五档退场后 DESC 加载必炸;壳面规避)。 */
|
|
237
|
+
const deprecatedLine = (old, action) => `${old} 已退役(本名不再装配)——请改用统一入口 ledger:action="${action}"(参数面与原工具相同)。`
|
|
238
|
+
|
|
239
|
+
const deprecatedShell = (old, action, readonly) => ({
|
|
240
|
+
name: old,
|
|
241
|
+
description: deprecatedLine(old, action),
|
|
242
|
+
parameters: ACTION_SPECS[action],
|
|
243
|
+
readonly,
|
|
244
|
+
async execute() { throw new Error(deprecatedLine(old, action)) },
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
export const ledgerQueryTool = deprecatedShell("ledger_query", "query", true)
|
|
248
|
+
export const ledgerCountTool = deprecatedShell("ledger_count", "count", true)
|
|
249
|
+
export const ledgerAddTool = deprecatedShell("ledger_add", "add", false)
|
|
250
|
+
export const ledgerUpdateTool = deprecatedShell("ledger_update", "update", false)
|
|
251
|
+
export const ledgerCloseTool = deprecatedShell("ledger_close", "close", false)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-variant-notice.mjs — 变体键库首跑检测提示(F-LX3 · 设计档 `docs/core/design/LEDGER.md` §12 ·
|
|
3
|
+
* 批 `docs/batches/2026-10-05-ledger-variant-db-notice.md` · 台账 #935)。
|
|
4
|
+
*
|
|
5
|
+
* 升级到含键归一的版本后,存量变体键库(旧产物写入的盘符翻转拼写键——`legacyKeyVariants`)不再无声搁浅:
|
|
6
|
+
* CLI 主入口启动时对**当前项目根**检测,命中 ⇒ 单行 i18n 提示(`thincoder ledger audit` 查看 ∥
|
|
7
|
+
* `thincoder ledger migrate` 收正);**只提示,零自动动作**(不迁移 / 不删档 / 不建库 / 零写)。
|
|
8
|
+
* 判据 = 变体键档**文件存在**(KD-VN1——零开库:不触 SQLite / 不建 WAL;坏档 / 空库不漏报)。
|
|
9
|
+
* 每进程至多一次 = 模块级闩(首唤检测 / 后唤零动作——重置缝 `_resetLedgerVariantNoticeForTest`,
|
|
10
|
+
* 先例 = `_resetLedgerDirForTest`)。降级 = 全径 try ⇒ 返回 `null`(零抛零阻塞——启动链不受累)。
|
|
11
|
+
* 本档静态链入 `ledger-migrate.mjs`(node:sqlite)⇒ 消费侧一律动态 import(W8 契约②)。
|
|
12
|
+
* 检测面零新建哈希式:键式直引 `ledgerKey` ∥ 变体枚举直引 `legacyKeyVariants`(单源不二写)。
|
|
13
|
+
*/
|
|
14
|
+
import { statSync } from "node:fs"
|
|
15
|
+
import { join, resolve } from "node:path"
|
|
16
|
+
|
|
17
|
+
import { t } from "./i18n.mjs"
|
|
18
|
+
import { ledgerDirPath, ledgerKey } from "./ledger-db.mjs"
|
|
19
|
+
import { legacyKeyVariants } from "./ledger-migrate.mjs"
|
|
20
|
+
import { resolveProjectRoot } from "./manifest.mjs"
|
|
21
|
+
|
|
22
|
+
/** 默认探针(吞错形——与 `migrate` 源枚举过滤同判据):`statSync(p).isFile()`,任何 stat 失败 ⇒ false。 */
|
|
23
|
+
const defaultExists = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
24
|
+
|
|
25
|
+
/** 一次为限闩(「每进程」= ESM 模块实例的天然粒度——首唤检测 / 后唤零动作)。 */
|
|
26
|
+
let noticeChecked = false
|
|
27
|
+
|
|
28
|
+
/** 测试重置缝(同 `_resetLedgerDirForTest` 先例——闩为进程级事实,直测需复位)。 */
|
|
29
|
+
export function _resetLedgerVariantNoticeForTest() { noticeChecked = false }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 检测当前项目根的变体键库:命中 ⇒ 单行提示文案(i18n 键 `ledger.variantDbNotice`——zh ∥ en 单源),
|
|
33
|
+
* 无 ∥ 已查过 ∥ 任何异常(根解析 / 探针 / 文案解析)⇒ `null`(静默降级——零抛)。
|
|
34
|
+
* 命中判据 = 变体键(`k !== 主键`)中有档在盘(文件存在——零开库);主库在否不影响判定。
|
|
35
|
+
* @param {{cwd?: string, dir?: string, locale?: string, exists?: (p: string) => boolean}} [opts]
|
|
36
|
+
* @returns {string|null} 单行文案 / null
|
|
37
|
+
*/
|
|
38
|
+
export function ledgerVariantNotice({ cwd = process.cwd(), dir = ledgerDirPath(), locale = "en", exists = defaultExists } = {}) {
|
|
39
|
+
if (noticeChecked) return null
|
|
40
|
+
noticeChecked = true
|
|
41
|
+
try {
|
|
42
|
+
const root = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
|
|
43
|
+
const targetKey = ledgerKey(root)
|
|
44
|
+
const hit = legacyKeyVariants(root).filter((k) => k !== targetKey).some((k) => exists(join(dir, `${k}.db`)))
|
|
45
|
+
return hit ? t("ledger.variantDbNotice", {}, locale) : null
|
|
46
|
+
} catch { return null }
|
|
47
|
+
}
|
package/ledger.mjs
CHANGED
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
* 锁死、COUNT 单源计数、事务保证收口原子、归档 = 软删除。行龄源 = SQLite 时间戳(无 git blame)。
|
|
7
7
|
*
|
|
8
8
|
* 面:① 命令族(查询 = 只读全角色 / 写 = 仅主 agent——核函数 `ledger-cmd.mjs` + 工具定义 `ledger-tools.mjs`
|
|
9
|
-
* 两源,接线见
|
|
9
|
+
* 两源,接线见 `agent/family-tools.mjs`——统一入口 `ledger` 两变体,2026-10-05 统一入口批)
|
|
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,48 +185,9 @@ 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"
|
|
217
|
-
export { ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
|
|
191
|
+
export { ledgerTool, ledgerReadTool, ledgerAddTool, ledgerCloseTool, ledgerCountTool, ledgerQueryTool, ledgerUpdateTool } from "./ledger-tools.mjs"
|
|
218
192
|
export { runLedgerAudit, runLedgerMigrate } from "./ledger-migrate.mjs"
|
|
219
193
|
export { resolveExecutorStates, executorTail, _setExecutorProbeTtlForTest } from "./ledger-executors.mjs"
|
package/manifest-schema.mjs
CHANGED
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* `docRoot` 子键**值形态判据**(F7 / KD-M1-6 / KD-M1-8——判据单源):非空字符串(`trim` 后
|
|
15
|
-
* 非空),或非空数组且元素皆非空字符串(**同款口径**:元素 `trim`
|
|
16
|
-
* 空白串 / 空数组 / 数组含非串 / 空串 /
|
|
15
|
+
* 非空),或非空数组且元素皆非空字符串(**同款口径**:元素 `trim` 后非空),或 `null`
|
|
16
|
+
* (**「本面无」合法哨兵**——KD-M1-37);其余(空串 / 空白串 / 空数组 / 数组含非串 / 空串 /
|
|
17
|
+
* 空白串元素 / 非串非数组)为非法。
|
|
17
18
|
* @param {unknown} value docRoot 某子键的值
|
|
18
19
|
* @returns {boolean}
|
|
19
20
|
*/
|
|
20
21
|
export function isValidDocRootValue(value) {
|
|
22
|
+
if (value === null) return true
|
|
21
23
|
if (typeof value === "string") return value.trim() !== ""
|
|
22
24
|
if (!Array.isArray(value) || value.length === 0) return false
|
|
23
25
|
return value.every((p) => typeof p === "string" && p.trim() !== "")
|
|
@@ -80,14 +82,24 @@ function isLineCountsValue(v) {
|
|
|
80
82
|
* 形状校验(模块设计 §2.2)——枚举 / version 数值恒做;**纯函数、零 fs**(原 `{ cwd }`
|
|
81
83
|
* 指针腿已随字段整链裁撤——KD-M1-5 墓志);不落盘。键存在性入 missingKeys(非拒):
|
|
82
84
|
* 缺键 = 便利 fallback(补默认值),不是错(与整档缺失两分——KD-M1-2)。
|
|
85
|
+
* 未知键(#802 / KD-M1-36):顶层 + 四嵌套层产 `unknownKeys`(非拒——ok 定性零变;产线恰 =
|
|
86
|
+
* fillDefaults 丢弃面;嵌套键用点形路径——同 missingKeys 口径)。
|
|
83
87
|
* @param {object} obj 待校验 manifest 对象
|
|
84
|
-
* @returns {{ok:boolean, errors:string[], missingKeys:string[]}}
|
|
88
|
+
* @returns {{ok:boolean, errors:string[], missingKeys:string[], unknownKeys:string[]}}
|
|
85
89
|
*/
|
|
86
90
|
export function validateManifest(obj) {
|
|
87
91
|
const errors = []
|
|
88
92
|
const missingKeys = []
|
|
89
93
|
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
90
|
-
return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys }
|
|
94
|
+
return { ok: false, errors: ["manifest 顶层必须是 JSON 对象"], missingKeys, unknownKeys: [] }
|
|
95
|
+
}
|
|
96
|
+
// #802(KD-M1-36——未知键可见化):顶层 + 四嵌套层逐键对照(非拒——ok 定性零变;嵌套键点形)。
|
|
97
|
+
const unknownKeys = []
|
|
98
|
+
for (const key of Object.keys(obj)) if (!MANIFEST_SCHEMA.keys.includes(key)) unknownKeys.push(key)
|
|
99
|
+
for (const [nested, subkeys] of Object.entries(MANIFEST_SCHEMA.nestedKeys)) {
|
|
100
|
+
const value = obj[nested]
|
|
101
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue
|
|
102
|
+
for (const sub of Object.keys(value)) if (!subkeys.includes(sub)) unknownKeys.push(`${nested}.${sub}`)
|
|
91
103
|
}
|
|
92
104
|
for (const key of MANIFEST_SCHEMA.keys) {
|
|
93
105
|
if (!(key in obj)) missingKeys.push(key)
|
|
@@ -100,9 +112,9 @@ export function validateManifest(obj) {
|
|
|
100
112
|
}
|
|
101
113
|
for (const sub of subkeys) {
|
|
102
114
|
if (!(sub in value)) { missingKeys.push(`${nested}.${sub}`); continue } // 子键路径(AC-3 / T3b)
|
|
103
|
-
// docRoot 子键值形态(F7 / KD-M1-7):串 |
|
|
115
|
+
// docRoot 子键值形态(F7 / KD-M1-7 / KD-M1-37):串 | 非空串数组 | null(本面无);非法 → 拒(不静默跳过)。
|
|
104
116
|
if (nested === "docRoot" && !isValidDocRootValue(value[sub])) {
|
|
105
|
-
errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}
|
|
117
|
+
errors.push(`docRoot.${sub} 值形态非法:${JSON.stringify(value[sub])}(须为非空字符串 / 非空字符串数组 / null(本面无)——KD-M1-6 / KD-M1-7 / KD-M1-37)`)
|
|
106
118
|
}
|
|
107
119
|
// checkConfig.lineCounts 元素层形态(#546):数组,元素 = { doc, section } 非空串;非法 → 拒(不静默跳过)。
|
|
108
120
|
if (nested === "checkConfig" && sub === "lineCounts" && !isLineCountsValue(value[sub])) {
|
|
@@ -138,7 +150,7 @@ export function validateManifest(obj) {
|
|
|
138
150
|
if (obj.version !== undefined && (typeof obj.version !== "number" || !Number.isFinite(obj.version))) {
|
|
139
151
|
errors.push(`version 非法:${JSON.stringify(obj.version)}(须为数值)`)
|
|
140
152
|
}
|
|
141
|
-
return { ok: errors.length === 0, errors, missingKeys }
|
|
153
|
+
return { ok: errors.length === 0, errors, missingKeys, unknownKeys }
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
/** 缺键补默认值(module 设计 §2.2 管线):顶层缺键补默认、嵌套键(docRoot/checkConfig/index/advisor)
|