@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/advisor/citations.mjs +40 -4
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/convergence.mjs +1 -1
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +5 -4
  8. package/advisor/run.mjs +5 -1
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/completion.mjs +3 -1
  12. package/agent/digest-account.mjs +188 -0
  13. package/agent/dispatch-run.mjs +29 -12
  14. package/agent/family-tools.mjs +11 -11
  15. package/agent/helpers.mjs +8 -0
  16. package/agent/record-results.mjs +10 -1
  17. package/agent/run-stages.mjs +27 -4
  18. package/agent/run-start.mjs +49 -3
  19. package/agent/spawn-child.mjs +2 -1
  20. package/agent/subagent-event.mjs +71 -0
  21. package/agent/suspension.mjs +60 -12
  22. package/agent/turn-loop.mjs +31 -2
  23. package/agent/write-gate.mjs +13 -25
  24. package/agent-tools/advisor-async.mjs +25 -7
  25. package/agent-tools/advisor-settle.mjs +7 -9
  26. package/agent-tools/advisor.mjs +34 -33
  27. package/agent-tools/async-settle.mjs +29 -5
  28. package/agent-tools/batch-lifecycle.mjs +29 -7
  29. package/agent-tools/batch-paths.mjs +22 -2
  30. package/agent-tools/batch-read.mjs +36 -0
  31. package/agent-tools/batch-skeleton.mjs +59 -33
  32. package/agent-tools/batch.mjs +16 -6
  33. package/agent-tools/design-token.mjs +76 -16
  34. package/agent-tools/escalate-async.mjs +4 -2
  35. package/agent-tools/review-facts.mjs +143 -16
  36. package/agent-tools/subagent-actions-query.mjs +13 -6
  37. package/agent-tools/subagent-async.mjs +16 -4
  38. package/agent-tools/subagent-panel.mjs +121 -37
  39. package/agent-tools/subagent-spawn.mjs +1 -1
  40. package/agent-tools/subagent.mjs +5 -3
  41. package/agent-tools/verify.mjs +5 -0
  42. package/agent-tools/zero-write-watch.mjs +77 -0
  43. package/agent.mjs +1 -1
  44. package/auto-think.mjs +26 -2
  45. package/config-presets.mjs +8 -0
  46. package/config.mjs +30 -0
  47. package/conventions.mjs +26 -6
  48. package/declaration.mjs +26 -2
  49. package/embedding.mjs +35 -3
  50. package/escape.mjs +1 -1
  51. package/i18n.mjs +5 -0
  52. package/ledger-cmd.mjs +13 -21
  53. package/ledger-db.mjs +34 -1
  54. package/ledger-migrate.mjs +4 -3
  55. package/ledger-read.mjs +94 -0
  56. package/ledger-surface.mjs +15 -46
  57. package/ledger-tools.mjs +148 -85
  58. package/ledger-variant-notice.mjs +47 -0
  59. package/ledger.mjs +38 -64
  60. package/manifest-schema.mjs +19 -7
  61. package/manifest.mjs +32 -11
  62. package/mcp/transport-http.mjs +52 -4
  63. package/mcp/transport-stdio.mjs +33 -8
  64. package/mcp.mjs +13 -5
  65. package/memory/code-search.mjs +5 -3
  66. package/memory/core.mjs +6 -4
  67. package/memory/docs.mjs +5 -3
  68. package/memory/schema.mjs +2 -0
  69. package/package.json +1 -1
  70. package/prompts/advisor-design.md +3 -2
  71. package/prompts/advisor-round1.md +1 -0
  72. package/prompts/advisor-round2.md +8 -3
  73. package/prompts/advisor-round3.md +7 -2
  74. package/prompts/common.md +2 -2
  75. package/prompts/discipline-normal.md +2 -1
  76. package/prompts/persona-engineering.md +7 -4
  77. package/provider/anthropic.mjs +14 -2
  78. package/provider/core.mjs +10 -4
  79. package/provider/errors.mjs +23 -0
  80. package/provider/google.mjs +31 -4
  81. package/provider/normalize.mjs +57 -0
  82. package/provider/responses-request.mjs +3 -1
  83. package/provider/responses.mjs +38 -4
  84. package/provider/sse.mjs +45 -13
  85. package/proxy.mjs +13 -3
  86. package/session.mjs +13 -2
  87. package/shell-identity.mjs +107 -0
  88. package/stream-destroy.mjs +62 -0
  89. package/tool-docs/advisor.md +1 -1
  90. package/tool-docs/bash.md +1 -1
  91. package/tool-docs/batch.md +1 -1
  92. package/tool-docs/delete.md +7 -5
  93. package/tool-docs/ledger.md +6 -0
  94. package/tool-docs/subagent.md +10 -0
  95. package/tool-docs/verify.md +1 -1
  96. package/tools/bash.mjs +9 -4
  97. package/tools/edit-batch.mjs +7 -2
  98. package/tools/index.mjs +1 -4
  99. package/tools/patch.mjs +8 -2
  100. package/tools/write-path.mjs +6 -4
  101. package/undo-stack.mjs +23 -9
  102. package/tool-docs/ledger_add.md +0 -1
  103. package/tool-docs/ledger_close.md +0 -1
  104. package/tool-docs/ledger_count.md +0 -1
  105. package/tool-docs/ledger_query.md +0 -1
  106. package/tool-docs/ledger_update.md +0 -1
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/escape.mjs CHANGED
@@ -52,7 +52,7 @@ export function escapeLiteralEscapes(text) {
52
52
  return out
53
53
  }
