@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/advisor/citations.mjs +13 -3
  3. package/advisor/convergence.mjs +1 -1
  4. package/advisor/messages.mjs +3 -3
  5. package/agent/dispatch-run.mjs +29 -12
  6. package/agent/record-results.mjs +6 -1
  7. package/agent/spawn-child.mjs +2 -1
  8. package/agent/subagent-event.mjs +71 -0
  9. package/agent/suspension.mjs +35 -9
  10. package/agent/turn-loop.mjs +6 -0
  11. package/agent-tools/advisor-async.mjs +11 -1
  12. package/agent-tools/advisor-settle.mjs +7 -9
  13. package/agent-tools/batch-lifecycle.mjs +3 -2
  14. package/agent-tools/batch-read.mjs +36 -0
  15. package/agent-tools/batch-skeleton.mjs +28 -0
  16. package/agent-tools/batch.mjs +5 -3
  17. package/agent-tools/design-token.mjs +56 -16
  18. package/agent-tools/subagent-async.mjs +6 -1
  19. package/agent-tools/subagent-panel.mjs +119 -36
  20. package/agent-tools/subagent.mjs +4 -2
  21. package/agent-tools/verify.mjs +5 -0
  22. package/auto-think.mjs +26 -2
  23. package/config-presets.mjs +8 -0
  24. package/config.mjs +42 -8
  25. package/declaration.mjs +26 -2
  26. package/embedding.mjs +35 -3
  27. package/ledger-cmd.mjs +13 -21
  28. package/ledger-db.mjs +34 -1
  29. package/ledger-migrate.mjs +4 -3
  30. package/ledger-read.mjs +94 -0
  31. package/ledger-surface.mjs +15 -46
  32. package/ledger.mjs +36 -62
  33. package/manifest-schema.mjs +13 -3
  34. package/manifest.mjs +27 -8
  35. package/mcp/transport-http.mjs +52 -4
  36. package/mcp/transport-stdio.mjs +33 -8
  37. package/mcp.mjs +13 -5
  38. package/memory/code-search.mjs +5 -3
  39. package/memory/core.mjs +6 -4
  40. package/memory/docs.mjs +5 -3
  41. package/memory/schema.mjs +2 -0
  42. package/model-ref.mjs +96 -3
  43. package/package.json +1 -1
  44. package/prompts/advisor-design.md +3 -2
  45. package/prompts/advisor-round1.md +1 -0
  46. package/prompts/advisor-round2.md +8 -3
  47. package/prompts/advisor-round3.md +7 -2
  48. package/prompts/discipline-normal.md +2 -1
  49. package/prompts/persona-engineering.md +3 -1
  50. package/provider/core.mjs +10 -4
  51. package/provider/google.mjs +18 -2
  52. package/provider/normalize.mjs +57 -0
  53. package/provider/sse.mjs +45 -13
  54. package/proxy.mjs +13 -3
  55. package/session-lifecycle.mjs +21 -12
  56. package/session.mjs +13 -2
  57. package/stream-destroy.mjs +62 -0
  58. package/tool-docs/verify.md +1 -1
  59. package/tools/edit-batch.mjs +7 -2
  60. 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
- body: JSON.stringify({ model: embedder.model, input }),
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 = new Error(message)
136
+ lastError = err
105
137
  continue
106
138
  }
107
- throw new Error(message)
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 { statSync } from "node:fs"
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 { declaredPublicRoots } from "./declaration.mjs"
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:声明源前缀形同判(§6.15——首段 = 声明公共仓目录名 ⇒ 于该声明根解析;可核 ⇒ 通过;缺位拒)。 */
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 = resolve(base, part)
61
- // §6.15 声明源候选(#832 · LEDGER.md §6.1-4):首段 = 声明公共仓目录名 ⇒ 该声明根解析余段(同名多仓 ⇒ 声明序首者);未声明 ∥ 缺位 ⇒ 照旧拒。
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})`)
@@ -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 = resolve(projectRoot, part)
81
- return isFile(abs) ? null : `task_book 指向的档不存在:${tb}(解析 = ${abs})`
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) {
@@ -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
+ }
@@ -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
- * 来源 = `thincoder-cli/src/tui/ledger-surface.mjs`(CLI 住 `tui/`、VSC 住 `extension/`;
6
- * VSC 头注自述同构)——逐字随迁,**渲染面收核为注入**:原档 `import { C } from "./ansi.mjs"`
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, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, REFRESH_MS, resolveExecutorStates, saveNotifyState } from "./ledger.mjs"
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
- * `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。
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
- const notify = loadNotifyState(notifyFile)
31
- const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
32
- const changeLines = plans.flatMap(({ plan }) => plan.lines)
33
- const showStartup = startup && scans.some((s) => s.actionable)
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 (startup) => {
33
+ const tick = async () => {
65
34
  if (disposed || inflight) return
66
- if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
35
+ if (ctx.state?.processing) return // 扫描不打断回合帧
67
36
  inflight = true
68
- try { await runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
37
+ try { await runLedgerScan(ctx) } catch { /* 可见面尽力——不崩(N1) */ } finally { inflight = false }
69
38
  }
70
39
  setImmediate(() => {
71
40
  if (disposed) return
72
- tick(true)
73
- timer = setInterval(() => tick(false), REFRESH_MS)
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
- * ④ 通知去重(送达门 + 一次性去重——去重档跨会话、跨端共享)⑤ 格式 helper(行文本逐字契约)。
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 { mkdirSync, opendirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"
18
+ import { opendirSync, statSync } from "node:fs"
19
19
  import { basename, dirname, join, resolve } from "node:path"
20
- import { configDir } from "./config.mjs"
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
- /** 变化行去重档(跨会话、跨端共享——需求档 §1.18 F5;configDir 先例 = crash-reports 同区)。 */
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
- * 位置无关(位移 / 他条编辑稳定);自身文本变更 = 键变(最坏一次重报)。§2.30.3.2。 */
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 字(超出加 `…`)。§2.30.3.3 L3。 */
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 current = findProject(anchor)
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 状态标记(逐字——§2.30.3.3)。 */
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"
@@ -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)