@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/ledger-cmd.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-cmd.mjs — 台账命令族(查询 = 只读全角色 / 写 = 仅主 agent 装配)+ 工具定义
|
|
3
|
+
* (M2 模块拆分件——设计档 docs/core/design/modules/ENGINEERING-MODE-V2-MODULE-LEDGER.md §2.3 拆分计划)。
|
|
4
|
+
*
|
|
5
|
+
* 接线(设计档 §2.2 命令面接线):查询命令 ← tools/index.mjs(assembleBuiltinTools 全角色面);
|
|
6
|
+
* 写命令 ← agent/family-tools.mjs(assembleFamilyTools 的 depthOnly 分支)——一律**动态 import** 本模块链
|
|
7
|
+
* (ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧静态 import 会破 W8 契约②)。
|
|
8
|
+
*/
|
|
9
|
+
import { statSync } from "node:fs"
|
|
10
|
+
import { resolve } from "node:path"
|
|
11
|
+
import { ALLOWED_MIGRATIONS, nowIso, openLedger, PENDING_STATUSES } from "./ledger-db.mjs"
|
|
12
|
+
import { resolveProjectRoot } from "./manifest.mjs"
|
|
13
|
+
|
|
14
|
+
/** 查询(只读,全角色):SELECT 行集(status/kind/board 过滤,缺省 = 全部行;id 升序)。库不在 = 空账。 */
|
|
15
|
+
export function ledgerQuery({ cwd, status = null, kind = null, board = null } = {}) {
|
|
16
|
+
const db = openLedger(cwd)
|
|
17
|
+
if (!db) return []
|
|
18
|
+
try {
|
|
19
|
+
const where = [], args = []
|
|
20
|
+
if (status) { where.push("status = ?"); args.push(status) }
|
|
21
|
+
if (kind) { where.push("kind = ?"); args.push(kind) }
|
|
22
|
+
if (board) { where.push("board = ?"); args.push(board) }
|
|
23
|
+
return db.prepare(`SELECT * FROM items${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`).all(...args)
|
|
24
|
+
} finally { db.close() }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 计数单源(只读,全角色):COUNT(*) WHERE 未决四态——替代 v1 md 计数面(AC-M2-3)。库不在 = 0。 */
|
|
28
|
+
export function ledgerCount({ cwd } = {}) {
|
|
29
|
+
const db = openLedger(cwd)
|
|
30
|
+
if (!db) return 0
|
|
31
|
+
try {
|
|
32
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM items WHERE status IN (${PENDING_STATUSES.map(() => "?").join(",")})`).get(...PENDING_STATUSES)
|
|
33
|
+
return Number(row.n)
|
|
34
|
+
} finally { db.close() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 路径 = 存在的**档**?(目录 / 缺失 → false——写门存在性判据用)。 */
|
|
38
|
+
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
39
|
+
|
|
40
|
+
/** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-8):写命令落盘前判**结果行**——`status ∈
|
|
41
|
+
* {在途, 待核销}` 且 `task_book` 非空 ⇒ 文件部分(首个 `§` 前子串,trim)须经基准 `resolve(base, …)`
|
|
42
|
+
* 指向存在的档;缺文件部分(`§2` / 全空白)⇒ 拒;不在册 / 非文件 ⇒ 拒(throw,行不变)。
|
|
43
|
+
* 判位与迁移表判同层(落盘前);文案前缀 = 调用函数名(与同函数既有两条文案同款)。
|
|
44
|
+
* 基准 `base` = **与台账库关联键同源**(2026-09-18 fix 轮 · O1 收正):`resolveProjectRoot(cwd) ??
|
|
45
|
+
* resolve(cwd ?? ".")`——同表达式见 `ledger-db.mjs` `ledgerDbPath`;容器根会话(锚 cwd 下唯一注册
|
|
46
|
+
* 子仓 P)与子仓会话 ⇒ 库键与指针基准同取 P(同库同基准;原实现 `resolve(cwd, …)` = 原始 cwd 会误拒)。 */
|
|
47
|
+
function assertTaskBookGate(cwd, fnName, status, taskBook) {
|
|
48
|
+
if (status !== "在途" && status !== "待核销") return
|
|
49
|
+
if (taskBook == null || taskBook === "") return
|
|
50
|
+
const part = String(taskBook).split("§")[0].trim()
|
|
51
|
+
if (!part) throw new Error(`${fnName}:task_book 不可解析(缺文件部分):${taskBook}`)
|
|
52
|
+
const base = resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
|
|
53
|
+
const abs = resolve(base, part)
|
|
54
|
+
if (!isFile(abs)) throw new Error(`${fnName}:task_book 指向的档不存在:${taskBook}(解析 = ${abs})`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 新增(写命令,仅主 agent):INSERT——入待讨论(六态状态机入口)。 */
|
|
58
|
+
export function ledgerAdd({ cwd, row }) {
|
|
59
|
+
const db = openLedger(cwd, { create: true })
|
|
60
|
+
try {
|
|
61
|
+
// 结果行状态 = INSERT 硬编码「待讨论」⇒ 门当下恒不触发(两入口形态统一——为将来「add 带状态」留位)
|
|
62
|
+
assertTaskBookGate(cwd, "ledgerAdd", "待讨论", row.task_book ?? null)
|
|
63
|
+
const now = nowIso()
|
|
64
|
+
const r = db.prepare(`INSERT INTO items (kind, status, title, board, req_doc, task_book, evidence, trigger, created_at, updated_at) VALUES (?, '待讨论', ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
65
|
+
.run(row.kind, row.title, row.board ?? null, row.req_doc ?? null, row.task_book ?? null, row.evidence ?? null, row.trigger ?? null, now, now)
|
|
66
|
+
return Number(r.lastInsertRowid)
|
|
67
|
+
} finally { db.close() }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 更新(写命令,仅主 agent):UPDATE——状态迁移前判允许迁移表(不在表内 → 拒,行不变)。 */
|
|
71
|
+
export function ledgerUpdate({ cwd, id, patch }) {
|
|
72
|
+
const db = openLedger(cwd, { create: true })
|
|
73
|
+
try {
|
|
74
|
+
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
75
|
+
if (!row) throw new Error(`ledgerUpdate:行 ${id} 不存在`)
|
|
76
|
+
const to = patch.status ?? row.status
|
|
77
|
+
if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
|
|
78
|
+
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
79
|
+
}
|
|
80
|
+
const nextTaskBook = patch.task_book ?? row.task_book
|
|
81
|
+
assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // 判序:迁移表判 → 本门 → UPDATE
|
|
82
|
+
const now = nowIso()
|
|
83
|
+
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, updated_at = ? WHERE id = ?`)
|
|
84
|
+
.run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, patch.evidence ?? row.evidence, patch.trigger ?? row.trigger, now, id)
|
|
85
|
+
return { id }
|
|
86
|
+
} finally { db.close() }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 收口(写命令,仅主 agent):勾销(待核销 → 已核销)/ 撤回(任意态 → 已废弃),事务包裹;归档 = 软删除。 */
|
|
90
|
+
export function ledgerClose({ cwd, id, status }) {
|
|
91
|
+
if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
|
|
92
|
+
const db = openLedger(cwd, { create: true })
|
|
93
|
+
try {
|
|
94
|
+
db.exec("BEGIN")
|
|
95
|
+
try {
|
|
96
|
+
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
97
|
+
if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
|
|
98
|
+
if (status === "已核销" && row.status !== "待核销") throw new Error(`ledgerClose:勾销仅限待核销(现态 ${row.status})`)
|
|
99
|
+
const now = nowIso()
|
|
100
|
+
db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ? WHERE id = ?").run(status, now, now, id)
|
|
101
|
+
db.exec("COMMIT")
|
|
102
|
+
return { id, status }
|
|
103
|
+
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
104
|
+
} finally { db.close() }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── 工具定义(接线:查询 → tools/index.mjs 全角色面;写 → family-tools.mjs depthOnly 分支) ──
|
|
108
|
+
|
|
109
|
+
/** 工具 cwd 供值面(缺省 → 当前项目根)。 */
|
|
110
|
+
const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
|
|
111
|
+
|
|
112
|
+
export const ledgerQueryTool = {
|
|
113
|
+
name: "ledger_query",
|
|
114
|
+
description: "台账查询(需求池 / 技术待办)——SQLite 行集,只读、全角色可用。未决四态 = 待讨论 / 待设计 / 在途 / 待核销;已核销 / 已废弃 = 归档态(软删除,行保留)。过滤参数缺省 = 全部行。",
|
|
115
|
+
parameters: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
|
|
119
|
+
status: { type: "string", enum: ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"], description: "按状态过滤(六态之一,缺省 = 不过滤)" },
|
|
120
|
+
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "按类别过滤" },
|
|
121
|
+
board: { type: "string", description: "按归属板块过滤(需求档文档名)" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
readonly: true,
|
|
125
|
+
async execute(args, ctx) {
|
|
126
|
+
return JSON.stringify(ledgerQuery({ cwd: args.cwd ?? cwdOf(ctx), status: args.status, kind: args.kind, board: args.board }), null, 2)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const ledgerCountTool = {
|
|
131
|
+
name: "ledger_count",
|
|
132
|
+
description: "台账计数单源:未决四态(待讨论 / 待设计 / 在途 / 待核销)COUNT(*),只读、全角色可用。",
|
|
133
|
+
parameters: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: { cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" } },
|
|
136
|
+
},
|
|
137
|
+
readonly: true,
|
|
138
|
+
async execute(args, ctx) {
|
|
139
|
+
return JSON.stringify({ count: ledgerCount({ cwd: args.cwd ?? cwdOf(ctx) }) })
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const ledgerAddTool = {
|
|
144
|
+
name: "ledger_add",
|
|
145
|
+
description: "台账新增条目(写命令,仅主 agent)——入待讨论(六态状态机入口)。kind = requirement(需求池)/ tech_todo(技术待办);trigger = 归批 / 条件 / 认账不排期(可空)。",
|
|
146
|
+
parameters: {
|
|
147
|
+
type: "object",
|
|
148
|
+
required: ["kind", "title"],
|
|
149
|
+
properties: {
|
|
150
|
+
cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
|
|
151
|
+
kind: { type: "string", enum: ["requirement", "tech_todo"], description: "类别" },
|
|
152
|
+
title: { type: "string", description: "条目标题" },
|
|
153
|
+
board: { type: "string", description: "归属板块(需求档文档名,可空)" },
|
|
154
|
+
req_doc: { type: "string", description: "需求档指针(可空)" },
|
|
155
|
+
task_book: { type: "string", description: "任务书指针(可空;在途 / 待核销必填——咬合 CHECK)" },
|
|
156
|
+
evidence: { type: "string", description: "最小证据行(可空)" },
|
|
157
|
+
trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "技术待办触发(可空)" },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
readonly: false,
|
|
161
|
+
async execute(args, ctx) {
|
|
162
|
+
const { cwd, ...row } = args
|
|
163
|
+
const id = ledgerAdd({ cwd: cwd ?? cwdOf(ctx), row })
|
|
164
|
+
return JSON.stringify({ id, status: "待讨论" })
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const ledgerUpdateTool = {
|
|
169
|
+
name: "ledger_update",
|
|
170
|
+
description: "台账更新条目(写命令,仅主 agent)——状态迁移按六态允许迁移表:待讨论→待设计 / 待设计→在途 / 在途→待核销 / 待核销→已核销 / 任意态→已废弃;表外迁移被拒。status 缺省 = 仅更新字段。",
|
|
171
|
+
parameters: {
|
|
172
|
+
type: "object",
|
|
173
|
+
required: ["id"],
|
|
174
|
+
properties: {
|
|
175
|
+
cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
|
|
176
|
+
id: { type: "number", description: "条目 id" },
|
|
177
|
+
status: { type: "string", description: "目标状态(六态之一,缺省 = 不变)" },
|
|
178
|
+
title: { type: "string", description: "标题(缺省 = 不变)" },
|
|
179
|
+
board: { type: "string", description: "归属板块(缺省 = 不变)" },
|
|
180
|
+
req_doc: { type: "string", description: "需求档指针(缺省 = 不变)" },
|
|
181
|
+
task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
|
|
182
|
+
evidence: { type: "string", description: "证据(缺省 = 不变)" },
|
|
183
|
+
trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
readonly: false,
|
|
187
|
+
async execute(args, ctx) {
|
|
188
|
+
const { cwd, id, ...patch } = args
|
|
189
|
+
return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch }))
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const ledgerCloseTool = {
|
|
194
|
+
name: "ledger_close",
|
|
195
|
+
description: "台账收口(写命令,仅主 agent)——勾销:待核销 → 已核销;撤回:任意态 → 已废弃。归档 = 软删除(写 closed_at,行保留)。",
|
|
196
|
+
parameters: {
|
|
197
|
+
type: "object",
|
|
198
|
+
required: ["id", "status"],
|
|
199
|
+
properties: {
|
|
200
|
+
cwd: { type: "string", description: "项目根目录——台账按项目根关联存储于用户数据目录(工作树外、不进 git)。相对路径按当前工作目录解析;查/写别的项目请给绝对路径。缺省 = 当前会话项目根" },
|
|
201
|
+
id: { type: "number", description: "条目 id" },
|
|
202
|
+
status: { type: "string", enum: ["已核销", "已废弃"], description: "目标态(勾销 / 撤回)" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
readonly: false,
|
|
206
|
+
async execute(args, ctx) {
|
|
207
|
+
return JSON.stringify(ledgerClose({ cwd: args.cwd ?? cwdOf(ctx), id: args.id, status: args.status }))
|
|
208
|
+
},
|
|
209
|
+
}
|
package/ledger-db.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-db.mjs — 台账 SQLite 连接 / DDL / 枚举常量(M2 模块拆分件——设计档
|
|
3
|
+
* docs/core/design/modules/ENGINEERING-MODE-V2-MODULE-LEDGER.md §2.3 拆分计划)。
|
|
4
|
+
*
|
|
5
|
+
* items 单表 + 三 CHECK 枚举(kind / status / trigger)+ task_book 咬合 CHECK——枚举机械锁死
|
|
6
|
+
* 靠 DDL(AC-M2-2),不靠应用层 if。归档 = 软删除(状态值,无物理 DELETE——KD-M2-4)。
|
|
7
|
+
* 本档静态 import node:sqlite ⇒ 消费侧一律动态 import 本模块链(W8 契约②——KD-M2-3)。
|
|
8
|
+
*
|
|
9
|
+
* 落点(2026-09-17 用户裁定收正):台账库**不在项目目录**——住用户数据目录
|
|
10
|
+
* `~/.thincoder/ledger/<sha1(项目根绝对路径)[:16]>.db`(工作树外、天然不进 git;
|
|
11
|
+
* 同区先例 = configDir 的 memory.db / tool-results)。项目根仅作**关联键**,不在项目内留任何文件。
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto"
|
|
14
|
+
import { existsSync, mkdirSync } from "node:fs"
|
|
15
|
+
import { join, resolve } from "node:path"
|
|
16
|
+
import { DatabaseSync } from "node:sqlite"
|
|
17
|
+
|
|
18
|
+
import { configDir } from "./config-io.mjs"
|
|
19
|
+
import { resolveProjectRoot } from "./manifest.mjs"
|
|
20
|
+
|
|
21
|
+
/** 台账库目录(默认 = 用户数据目录;测试注入面 `_setLedgerDirForTest` 覆盖)。 */
|
|
22
|
+
let ledgerDir = join(configDir, "ledger")
|
|
23
|
+
|
|
24
|
+
/** 测试注入:覆盖台账库目录(同 `_setConfigPathForTest` 先例——测试不碰真实用户目录)。 */
|
|
25
|
+
export function _setLedgerDirForTest(dir) { ledgerDir = dir }
|
|
26
|
+
export function _resetLedgerDirForTest() { ledgerDir = join(configDir, "ledger") }
|
|
27
|
+
|
|
28
|
+
/** 项目根 → 库路径(稳定键 = sha1(规范化项目根绝对路径) 前 16 位——路径唯一标识,不含分隔符。
|
|
29
|
+
* 项目根与 manifest 同一判据(git 仓根 / 显式声明容器根)——两端会话键一致)。 */
|
|
30
|
+
export function ledgerDbPath(cwd) {
|
|
31
|
+
const key = createHash("sha1").update(resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")).digest("hex").slice(0, 16)
|
|
32
|
+
return join(ledgerDir, `${key}.db`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 未决四态(「活条目」口径 = 计数单源 WHERE 集;已核销 / 已废弃 = 归档态)。 */
|
|
36
|
+
export const PENDING_STATUSES = ["待讨论", "待设计", "在途", "待核销"]
|
|
37
|
+
|
|
38
|
+
/** 六态允许迁移表(ledgerUpdate 迁移前判,不在表内 → 拒——规格 ②.2;设计档 §2.1)。 */
|
|
39
|
+
export const ALLOWED_MIGRATIONS = {
|
|
40
|
+
"待讨论": ["待设计", "已废弃"],
|
|
41
|
+
"待设计": ["在途", "已废弃"],
|
|
42
|
+
"在途": ["待核销", "已废弃"],
|
|
43
|
+
"待核销": ["已核销", "已废弃"],
|
|
44
|
+
"已核销": ["已废弃"],
|
|
45
|
+
"已废弃": [],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 建表 DDL(幂等——设计档 §2.2 逐字;CHECK 字符串用单引号——KD7 实核)。 */
|
|
49
|
+
const DDL = `
|
|
50
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
51
|
+
id INTEGER PRIMARY KEY,
|
|
52
|
+
kind TEXT NOT NULL CHECK(kind IN ('requirement','tech_todo')),
|
|
53
|
+
status TEXT NOT NULL CHECK(status IN ('待讨论','待设计','在途','待核销','已核销','已废弃')),
|
|
54
|
+
title TEXT NOT NULL,
|
|
55
|
+
board TEXT,
|
|
56
|
+
req_doc TEXT,
|
|
57
|
+
task_book TEXT,
|
|
58
|
+
evidence TEXT,
|
|
59
|
+
trigger TEXT CHECK(trigger IN ('归批','条件','认账不排期') OR trigger IS NULL),
|
|
60
|
+
created_at TEXT,
|
|
61
|
+
updated_at TEXT,
|
|
62
|
+
closed_at TEXT,
|
|
63
|
+
CHECK (status NOT IN ('在途','待核销') OR (task_book IS NOT NULL AND task_book <> ''))
|
|
64
|
+
)`
|
|
65
|
+
|
|
66
|
+
/** 开库(cwd = 项目根——仅作关联键)→ DatabaseSync 句柄 + 幂等建表。
|
|
67
|
+
* 读面(create=false):库文件不存在 → 返回 null(空账——读面不建库、无副作用)。
|
|
68
|
+
* 写面(create=true):库文件不存在自动建(台账目录随之创建);项目根目录不存在 → 抛友好错误。 */
|
|
69
|
+
export function openLedger(cwd, { create = false } = {}) {
|
|
70
|
+
const dir = resolve(cwd ?? ".")
|
|
71
|
+
const file = ledgerDbPath(dir)
|
|
72
|
+
if (!existsSync(dir)) {
|
|
73
|
+
throw new Error(`项目目录不存在:${dir}——cwd 请给存在的项目根(相对路径按当前工作目录解析;缺省 = 会话项目根)`)
|
|
74
|
+
}
|
|
75
|
+
if (!existsSync(file) && !create) return null
|
|
76
|
+
try { mkdirSync(ledgerDir, { recursive: true }) } catch { /* 已存在 / 并发建目录竞争——忽略 */ }
|
|
77
|
+
const db = new DatabaseSync(file)
|
|
78
|
+
try { db.exec(DDL) } catch (e) {
|
|
79
|
+
db.close()
|
|
80
|
+
throw new Error(`台账库打开失败:${file}(${e.message})`)
|
|
81
|
+
}
|
|
82
|
+
return db
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 时间戳统一口径(ISO 字符串——行龄源 = 时间戳差值,KD-M2-6)。 */
|
|
86
|
+
export const nowIso = () => new Date().toISOString()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-surface.mjs — 台账可见面(核内结构归位;CORE-UNIFICATION §2.5 #174
|
|
3
|
+
* 「融合:取一侧 + 渲染面按端注入」)。
|
|
4
|
+
*
|
|
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`)。
|
|
14
|
+
*/
|
|
15
|
+
import { buildScan, detailScans, discoverFamily, formatDetailLine, formatMarker, loadNotifyState, NOTIFY_FILE, notifyKey, planChangeLines, REFRESH_MS, saveNotifyState } from "./ledger.mjs"
|
|
16
|
+
|
|
17
|
+
/** 单次扫描(直驱面——timer 包装见 `startLedgerSurface`)。
|
|
18
|
+
* `startup=true` = 会话首扫(补启动行;变化行在前、明细行在后)。
|
|
19
|
+
* `colors` = 端注入的渲染色表(`{ warn, dim }`);未注入 ⇒ `{}`(`undefined` 色值 = 纯文本)。 */
|
|
20
|
+
export function runLedgerScan({ state, agent = null, anchor = null, notifyFile = NOTIFY_FILE, pushLine = () => {}, render = () => {}, startup = false, colors = {} } = {}) {
|
|
21
|
+
const base = anchor ?? agent?.cwd ?? process.cwd()
|
|
22
|
+
const family = discoverFamily(base)
|
|
23
|
+
const scans = []
|
|
24
|
+
for (const p of family.projects) {
|
|
25
|
+
try { scans.push(buildScan({ cwd: p.root })) } catch { /* 台账不可读 → 该项目跳过(余者照常——N1/T110①) */ }
|
|
26
|
+
}
|
|
27
|
+
const current = family.current ? scans.find((s) => s.root === family.current.root) ?? null : null
|
|
28
|
+
const notify = loadNotifyState(notifyFile)
|
|
29
|
+
const plans = scans.map((s) => ({ s, plan: planChangeLines(notify.ledgers[notifyKey(s.ledger)], s) }))
|
|
30
|
+
const changeLines = plans.flatMap(({ plan }) => plan.lines)
|
|
31
|
+
const showStartup = startup && scans.some((s) => s.actionable)
|
|
32
|
+
const rows = showStartup
|
|
33
|
+
? [...changeLines, ...detailScans(scans, current).map((s) => ({ text: formatDetailLine(s), warn: s.actionable }))]
|
|
34
|
+
: changeLines
|
|
35
|
+
let delivered = true
|
|
36
|
+
for (const line of rows) {
|
|
37
|
+
try { pushLine(line.text, line.warn ? colors.warn : colors.dim) } catch { delivered = false; break }
|
|
38
|
+
}
|
|
39
|
+
// 送达门(F5):有事件行 → 仅在送达后记账;无事件行 → 仅状态确有变化时静默同步(跌回 false 即重置)
|
|
40
|
+
const changed = plans.some(({ s, plan }) => {
|
|
41
|
+
const prev = notify.ledgers[notifyKey(s.ledger)]
|
|
42
|
+
return !prev || JSON.stringify(prev.aged ?? []) !== JSON.stringify(plan.next.aged) || Boolean(prev.threshold) !== plan.next.threshold
|
|
43
|
+
})
|
|
44
|
+
if (changeLines.length ? delivered : changed) {
|
|
45
|
+
const ledgers = { ...notify.ledgers }
|
|
46
|
+
for (const { s, plan } of plans) ledgers[notifyKey(s.ledger)] = { ...plan.next, updatedAt: Date.now() }
|
|
47
|
+
saveNotifyState(notifyFile, { version: 1, ledgers })
|
|
48
|
+
}
|
|
49
|
+
state.ledger = { marker: current ? formatMarker(current) : null, warn: Boolean(current && current.aged > 0), scannedAt: Date.now() }
|
|
50
|
+
render()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 挂载:首扫(`setImmediate`——不抢首帧)+ 周期(`REFRESH_MS`;`state.processing` 期间跳过本轮)。
|
|
54
|
+
* 端注入的渲染面(`ctx.colors`)与推送面(`ctx.pushLine` / `ctx.render`)照传。 */
|
|
55
|
+
export function startLedgerSurface(ctx = {}) {
|
|
56
|
+
let disposed = false
|
|
57
|
+
let timer = null
|
|
58
|
+
const tick = (startup) => {
|
|
59
|
+
if (disposed) return
|
|
60
|
+
if (!startup && ctx.state?.processing) return // 扫描不打断回合帧
|
|
61
|
+
try { runLedgerScan({ ...ctx, startup }) } catch { /* 可见面尽力——不崩(N1) */ }
|
|
62
|
+
}
|
|
63
|
+
setImmediate(() => {
|
|
64
|
+
if (disposed) return
|
|
65
|
+
tick(true)
|
|
66
|
+
timer = setInterval(() => tick(false), REFRESH_MS)
|
|
67
|
+
timer.unref?.()
|
|
68
|
+
})
|
|
69
|
+
return {
|
|
70
|
+
dispose() {
|
|
71
|
+
disposed = true
|
|
72
|
+
if (timer) clearInterval(timer)
|
|
73
|
+
timer = null
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
package/ledger.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger.mjs — 台账(需求池 / 技术待办)单一权威源(M2 模块——设计档
|
|
3
|
+
* docs/core/design/modules/ENGINEERING-MODE-V2-MODULE-LEDGER.md §2.2)。
|
|
4
|
+
*
|
|
5
|
+
* v1 md 台账(docs/TODO.md 扫描器)→ SQLite 单表(ledger.db,项目级、不进 git):六态 CHECK 机械
|
|
6
|
+
* 锁死、COUNT 单源计数、事务保证收口原子、归档 = 软删除。行龄源 = SQLite 时间戳(无 git blame)。
|
|
7
|
+
*
|
|
8
|
+
* 面:① 命令族(查询 = 只读全角色 / 写 = 仅主 agent——接线见 tools/index.mjs 与 agent/family-tools.mjs)
|
|
9
|
+
* ② 族发现(findProject / discoverFamily——标记 = ledger.db)③ scan 组装(buildScan——行集 + ledgerCount
|
|
10
|
+
* → scan 对象,形状契约 = pool/tech/aged/thresholdReached/actionable/root/name/ledger)
|
|
11
|
+
* ④ 通知去重(送达门 + 一次性去重——去重档跨会话、跨端共享)⑤ 格式 helper(行文本逐字契约)。
|
|
12
|
+
*
|
|
13
|
+
* W8 契约②:ledger-db.mjs 静态 import node:sqlite ⇒ 消费侧一律**动态 import** 本档(禁止静态 import)。
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, opendirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
17
|
+
import { configDir } from "./config.mjs"
|
|
18
|
+
import { ledgerDbPath, PENDING_STATUSES } from "./ledger-db.mjs"
|
|
19
|
+
import { ledgerQuery } from "./ledger-cmd.mjs"
|
|
20
|
+
|
|
21
|
+
/** 老化阈值(天——口径 = 需求档;`days` 参数可覆盖)。 */
|
|
22
|
+
export const AGING_DAYS = 30
|
|
23
|
+
/** 「可开批」阈值:同一板块(需求类条目 board 列)未决 ≥ 2。 */
|
|
24
|
+
export const THRESHOLD_BOARD = 2
|
|
25
|
+
/** 「可开批」阈值:需求池未决 ≥ 3。 */
|
|
26
|
+
export const THRESHOLD_POOL = 3
|
|
27
|
+
/** 刷新周期(N2 实现常量;VSC 端另有换项目事件触发)。 */
|
|
28
|
+
export const REFRESH_MS = 120000
|
|
29
|
+
/** 变化行去重档(跨会话、跨端共享——需求档 §1.18 F5;configDir 先例 = crash-reports 同区)。 */
|
|
30
|
+
export const NOTIFY_FILE = join(configDir, "ledger-notify.json")
|
|
31
|
+
/** 空族提示(仅命令面——运行时面静默,N1 / 设计档 §2.30.3.3)。 */
|
|
32
|
+
export const EMPTY_FAMILY_LINE = "台账:未发现台账。"
|
|
33
|
+
|
|
34
|
+
/** 条目归一化文本 = 条目键(去 `- [ ] ` 前缀 + 去首尾空白 + 连续空白折叠单空格)。
|
|
35
|
+
* 位置无关(位移 / 他条编辑稳定);自身文本变更 = 键变(最坏一次重报)。§2.30.3.2。 */
|
|
36
|
+
export function normalizeEntry(text) {
|
|
37
|
+
return text.replace(/^- \[[ x]\]\s+/, "").trim().replace(/\s+/g, " ")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 条目标题:首个 `**…**` 段;无粗体段 → 归一化文本前 20 字(超出加 `…`)。§2.30.3.3 L3。 */
|
|
41
|
+
export function entryTitle(text) {
|
|
42
|
+
const m = /\*\*(.+?)\*\*/.exec(text)
|
|
43
|
+
if (m) return m[1]
|
|
44
|
+
const norm = normalizeEntry(text)
|
|
45
|
+
return norm.length > 20 ? norm.slice(0, 20) + "…" : norm
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
49
|
+
|
|
50
|
+
/** 向上(含自身)最近的已注册台账项目根 → {root, ledger} / null(F4——CLI 锚 = cwd;
|
|
51
|
+
* 标记 = 用户数据目录里以该项目路径为键的台账库——项目目录内不落任何标记文件)。 */
|
|
52
|
+
export function findProject(anchor) {
|
|
53
|
+
let dir = resolve(anchor)
|
|
54
|
+
for (;;) {
|
|
55
|
+
const ledger = ledgerDbPath(dir)
|
|
56
|
+
if (isFile(ledger)) return { root: dir, ledger }
|
|
57
|
+
const parent = dirname(dir)
|
|
58
|
+
if (parent === dir) return null
|
|
59
|
+
dir = parent
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 同级枚举上限(N2 成本有界:父目录超此规模 = 缓存 / 临时等非仓族形态 → 退化空集,只显当前项目)。 */
|
|
64
|
+
export const MAX_SIBLING_SCAN = 100
|
|
65
|
+
|
|
66
|
+
/** 目录下已注册台账的直接子目录(目录名升序;不可读 → [];**惰性枚举 + 上限**——大目录零全量扫描)。 */
|
|
67
|
+
function ledgerChildren(dir) {
|
|
68
|
+
const out = []
|
|
69
|
+
let handle
|
|
70
|
+
try { handle = opendirSync(dir) } catch { return out }
|
|
71
|
+
try {
|
|
72
|
+
let n = 0
|
|
73
|
+
for (;;) {
|
|
74
|
+
const ent = handle.readSync()
|
|
75
|
+
if (!ent) break
|
|
76
|
+
if (++n > MAX_SIBLING_SCAN) return []
|
|
77
|
+
if (!ent.isDirectory()) continue
|
|
78
|
+
const root = join(dir, ent.name)
|
|
79
|
+
const ledger = ledgerDbPath(root)
|
|
80
|
+
if (isFile(ledger)) out.push({ root, ledger })
|
|
81
|
+
}
|
|
82
|
+
} catch { /* 读中断 → 已得子集(尽力而为) */ }
|
|
83
|
+
finally { try { handle.closeSync() } catch { /* 已关 */ } }
|
|
84
|
+
return out.sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 项目族发现:{current, projects}——projects = current + 其同级含台账库目录(**发现序:current 在前**,
|
|
88
|
+
* 其余按目录名升序);current 缺(容器目录,K6)→ 上下文目录向上最近「含台账子目录」者取其子目录。 */
|
|
89
|
+
export function discoverFamily(anchor) {
|
|
90
|
+
const current = findProject(anchor)
|
|
91
|
+
if (current) {
|
|
92
|
+
const siblings = ledgerChildren(dirname(current.root)).filter((p) => p.root !== current.root)
|
|
93
|
+
return { current, projects: [current, ...siblings] }
|
|
94
|
+
}
|
|
95
|
+
let dir = resolve(anchor)
|
|
96
|
+
for (;;) {
|
|
97
|
+
const kids = ledgerChildren(dir)
|
|
98
|
+
if (kids.length) return { current: null, projects: kids }
|
|
99
|
+
const parent = dirname(dir)
|
|
100
|
+
if (parent === dir) return { current: null, projects: [] }
|
|
101
|
+
dir = parent
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** scan 组装(SQLite 行集 + 计数单源 → scan 对象)——形状契约(设计档 §2.2):pool/tech/aged/
|
|
106
|
+
* agedKeys/agedTitles/thresholdReached/boards/actionable/root/name/ledger。行龄 = 时间戳
|
|
107
|
+
* (updated_at ?? created_at 距今 > days;未知不计——零假阳降级,同 v1 行龄未知口径)。
|
|
108
|
+
* `now`/`days` = 注入面(确定性用例——替代 v1 ageOf git 注入面)。 */
|
|
109
|
+
export function buildScan({ cwd, days = AGING_DAYS, now = Date.now() } = {}) {
|
|
110
|
+
const rows = ledgerQuery({ cwd })
|
|
111
|
+
const pending = rows.filter((r) => PENDING_STATUSES.includes(r.status))
|
|
112
|
+
const poolEntries = pending.filter((r) => r.kind === "requirement")
|
|
113
|
+
const techEntries = pending.filter((r) => r.kind === "tech_todo")
|
|
114
|
+
const candidates = techEntries.filter((r) => !r.trigger)
|
|
115
|
+
const aged = candidates.filter((r) => {
|
|
116
|
+
const t = r.updated_at ?? r.created_at
|
|
117
|
+
return t != null && (now - Date.parse(t)) / 86400000 > days
|
|
118
|
+
})
|
|
119
|
+
const boards = new Map()
|
|
120
|
+
for (const e of poolEntries) {
|
|
121
|
+
const key = e.board ?? (e.req_doc ? basename(String(e.req_doc).split(/\s+/)[0]) : null)
|
|
122
|
+
if (key) boards.set(key, (boards.get(key) ?? 0) + 1)
|
|
123
|
+
}
|
|
124
|
+
const thresholdReached = poolEntries.length >= THRESHOLD_POOL || [...boards.values()].some((n) => n >= THRESHOLD_BOARD)
|
|
125
|
+
const root = resolve(cwd)
|
|
126
|
+
return {
|
|
127
|
+
name: basename(root), root, ledger: ledgerDbPath(root),
|
|
128
|
+
pool: poolEntries.length, tech: techEntries.length, aged: aged.length,
|
|
129
|
+
agedKeys: aged.map((r) => normalizeEntry(r.title)), agedTitles: aged.map((r) => entryTitle(r.title)),
|
|
130
|
+
thresholdReached, boards, actionable: aged.length > 0 || thresholdReached,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** L1 状态标记(逐字——§2.30.3.3)。 */
|
|
135
|
+
export function formatMarker(scan) {
|
|
136
|
+
return scan ? `台账 ${scan.pool}·${scan.tech}` : null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** L2 明细行(逐字——`(老化 <n>)` 恒显;阈值达成加 ` — 可开批`)。 */
|
|
140
|
+
export function formatDetailLine(scan) {
|
|
141
|
+
return `台账 ${scan.name}:需求池 ${scan.pool} · 技术待办 ${scan.tech}(老化 ${scan.aged})${scan.thresholdReached ? " — 可开批" : ""}`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** L3 变化行·老化(titles = 新增老化条目标题;> 3 条时第三项后接 `;…`)。 */
|
|
145
|
+
export function formatAgingLine(scan, titles) {
|
|
146
|
+
return `台账变化:${scan.name} 老化首次越线 ${titles.length} 条(超 30 天未处置):${titles.slice(0, 3).join(";")}${titles.length > 3 ? ";…" : ""}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** L4 变化行·阈值。 */
|
|
150
|
+
export function formatThresholdLine(scan) {
|
|
151
|
+
return `台账变化:${scan.name} 需求池达阈值(${scan.pool} 条)— 可开批`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 明细行集 = {current} ∪ {可动作项目}(发现序——current 在前,其余按目录名升序;同项去重)。 */
|
|
155
|
+
export function detailScans(scans, current) {
|
|
156
|
+
const out = []
|
|
157
|
+
if (current) out.push(current)
|
|
158
|
+
for (const s of scans) if (s !== current && s.actionable) out.push(s)
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 变化行规划(纯函数):{lines:[{text,warn}], next:{aged,threshold}}(去重口径 §2.30.3.2)。 */
|
|
163
|
+
export function planChangeLines(prev, scan) {
|
|
164
|
+
const p = prev && typeof prev === "object" ? prev : {}
|
|
165
|
+
const before = new Set(Array.isArray(p.aged) ? p.aged : [])
|
|
166
|
+
const freshTitles = scan.agedKeys.map((k, i) => (before.has(k) ? null : scan.agedTitles[i])).filter((t) => t != null)
|
|
167
|
+
const lines = []
|
|
168
|
+
if (freshTitles.length) lines.push({ text: formatAgingLine(scan, freshTitles), warn: true })
|
|
169
|
+
if (scan.thresholdReached && !p.threshold) lines.push({ text: formatThresholdLine(scan), warn: true })
|
|
170
|
+
return { lines, next: { aged: scan.agedKeys, threshold: scan.thresholdReached } }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 去重档键 = 台账库绝对路径·正斜杠 + **盘符大写**(跨端同规则——CLI 的 `process.cwd()` 盘符大写、
|
|
174
|
+
* VSC 的 `uri.fsPath` 小写(`session-slots.mjs` `normalizeCwd` 同一契约——session / checkpoint /
|
|
175
|
+
* trace 跨端共享即赖此);CLI 写的键 VSC 须逐字认得)。 */
|
|
176
|
+
export function notifyKey(ledger) {
|
|
177
|
+
return resolve(ledger).replace(/\\/g, "/").replace(/^([a-z]):/, (_, d) => `${d.toUpperCase()}:`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 去重档读(缺失 / 坏 JSON → 空态——N1 降级不崩)。 */
|
|
181
|
+
export function loadNotifyState(file = NOTIFY_FILE) {
|
|
182
|
+
try {
|
|
183
|
+
const j = JSON.parse(readFileSync(file, "utf8"))
|
|
184
|
+
if (j && typeof j === "object" && j.ledgers && typeof j.ledgers === "object") return { version: 1, ledgers: j.ledgers }
|
|
185
|
+
} catch { /* 缺失 / 坏档 → 空态 */ }
|
|
186
|
+
return { version: 1, ledgers: {} }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 去重档写(temp + rename 防撕裂;写失败静默返回 false——唯一写面,仅送达后调)。 */
|
|
190
|
+
export function saveNotifyState(file, state) {
|
|
191
|
+
try {
|
|
192
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
193
|
+
const tmp = `${file}.tmp-${process.pid}`
|
|
194
|
+
writeFileSync(tmp, JSON.stringify(state), "utf8")
|
|
195
|
+
renameSync(tmp, file)
|
|
196
|
+
return true
|
|
197
|
+
} catch { return false }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── re-export(拆分件接口——命令面接线 = 动态 import 本档,KD-M2-3) ──
|
|
201
|
+
export { ALLOWED_MIGRATIONS, ledgerDbPath, openLedger, PENDING_STATUSES, _setLedgerDirForTest, _resetLedgerDirForTest } from "./ledger-db.mjs"
|
|
202
|
+
export { ledgerAdd, ledgerAddTool, ledgerClose, ledgerCloseTool, ledgerCount, ledgerCountTool, ledgerQuery, ledgerQueryTool, ledgerUpdate, ledgerUpdateTool } from "./ledger-cmd.mjs"
|