54
54
 
55
- /** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 替换为 。 */
55
+ /** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 剥除(出串零该码元——码 :66/:68 实读为准)。 */
56
56
  export function sanitizeLoneSurrogates(text) {
57
57
  text = String(text ?? "")
58
58
  let out = ""
package/i18n.mjs CHANGED
@@ -41,6 +41,9 @@ export const CORE_MESSAGES = Object.freeze({
41
41
  "digest.turnLabelAsk": { en: "[auto-turn: answering ${from}: ${msg}]", zh: "自动回合:答复 ${from}:${msg}" },
42
42
  "digest.capAuto": { en: "[auto-turn: continuing past turn cap…]", zh: "自动回合:越过轮次上限,继续推进…" },
43
43
  "digest.capStop": { en: "[auto-turn stopped at ${turns} turns — partial digest; finished reports stay in history]", zh: "自动回合在 ${turns} 轮处停止——部分消化;已完成的报告保留在历史中" },
44
+ // 消化账务批 · 2026-10-05 · 台账 #930(§6.31.6 端面残余行——判据 = `unsettledCount(carrier)`;
45
+ // 消费面 = CLI 痕行 ∥ 桌面 / VSC 行元素;三端共读本键)
46
+ "digest.residue": { en: "${n} background report(s) not accounted — they will be re-delivered", zh: "有 ${n} 份后台报告未销账——将自动重投" },
44
47
  // ── 限流 / 供应商状态行 ──
45
48
  "status.rateWait": { en: "TPM throttle wait ~${s}s", zh: "TPM 限流等待 ~${s}s" },
46
49
  "status.rateLimited": { en: "Rate-limited 429, retry in ${s}s", zh: "限流 429,${s}s 后重试" },
@@ -69,6 +72,8 @@ export const CORE_MESSAGES = Object.freeze({
69
72
  "status.turn": { en: "turn ${n}/${m}", zh: "轮次 ${n}/${m}" },
70
73
  "tool.running": { en: "running…", zh: "执行中…" },
71
74
  "tool.done": { en: "done", zh: "完成" },
75
+ // ── 台账(ledger)· F-LX3 变体键库首跑提示(批 2026-10-05-ledger-variant-db-notice · 台账 #935 · LEDGER.md §12.2 逐字;消费面 = CLI TUI 启动行)
76
+ "ledger.variantDbNotice": { en: "Legacy variant-key ledger DB found for this project (pre-upgrade; rows may be outside the current ledger): thincoder ledger audit to inspect; thincoder ledger migrate to reconcile", zh: "检测到本项目的存量变体键台账库(升级遗留,数据可能未并入当前库):thincoder ledger audit 查看;thincoder ledger migrate 收正" },
72
77
  })
73
78
  // 深度冻结(机器消费面冻结——嵌套 { en, zh } 也冻结;浅冻结下值可被静默改写)。
74
79
  for (const entry of Object.values(CORE_MESSAGES)) Object.freeze(entry)
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 {