@thincoder/core 0.10.11 → 0.10.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent/run-stages.mjs +33 -29
- package/agent/setup.mjs +3 -0
- package/agent/turn-loop.mjs +3 -0
- package/agent-tools/batch-gate.mjs +65 -49
- package/agent-tools/batch-lifecycle.mjs +82 -12
- package/agent-tools/batch-paths.mjs +10 -8
- package/agent-tools/batch-skeleton.mjs +9 -1
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/design-slots.mjs +72 -12
- package/agent-tools/design-token.mjs +18 -2
- package/agent-tools/manifest.mjs +1 -1
- package/agent-tools/review-facts.mjs +1 -1
- package/agent.mjs +1 -0
- package/client-identity.mjs +54 -0
- package/config.mjs +19 -13
- package/context.mjs +5 -5
- package/doc-check/index.mjs +28 -15
- package/doc-check/paragraph.mjs +6 -3
- package/embedding.mjs +2 -0
- package/generate-title.mjs +3 -2
- package/ledger-close-proof.mjs +80 -0
- package/ledger-cmd.mjs +64 -53
- package/ledger-db.mjs +60 -59
- package/ledger-tools.mjs +60 -23
- package/ledger.mjs +12 -10
- package/manifest-discovery.mjs +41 -26
- package/manifest.mjs +17 -16
- package/model-specs.mjs +17 -18
- package/package.json +1 -1
- package/prompts/advisor-design.md +4 -3
- package/prompts/common.md +11 -1
- package/prompts/discipline-engineering.md +16 -7
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-coder.md +8 -1
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +11 -1
- package/prompts/persona-engineering.md +23 -11
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +19 -3
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +10 -4
- package/provider/list-models.mjs +4 -3
- package/provider/responses.mjs +17 -7
- package/session-lifecycle.mjs +3 -0
- package/session-slot-write.mjs +21 -1
- package/session-slots-manifest.mjs +21 -4
- package/token-ttl.mjs +68 -4
- package/token-window.mjs +23 -2
- package/tool-docs/wait_for.md +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to the core package are documented here.
|
|
4
4
|
Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
|
|
5
5
|
|
|
6
|
+
## [0.10.13] — 2026-10-26
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`client-identity.mjs`:模型请求带产品身份头**——新增 `setClientIdentity({ surface, version })` ∥ `clientIdentity()` ∥ `userAgent()` ∥ `withUserAgent(headers)`;字形 `ThinCoder/<surface>/<version>`,surface 小写枚举 `cli` ∥ `vscode` ∥ `desktop` ∥ `server`(内部缺省 `core`)。
|
|
11
|
+
- **不可被渠道配置覆盖**:`withUserAgent` 先删任何大小写变体的 `user-agent` 键再写本产品值——fetch `Headers` 对变体是合并语义,只靠后写会得双值。
|
|
12
|
+
- **七处消费点全部装配**:provider/{core,anthropic,google,responses,list-models} · `generate-title.mjs` · `embedding.mjs`。
|
|
13
|
+
- **缺省哨兵** `ThinCoder/core/0.0.0`:任何真实端版本都不等于 `0.0.0`,四端进程内出现即声明漏接。
|
|
14
|
+
|
|
15
|
+
### Note
|
|
16
|
+
|
|
17
|
+
- **核须先于端发版**:四端都载本件,旧核 + 新端 = 启动即 `ERR_MODULE_NOT_FOUND`。
|
|
18
|
+
|
|
19
|
+
## [0.10.12] — 2026-10-26
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **台账在无根目录从「静默空集」改为「抛明」**:项目根推导不出来时,`query` / `count` / `add` / `update` / `close` 一律抛错并说明「自哪个路径、向上找到哪里为止」,不再返回空集。空集与「记到别的项目」在回执上分不开,静默空集等于让人以为没有台账。
|
|
24
|
+
- **多根工作区下建批档:报明项目并给路径,不再落锚目录**:工作目录本身没有项目、直接子目录里有项目(1 个同判、多个同判),此时建批档会列全下辖项目并给显式路径指引;目标属于某个项目时照常落该项目的声明基底。
|
|
25
|
+
- **项目根解析:多根工作区一律「≥1 同判」**:取消此前「恰一个子项目」与「多个」的区别——工作目录下直接子目录带项目清单,不论 1 个还是多个,都是多根工作区,逻辑一致。
|
|
26
|
+
|
|
6
27
|
## [0.10.11] — 2026-10-25
|
|
7
28
|
|
|
8
29
|
### Added
|
package/advisor/messages.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable
|
|
|
12
12
|
import { injectProjectGuide, injectDocumentMap, injectProjectStandards, NO_GIT_NOTICE } from "./project-context.mjs"
|
|
13
13
|
|
|
14
14
|
// Structural split (not an authority migration): the moved helpers stay reachable here.
|
|
15
|
-
export {
|
|
15
|
+
export { findGuideRoot, injectProjectGuide } from "./project-context.mjs"
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Build the mechanical review-object declaration block (AGENT-LOOP-ASYNC-POOL.md §6.18
|
|
@@ -51,7 +51,7 @@ export const NO_GIT_NOTICE = "(No git repository detected — change-set context
|
|
|
51
51
|
* @param {string[]} scopeFiles — cwd-relative review-scope paths (may be empty)
|
|
52
52
|
* @returns {string|null} absolute project root with an AGENTS.md, or null
|
|
53
53
|
*/
|
|
54
|
-
export function
|
|
54
|
+
export function findGuideRoot(cwd, scopeFiles) {
|
|
55
55
|
// Normalize separators before comparing: input paths may use either
|
|
56
56
|
// convention (join() → "\\" on Windows; tool args / tests → "/"). Mixed
|
|
57
57
|
// styles made isInside(cwd + sep) miss legitimately nested paths.
|
|
@@ -88,7 +88,7 @@ export function findProjectRoot(cwd, scopeFiles) {
|
|
|
88
88
|
*/
|
|
89
89
|
export function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
90
90
|
parts.push("## Project Guide (AGENTS.md)")
|
|
91
|
-
const root =
|
|
91
|
+
const root = findGuideRoot(agent.cwd, scopeFiles)
|
|
92
92
|
const path = root ? join(root, "AGENTS.md") : null
|
|
93
93
|
let text
|
|
94
94
|
if (!path) {
|
package/agent/run-stages.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../c
|
|
|
11
11
|
// §6.17/D-CC32 溢出守卫(兜底,非主修):预算面单源 requestBudget ∥ 再估面 contextUsage(与压缩判定同一函数)
|
|
12
12
|
import { requestBudget } from "../config.mjs"
|
|
13
13
|
import { contextUsage } from "../token-window.mjs"
|
|
14
|
+
import { contextOverflowError } from "../provider/errors.mjs"
|
|
14
15
|
import { ensureAutoReminder, injectEngineeringReminder, ContinueError, snapshotGuard } from "./helpers.mjs"
|
|
15
16
|
import { pushManifestStateReminder } from "./setup-reminders.mjs"
|
|
16
17
|
import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
|
|
@@ -97,6 +98,19 @@ export async function runCompactionCheck(agent, ctx) {
|
|
|
97
98
|
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
98
99
|
ensureAutoReminder(agent)
|
|
99
100
|
}
|
|
101
|
+
// 压缩失败记账(两处共用:下面的阈值面 catch ∥ 下面的守卫面强制压缩)——AbortError 必须透传。
|
|
102
|
+
const noteCompressFailure = (compressError) => {
|
|
103
|
+
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
104
|
+
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
105
|
+
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
106
|
+
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
107
|
+
callbacks?.onCompressFail?.(compressError)
|
|
108
|
+
}
|
|
109
|
+
const degradeIfCompressExhausted = () => {
|
|
110
|
+
if (agent._compressFailures < COMPRESS_FAILURE_LIMIT) return
|
|
111
|
+
agent._compressFailures = 0
|
|
112
|
+
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
113
|
+
}
|
|
100
114
|
try {
|
|
101
115
|
let compacted = await compressIfNeeded(
|
|
102
116
|
agent, threshold, callbacks,
|
|
@@ -111,49 +125,39 @@ export async function runCompactionCheck(agent, ctx) {
|
|
|
111
125
|
}
|
|
112
126
|
if (compacted) bookkeepCompaction()
|
|
113
127
|
} catch (compressError) {
|
|
114
|
-
|
|
115
|
-
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
116
|
-
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
117
|
-
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
118
|
-
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
119
|
-
callbacks?.onCompressFail?.(compressError)
|
|
128
|
+
noteCompressFailure(compressError)
|
|
120
129
|
if (pending) {
|
|
121
130
|
// 失败注记(§6.16.2 回执面②):模型面告知缺口——既有失败链 / 面板面零改,仅补一行机器注记
|
|
122
131
|
const { compactFailureNote } = await import("../agent-tools/context.mjs")
|
|
123
132
|
agent.history.push({ role: "user", content: compactFailureNote(errText(compressError), agent._compressFailures), transient: true })
|
|
124
133
|
}
|
|
125
|
-
|
|
126
|
-
agent._compressFailures = 0
|
|
127
|
-
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
128
|
-
}
|
|
134
|
+
degradeIfCompressExhausted()
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
// 溢出守卫(CONTEXT-COMPACTION.md §6.17 / D-CC32——兜底,非主修;本缝 = catch 闭合后、返回前)。
|
|
132
|
-
// 判据 =
|
|
133
|
-
//
|
|
134
|
-
// ⇒ 守卫不覆盖(服务端 400 兜——把规格 maxOutput 当实发判会让 doubao 三行永久发不出去)。
|
|
138
|
+
// 判据 = prompt 估算 > `budget.limit`(**无档次门**——absent 档 limit 取地板 max(window − reserve, threshold),
|
|
139
|
+
// 恒为正数、恒可判)。**判据不用 maxTokens**:未发时为 undefined ⇒ NaN ⇒ 守卫静默失效(§6.17)。
|
|
135
140
|
// 真 ⇒ 以 force 强制压缩一次(先压再判);再估仍超 ⇒ 抛三实数、回合请求零发。不截断、不静默削输出。
|
|
136
141
|
const budget = requestBudget(agent.provider)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
let usage = contextUsage(agent, compactionOverhead)
|
|
143
|
+
if (usage.total > budget.limit) {
|
|
144
|
+
// 强压一次:失败走与阈值面同一条记账 / 降级链(不把摘要错误当回合错误直抛——
|
|
145
|
+
// 记账后仍按「压后仍超」判:降级截断可能已把上下文压到线下)。
|
|
146
|
+
try {
|
|
140
147
|
const compacted = await compressIfNeeded(
|
|
141
148
|
agent, threshold, callbacks, { ...compactionOverhead, force: true }, signal,
|
|
142
149
|
)
|
|
143
150
|
if (compacted) bookkeepCompaction()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
overflow.retryable = false
|
|
155
|
-
throw overflow
|
|
156
|
-
}
|
|
151
|
+
} catch (compressError) {
|
|
152
|
+
noteCompressFailure(compressError)
|
|
153
|
+
degradeIfCompressExhausted()
|
|
154
|
+
}
|
|
155
|
+
usage = contextUsage(agent, compactionOverhead)
|
|
156
|
+
if (usage.total > budget.limit) {
|
|
157
|
+
const remedy = budget.limit < 0
|
|
158
|
+
? `max_tokens ${budget.maxTokens} alone exceeds the window — lower provider.maxTokens or switch to a larger-window model`
|
|
159
|
+
: "forced compaction ran but the prompt still exceeds the window; reduce the conversation (/compact or prune) before retrying"
|
|
160
|
+
throw contextOverflowError(agent.provider, usage.total, budget, remedy)
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
163
|
}
|
package/agent/setup.mjs
CHANGED
|
@@ -64,6 +64,9 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
64
64
|
|
|
65
65
|
agent._lastPromptTokens = null
|
|
66
66
|
agent._usageAtLen = null
|
|
67
|
+
// §6.17 估算自校准比(`_tok` / `_tokRatio`)**不在本处清**:本函数是每 run 的 pre-flight,
|
|
68
|
+
// 而在每轮用户消息上归零会让池化比退化成单样本(R2 失效)。初值在 `agent.mjs` 构造字面量,
|
|
69
|
+
// 清空点 = `/new`(`session-lifecycle.mjs`)∥ `key` 变自清(`token-window.mjs` `recordTokenSample`)。
|
|
67
70
|
// Cumulative provider cache accounting (batch 2026-10-18-context-cache-visibility):
|
|
68
71
|
// the service reports prompt_cache_hit/miss per response; the terminal faces each kept
|
|
69
72
|
// their own tally, so the CORE had none and `context` could not report cache efficiency.
|
package/agent/turn-loop.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { recordToolResults } from "./record-results.mjs"
|
|
|
13
13
|
import { injectPostTurn } from "./post-turn.mjs"
|
|
14
14
|
import { handleCompletion } from "./completion.mjs"
|
|
15
15
|
import { runCompactionCheck, injectTurnReminders, injectResponseReminders } from "./run-stages.mjs"
|
|
16
|
+
import { recordTokenSample } from "../token-window.mjs"
|
|
16
17
|
import { callModelTurn } from "./chat-call.mjs"
|
|
17
18
|
import {
|
|
18
19
|
turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
|
|
@@ -194,6 +195,8 @@ export async function runTurnLoop(agent, {
|
|
|
194
195
|
agent._lastPromptTokens = response.usage.prompt_tokens
|
|
195
196
|
agent._usageAtLen = agent.history.length
|
|
196
197
|
}
|
|
198
|
+
// §6.17 估算自校准:服务端真值 ÷ 输入侧估算(`estimated` = 发送面净判同一个 estPrompt)
|
|
199
|
+
recordTokenSample(agent, agent.provider, response.estimated, response.usage)
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-tools/batch-gate.mjs — 文档机检门(`batch` 工具两机械点的**单源门**)。
|
|
3
3
|
*
|
|
4
|
-
* 权威规格 = `docs/core/design/DOC-DISCIPLINE.md` §3.20「结构兜底」(判据 / 范围 /
|
|
4
|
+
* 权威规格 = `docs/core/design/DOC-DISCIPLINE.md` §3.20「结构兜底」(判据 / 范围 / 适用面 /
|
|
5
5
|
* ERROR 消费分野 / 门自身异常 / 不检面)——本档只实现,**不重述口径**(D2)。需求 = `DOC-CHECK.md` F9 / AC-7。
|
|
6
6
|
*
|
|
7
7
|
* 调用面两处、各一取值(`scope` 语义 = 规范「调用面开关,无第三值」):
|
|
8
8
|
* ① `batch.mjs` append 写 §2 / §5(`!depth0 && !review`)⇒ `scope: "writer"`——范围 = 写者触达记账
|
|
9
9
|
* (与工具面 gate 同源 `docsSource="session"`);`REFUSED` ⇒ 调用面 throw;`ERROR` / 门自身异常 ⇒ 放行 +
|
|
10
10
|
* 回执明示(子代理无法重启会话,卡死写账 = 无账之笔;② 会拒收口 ⇒ 轮次不会被当成已通过)。
|
|
11
|
-
* ② `batch-lifecycle.mjs` close ∥ status 值余核「已收口」⇒ `scope: "close"`——范围 =
|
|
12
|
-
* `git diff --name-only <批起 sha> HEAD`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* ② `batch-lifecycle.mjs` close ∥ status 值余核「已收口」⇒ `scope: "close"`——范围 = 本批笔域集
|
|
12
|
+
* (父侧触达账 ∪ 本批提交 diff = `git diff --name-only <批起 sha> HEAD` 内的 `.md`);
|
|
13
|
+
* `REFUSED`(K ≥ 1)∥ `ERROR` ⇒ 调用面 throw(fail-closed)。
|
|
14
|
+
* **单集**:范围内不设第二集——工作树里范围外的档不入判、不入报面(用户 2026-10-25 裁定「只判指定文件」)。
|
|
15
15
|
*
|
|
16
16
|
* 判据零变:本档不新增判据——只决定「判谁」与「何时强制」;判据与裁定串由产品引擎
|
|
17
17
|
* `doc-check/index.mjs` 产出(判据源唯一)。
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* ②
|
|
19
|
+
* 两条边界(设计与实测同口径):
|
|
20
|
+
* ① **批档自身不入范围**(载体不是判据对象——判它只会把载体的超段记成本批欠账);
|
|
21
|
+
* ② **提交 diff 腿的基 sha 只读批档 §1 的记入行**(`批起 sha = <sha>`——取行内首个 40 位十六进制串);
|
|
22
|
+
* 取不到(无该行 ∥ 形态不合 ∥ git 跑不出该 diff)⇒ 只认触达账 + 回执明示「基 sha 未定」(不静默)。
|
|
22
23
|
*/
|
|
23
24
|
import { execFileSync } from "node:child_process"
|
|
25
|
+
import { readFileSync } from "node:fs"
|
|
24
26
|
import { relative, resolve } from "node:path"
|
|
25
27
|
|
|
26
28
|
import { owningProject, readManifest } from "../manifest.mjs"
|
|
27
29
|
import { runDocCheck } from "../doc-check/index.mjs"
|
|
30
|
+
import { sectionHeaderRe } from "./batch-skeleton.mjs"
|
|
28
31
|
import { batchProjectRoot } from "./batch-paths.mjs"
|
|
29
32
|
|
|
30
33
|
/** 平台路径键(win32 大小写不敏感——只作集比对,非判据面)。 */
|
|
31
34
|
const pathKey = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
32
35
|
|
|
33
|
-
/** 文档档谓词 + 绝对化(触达账 /
|
|
36
|
+
/** 文档档谓词 + 绝对化(触达账 / 提交 diff 同形;非 `.md` 不进范围)。 */
|
|
34
37
|
const isDocPath = (p) => typeof p === "string" && p.trim() !== "" && p.toLowerCase().endsWith(".md")
|
|
35
38
|
function docPaths(list, base) {
|
|
36
39
|
const out = []
|
|
@@ -39,18 +42,41 @@ function docPaths(list, base) {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* `<批起 sha>` 的取法(唯一)= 批档 §1 的记入行 `批起 sha = <sha>`:段界消费 `sectionHeaderRe(1)`
|
|
46
|
+
* (段头形态单源——`## §10` 不误命中 `§1`),取段内该行行内首个 40 位十六进制串。
|
|
47
|
+
* 无该行 ∥ 形态不合 ∥ 读不到 ⇒ `null`——调用面按「基 sha 未定」明示,不静默;段界外提到该行形不算数
|
|
48
|
+
* (记入行只认 §1:取法与写入义务单源 = 规范「② 的范围」)。
|
|
45
49
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
50
|
+
function baseShaOf(abs) {
|
|
51
|
+
let src
|
|
52
|
+
try { src = readFileSync(abs, "utf8") } catch { return null }
|
|
53
|
+
const hdr = sectionHeaderRe(1).exec(src)
|
|
54
|
+
if (!hdr) return null
|
|
55
|
+
const nextRe = /^## §\d/gm
|
|
56
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
57
|
+
const next = nextRe.exec(src)
|
|
58
|
+
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
59
|
+
for (const line of body.split(/\r?\n/)) {
|
|
60
|
+
if (!line.includes("批起 sha")) continue
|
|
61
|
+
const sha = /[0-9a-f]{40}/i.exec(line)?.[0]
|
|
62
|
+
if (sha) return sha
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 本批提交 diff 腿(`git diff --name-only <sha> HEAD`——只取清单,判定归引擎):路径以**仓根**
|
|
69
|
+
* (`rev-parse --show-toplevel`)为基还原——项目根住大仓子目录时条目不丢(清单相对仓根)。
|
|
70
|
+
* `-c core.quotepath=false`:不关此开关时 git 把含非 ASCII 字节的路径整串加引号转义,
|
|
71
|
+
* 下流 `endsWith(".md")` 判据会判假 ⇒ 非 ASCII 档名的档静默掉出范围(正是本批要关的「范围跑偏」)。
|
|
72
|
+
* 跑不出(非仓 / sha 不可解析)⇒ `ok: false` + 空集(调用面按「基 sha 未定」明示,不静默)。
|
|
73
|
+
*/
|
|
74
|
+
function committedDocs(root, sha) {
|
|
75
|
+
const run = (cwd, argv) => execFileSync("git", ["-C", cwd, "-c", "core.quotepath=false", ...argv], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 8000 })
|
|
48
76
|
try {
|
|
49
|
-
const top = run(root, ["rev-parse", "--show-toplevel"]).trim()
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const untracked = run(base, ["ls-files", "--others", "--exclude-standard"]).split(/\r?\n/)
|
|
53
|
-
return { ok: true, base, files: [...changed, ...untracked].filter(Boolean) }
|
|
77
|
+
const top = run(root, ["rev-parse", "--show-toplevel"]).trim() || root
|
|
78
|
+
const files = run(top, ["diff", "--name-only", sha, "HEAD"]).split(/\r?\n/).filter(Boolean)
|
|
79
|
+
return { ok: true, base: top, files }
|
|
54
80
|
} catch {
|
|
55
81
|
return { ok: false, base: root, files: [] }
|
|
56
82
|
}
|
|
@@ -69,15 +95,9 @@ function hitEntries(res, root) {
|
|
|
69
95
|
return { aligned, entries }
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
/**
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** 报面组装:只换首行 + 插入本条附加行(其余行 = 引擎原样,不重写格式);`dropDisposal` ⇒ 剔除引擎的
|
|
78
|
-
* 「处置:同轮修正后复跑…」行(close 面 PASS 且命中全属他批在途时——该行会把读者引去改别人的档)。 */
|
|
79
|
-
function composeReport(res, first, notes, dropDisposal) {
|
|
80
|
-
const lines = String(res.report).split("\n").filter((l) => !(dropDisposal && l.startsWith("处置:")))
|
|
98
|
+
/** 报面组装:只换首行 + 插入本条附加行(其余行 = 引擎原样,不重写格式)。 */
|
|
99
|
+
function composeReport(res, first, notes) {
|
|
100
|
+
const lines = String(res.report).split("\n")
|
|
81
101
|
lines[0] = first
|
|
82
102
|
if (notes.length) {
|
|
83
103
|
const at = lines.findIndex((l) => l.startsWith("处置:"))
|
|
@@ -88,7 +108,7 @@ function composeReport(res, first, notes, dropDisposal) {
|
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
/** 空范围返回形(`range` 恒在场——回执要读 `范围 N 档`)。 */
|
|
91
|
-
const emptyRange = (basis) => ({ count: 0, k: 0,
|
|
111
|
+
const emptyRange = (basis) => ({ count: 0, k: 0, basis })
|
|
92
112
|
const errorState = (first, range = emptyRange("none")) => ({ state: "error", first, report: first, hitLines: [], noteLines: [], range })
|
|
93
113
|
|
|
94
114
|
/**
|
|
@@ -96,7 +116,7 @@ const errorState = (first, range = emptyRange("none")) => ({ state: "error", fir
|
|
|
96
116
|
* @param {{abs:string, cwd?:string, agent?:object, scope?:'writer'|'close'}} o
|
|
97
117
|
* `abs` = 目标批次档绝对路径(根解析用);`agent._touchedFiles` = 触达记账。
|
|
98
118
|
* @returns {{state:'pass'|'refused'|'error'|'not-applicable', first:string|null, report:string,
|
|
99
|
-
* hitLines:string[], noteLines:string[], range:{count:number, k:number,
|
|
119
|
+
* hitLines:string[], noteLines:string[], range:{count:number, k:number, basis:string}}}
|
|
100
120
|
* `first` = 唯一机判串(首行 `DOC-CHECK GATE:`);`not-applicable` ⇒ `first = null` + 固定回执行。
|
|
101
121
|
*/
|
|
102
122
|
export function runBatchGate({ abs, cwd, agent = {}, scope = "writer" } = {}) {
|
|
@@ -126,46 +146,42 @@ function evaluate({ abs, cwd, agent, scope }) {
|
|
|
126
146
|
if (!read.ok && read.reason === "missing") {
|
|
127
147
|
return { state: "not-applicable", first: null, report: "[doc-check gate] 未跑(本项目无声明面)", hitLines: [], noteLines: [], range: emptyRange("none") }
|
|
128
148
|
}
|
|
129
|
-
// 范围(判谁):writer = 写者触达账;close =
|
|
149
|
+
// 范围(判谁):writer = 写者触达账;close = 本批笔域集 = 触达账 ∪ 本批提交 diff(**单集**——
|
|
150
|
+
// 范围内不设第二集:工作树里范围外的档不入判、不入报面)。
|
|
130
151
|
// 批档自身不入范围(载体不是判据对象——判它只会把载体的超段记成本批欠账)。
|
|
131
152
|
const selfKey = pathKey(resolve(abs))
|
|
132
153
|
const touched = docPaths(agent?._touchedFiles, base).filter((p) => pathKey(p) !== selfKey)
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
154
|
+
const baseSha = close ? baseShaOf(abs) : null
|
|
155
|
+
const diff = baseSha ? committedDocs(root, baseSha) : { ok: false, base: root, files: [] }
|
|
156
|
+
const diffDocs = docPaths(diff.ok ? diff.files : [], diff.base).filter((p) => pathKey(p) !== selfKey)
|
|
157
|
+
const docs = close ? [...new Set([...touched, ...diffDocs])] : touched
|
|
158
|
+
const basis = !close ? "touched" : diff.ok ? "touched+diff" : "touched(基 sha 未定)"
|
|
137
159
|
const res = runDocCheck(root, { mode: "gate", docs, docsSource: close ? "explicit" : "session" })
|
|
138
160
|
|
|
139
161
|
const { aligned, entries } = hitEntries(res, root)
|
|
140
|
-
const
|
|
141
|
-
const own = close ? entries.filter((e) => ownKeys.has(pathKey(e.file))) : entries
|
|
142
|
-
const foreign = close ? entries.filter((e) => !ownKeys.has(pathKey(e.file))) : []
|
|
143
|
-
const k = own.length
|
|
162
|
+
const k = entries.length // 单集:命中数即 K(没有第二集可排除)
|
|
144
163
|
const first = k >= 1
|
|
145
164
|
? `DOC-CHECK GATE: REFUSED(命中 ${k} 条)`
|
|
146
165
|
: (res.checkFiles.length ? "DOC-CHECK GATE: PASS" : "DOC-CHECK GATE: PASS(0 档)")
|
|
147
166
|
|
|
148
|
-
//
|
|
167
|
+
// 附加行(不静默面):命中行不可对齐(判定面自述)+ 收口面取不到基(只认触达账)。
|
|
149
168
|
const notes = []
|
|
150
169
|
if (!aligned) notes.push("命中行不可对齐(引擎报面 ✗ 行数与命中数不等)——显示行按自造形,判定按命中对象")
|
|
151
|
-
if (close)
|
|
152
|
-
if (foreign.length) notes.push(`他批在途 ${foreign.length} 档(非本批笔域 · 未计入 K · 请其作者在其批修复):${foreign.map((e) => locationOf(e, root)).join(" ∥ ")}`)
|
|
153
|
-
if (!git.ok) notes.push("git 面不可得——他批在途集只按触达账判(不静默)")
|
|
154
|
-
notes.push("基 sha 未定——本批笔域集只认触达账(git 提交面由父侧协议清单兜)")
|
|
155
|
-
}
|
|
170
|
+
if (close && !diff.ok) notes.push("基 sha 未定——本批笔域集只认触达账")
|
|
156
171
|
|
|
157
172
|
return {
|
|
158
173
|
state: k >= 1 ? "refused" : "pass",
|
|
159
174
|
first,
|
|
160
|
-
report: composeReport(res, first, notes
|
|
175
|
+
report: composeReport(res, first, notes),
|
|
161
176
|
hitLines: entries.map((e) => e.line),
|
|
162
177
|
noteLines: notes,
|
|
163
|
-
range: { count: res.checkFiles.length, k,
|
|
178
|
+
range: { count: res.checkFiles.length, k, basis },
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
/** 成功回执行(append / close / status 三面同形;`null` 门 ⇒ 零改——非挂门路径回执逐字旧形)。
|
|
168
|
-
*
|
|
183
|
+
* 两件 = verdict + `范围 N 档` 事实行;附注行(「基 sha 未定」∥ 命中行不可对齐)紧随其后**逐字**列出——
|
|
184
|
+
* 不静默吞掉可见性。 */
|
|
169
185
|
export function gateReceiptLine(gate) {
|
|
170
186
|
if (!gate) return ""
|
|
171
187
|
if (gate.state === "not-applicable") return "\n[doc-check gate] 未跑(本项目无声明面)"
|
|
@@ -184,6 +200,6 @@ export function gateRefusalMessage(gate, action = "append") {
|
|
|
184
200
|
const detail = gate.state === "error" ? String(gate.report ?? "").split("\n").slice(1) : gate.hitLines
|
|
185
201
|
const tail = action === "append"
|
|
186
202
|
? "处置:同轮修正后复跑(改文档 ∥ 修引擎);不转台账;拿不准 ⇒ 上抛主 agent 裁决(主 agent 拿不准 ⇒ 转呈用户)。Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close)."
|
|
187
|
-
: "处置:同轮修正后复跑(改文档 ∥
|
|
203
|
+
: "处置:同轮修正后复跑(改文档 ∥ 修引擎);不转台账;拿不准 ⇒ 上抛主 agent 裁决(主 agent 拿不准 ⇒ 转呈用户)。Nothing was written."
|
|
188
204
|
return [lead, gate.first, ...(detail ?? []), ...(gate.noteLines ?? []), tail].join("\n")
|
|
189
205
|
}
|
|
@@ -22,12 +22,22 @@
|
|
|
22
22
|
*
|
|
23
23
|
* 链完整性门(§4.18-A):`close` ∥ `status` 写「已收口」两路在写盘前同过 `assertCloseoutChainComplete`
|
|
24
24
|
* ——§2/§3/§4/§6 各段有内容 ∥ §1 声明不适用(仅 §2/§3/§4 可声明;§6 不可声明 · §5 段号豁免);
|
|
25
|
-
* 缺段 ⇒ 点名拒(门位 =
|
|
25
|
+
* 缺段 ⇒ 点名拒(门位 = 冻结拒写之后、文档机检门之前)。
|
|
26
|
+
*
|
|
27
|
+
* 本批批内单测档门(`TESTING.md` §3.5 零留存的收口门):同一两路在链门之后、文档机检门之前同过
|
|
28
|
+
* `assertUnitTestFilesCleared`——本批自己的单元测试档仍在盘 ⇒ 点名拒(单源叶档 =
|
|
29
|
+
* `batch-unit-test-gate.mjs`;判据 = 同目录最长批档名前缀归属)。门只拒、不代删。
|
|
30
|
+
*
|
|
31
|
+
* 重开径(§4.18-B):已收口档
|
|
26
32
|
* `status` §1 → 进行中 + `note` 非空 ⇒ 状态行改写 + §1 段尾追记行(只追加,其余行字节不变)。
|
|
27
33
|
*
|
|
28
34
|
* 收口重锚(§6.2 · C · AC-M2-30):收口有两条通路——`close` ∥ `status` 写 §1「已收口」——两路都冻结档、
|
|
29
35
|
* 都让批「收口了」,故**两路同调**单源 `reanchorOnClose`(本档下方),调点均在状态行写盘之后。
|
|
30
36
|
* 一次收口只出一份 `[ledger reanchor]` 回执段(判据 = 前态未冻结:已冻结档在两路都被冻结门早拒)。
|
|
37
|
+
*
|
|
38
|
+
* 收口自动消费本批设计凭证(DESIGN-TOKEN-SETTLEMENT §10 · 2026-10-26):同一两路在**写状态行之前**
|
|
39
|
+
* 调 `consumeBatchDesignTokens`(判据 = 槽的批次归属旁列;口径单源见该档 §10)。消费失败 ⇒ 原样上抛、
|
|
40
|
+
* close 被拒、档零字节变——位次与重锚(写盘之后)相反,这是机制本体(KD-BG3)。
|
|
31
41
|
*/
|
|
32
42
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
33
43
|
import { dirname, relative, resolve } from "node:path"
|
|
@@ -39,6 +49,8 @@ import {
|
|
|
39
49
|
} from "./batch-skeleton.mjs"
|
|
40
50
|
import { resolveBatchCreatePath } from "./batch-paths.mjs"
|
|
41
51
|
import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
|
|
52
|
+
import { assertUnitTestFilesCleared } from "./batch-unit-test-gate.mjs"
|
|
53
|
+
import { batchPathKey, executeConsumeDesignAction, unionBatches, unionSlots } from "./design-slots.mjs"
|
|
42
54
|
|
|
43
55
|
/** 可读文件判据(存在且为文件——目录/缺失同判不可读;与主档同名 helper 同型——KD-4 单向
|
|
44
56
|
* 依赖下各自持有,三行谓词不构成第二权威源)。 */
|
|
@@ -397,7 +409,7 @@ function assertStatusNote(note) {
|
|
|
397
409
|
* path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path ⇒ 拒(目标 = spawn/实例注入,语法上
|
|
398
410
|
* 写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):档头结构行含骨架死占位 ⇒ 拒
|
|
399
411
|
* (段体引用豁免;close 不拦——收口是主 agent 终态动作)。
|
|
400
|
-
* **收口第二通路**:值余核 = §1「已收口」时本 action 同样收口(链门 + 机检门 + 冻结与 close 同序)⇒
|
|
412
|
+
* **收口第二通路**:值余核 = §1「已收口」时本 action 同样收口(链门 + 批内单测档门 + 机检门 + 冻结与 close 同序)⇒
|
|
401
413
|
* 状态行写盘后同调 `reanchorOnClose`(单源——不复制 close 径逻辑),回执携同一 `[ledger reanchor]` 段。
|
|
402
414
|
* @returns {Promise<string>} 成功消息
|
|
403
415
|
*/
|
|
@@ -470,10 +482,15 @@ export async function statusBatchRecord({ args, ctx, review, pickTarget, onWritt
|
|
|
470
482
|
// (`STATUS_WORDS[1].closed` 只属 §1 项,`assertStatusValue` 亦拒他段该值;显式 `seg === 1` 让不变量不靠巧合)。
|
|
471
483
|
const closingValue = seg === 1 && stripDecorations(value) === STATUS_WORDS[1].closed
|
|
472
484
|
if (closingValue) assertCloseoutChainComplete(src)
|
|
485
|
+
// 本批批内单测档门(`TESTING.md` §3.5——门位 = 链门之后、文档机检门之前):本批自己的单元测试档
|
|
486
|
+
// 仍在盘 ⇒ 拒(零留存的结构兜底;两路同判——close 与 status 写「已收口」各过一次;放行零回执)。
|
|
487
|
+
if (closingValue) assertUnitTestFilesCleared(abs, "status")
|
|
473
488
|
// ④ 文档机检门(§3.20「结构兜底」②——门位 = 链门之后、状态行改写之前):值余核 =「已收口」时同门;
|
|
474
489
|
// REFUSED(K ≥ 1)∥ ERROR ⇒ throw(§1 状态行字节不变);其余值零挂(回执逐字旧形)。
|
|
475
490
|
const gate = closingValue ? runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" }) : null
|
|
476
491
|
if (gate && (gate.state === "refused" || gate.state === "error")) throw new Error(gateRefusalMessage(gate, "status"))
|
|
492
|
+
// 收口自动消费(§10——第二通路与 close 同效:位次 = 上两道门之后、状态行写盘之前)
|
|
493
|
+
const designTokens = closingValue ? consumeBatchDesignTokens(ctx?.agent ?? {}, abs) : null
|
|
477
494
|
const written = updateSectionStatusLine(src, seg, lineValue)
|
|
478
495
|
writeFileSync(abs, written)
|
|
479
496
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
@@ -483,15 +500,15 @@ export async function statusBatchRecord({ args, ctx, review, pickTarget, onWritt
|
|
|
483
500
|
// **一次收口只出一份清单**:触发 = `closingValue` ∧ 前态未冻结;`batch` 一次调用只分派一个 action
|
|
484
501
|
// (close ∥ status 不会同拍)⇒ 同一拍至多一次重锚、至多一份 `[ledger reanchor]`。前一合取项 =
|
|
485
502
|
// **defensive-only**:前态 closed 已被冻结门(`assertGateOpen`,本函数上方)早拒,正常路径跑不到这里。
|
|
486
|
-
const reanchor = closingValue && readBatchStatusLine(src) !== "closed" ? await reanchorOnClose(abs) : null
|
|
487
|
-
return `batch: §${seg} status line updated to ${JSON.stringify(lineValue)} — the freeze gate (§1) is a read-only domain for non-§1 writes (frozen truth stays = the §1 line).${gateReceiptLine(gate)}${reanchorReceiptLine(reanchor)}`
|
|
503
|
+
const reanchor = closingValue && readBatchStatusLine(src) !== "closed" ? await reanchorOnClose(abs, ctx?.agent?.cwd ?? process.cwd()) : null
|
|
504
|
+
return `batch: §${seg} status line updated to ${JSON.stringify(lineValue)} — the freeze gate (§1) is a read-only domain for non-§1 writes (frozen truth stays = the §1 line).${gateReceiptLine(gate)}${designTokensReceiptLine(designTokens)}${reanchorReceiptLine(reanchor)}`
|
|
488
505
|
}
|
|
489
506
|
|
|
490
507
|
/**
|
|
491
508
|
* close——收口冻结(§4.13,仅 depth-0)。§1 状态行 →「已收口 <YYYY-MM-DD>」;此后该档
|
|
492
509
|
* append/status 全拒(冻结判据唯一真值 = §1 行——§4.9 零语义变;唯一例外 = 重开合取 §4.18-B)。
|
|
493
510
|
* 对已收口档再 close ⇒ 拒(close 本身是对冻结档的写)。不代写 §6 内容、不替代台账核销事务。
|
|
494
|
-
* 前提规则(#422 +
|
|
511
|
+
* 前提规则(#422 + A 链门 §4.18-A + 本批单测档门 §3.5):① 链完整性门(收口前置——§2/§3/§4/§6 各段有内容 ∥ 声明
|
|
495
512
|
* 不适用;§6 唯一缺段时拒串 = 旧 §6 串逐字零回归);② 拒句含「失败 ⇒ 勿 close」(append/status 面
|
|
496
513
|
* ——见 batch.mjs 与上方 assertGateOpen);③ 无状态行档 ⇒ 自建机读位(updateSectionStatusLine
|
|
497
514
|
* 缺失即插——同路收口;「有行但不可解析」仍 fail-closed)。
|
|
@@ -507,33 +524,86 @@ export async function closeBatchRecord({ args, ctx, review, pickTarget, onWritte
|
|
|
507
524
|
// 链完整性门(§4.18-A——门位 = 既有拒因之后、文档机检门之前):§2/§3/§4/§6 各段有内容 ∥ 声明
|
|
508
525
|
// 不适用;缺段 ⇒ 拒并点名(§6 唯一缺段 = J2 旧串逐字)。含声明形不合判(J3——不静默当作无声明)。
|
|
509
526
|
assertCloseoutChainComplete(src)
|
|
527
|
+
// 本批批内单测档门(`TESTING.md` §3.5——门位 = 链门之后、文档机检门之前):本批自己的单元测试档
|
|
528
|
+
// 仍在盘 ⇒ 拒(零留存的结构兜底;`close` 与 `status` 写「已收口」两路同判;放行零回执)。
|
|
529
|
+
assertUnitTestFilesCleared(abs, "close")
|
|
510
530
|
// 文档机检门(§3.20「结构兜底」②——门位 = 链门之后、状态行改写之前):REFUSED(K ≥ 1)∥ ERROR
|
|
511
531
|
// ⇒ throw(fail-closed,不落「已收口」);PASS / 未跑(无声明面)⇒ 落盘 + 回执明示。
|
|
512
532
|
const gate = runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" })
|
|
513
533
|
if (gate.state === "refused" || gate.state === "error") throw new Error(gateRefusalMessage(gate, "close"))
|
|
534
|
+
// 收口自动消费本批设计凭证(§10——位次 = 全部拒因之后、写盘之前;失败 ⇒ 原样上抛、本函数拒)
|
|
535
|
+
const designTokens = consumeBatchDesignTokens(ctx?.agent ?? {}, abs)
|
|
514
536
|
const date = todayLocal()
|
|
515
537
|
const written = updateSectionStatusLine(src, 1, `已收口 ${date}`)
|
|
516
538
|
writeFileSync(abs, written)
|
|
517
539
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
518
540
|
// 收口重锚(§6.2 · C · AC-M2-30):顺序位 = 全部拒因通过、§1 状态行写盘**之后**(上两行)——close 被拒
|
|
519
541
|
//(身份 ∥ 冻结 ∥ 链完整性门 ∥ 文档机检门)⇒ 台账零写(被拒的 close 没有收口,「原批已收口」那时是假事实)。
|
|
520
|
-
const reanchor = await reanchorOnClose(abs)
|
|
521
|
-
return `batch: §1 status line → 「已收口 ${date}」 — the record is frozen: append is refused from now on, and status is refused except the reopen conjunction (§1 → 进行中 with a note) (整档冻结;改 = 新批新档; §6 内容与台账核销仍走既有通道).${gateReceiptLine(gate)}${reanchorReceiptLine(reanchor)}`
|
|
542
|
+
const reanchor = await reanchorOnClose(abs, ctx?.agent?.cwd ?? process.cwd())
|
|
543
|
+
return `batch: §1 status line → 「已收口 ${date}」 — the record is frozen: append is refused from now on, and status is refused except the reopen conjunction (§1 → 进行中 with a note) (整档冻结;改 = 新批新档; §6 内容与台账核销仍走既有通道).${gateReceiptLine(gate)}${designTokensReceiptLine(designTokens)}${reanchorReceiptLine(reanchor)}`
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ─── 收口自动消费本批设计凭证(DESIGN-TOKEN-SETTLEMENT §10——单源,close ∥ status 两通路同调)──
|
|
547
|
+
|
|
548
|
+
/** 「留槽无归属」枚数(§10 观测量):并集视图里在旁列**无归属行**的槽数——含过期项(清点面
|
|
549
|
+
* 照列;过期槽同样撑开写门,正是敞口本身)。只认旁列:注册表反查是显示面兜底、不是消费判据
|
|
550
|
+
* (旧槽不猜归属——KD-BG4)。 */
|
|
551
|
+
function unattributedSlotCount(agent, batches) {
|
|
552
|
+
let n = 0
|
|
553
|
+
for (const id of unionSlots(agent).keys()) if (!batches.has(id)) n++
|
|
554
|
+
return n
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 收口自动消费(DESIGN-TOKEN-SETTLEMENT §10——两通路共调本函数,不复制逻辑)。四步:
|
|
559
|
+
* ①非工程模式 ⇒ no-op(普通模式批无设计槽——槽只在工程模式评审 settle 时铸,`executeConsumeDesignAction` 同门);
|
|
560
|
+
* ②反查 = 内存 Map ∪ 槽文件旁列(`unionBatches`,**严格读** `{strict:true}`——旁列读不到即抛 ⇒ close 被拒,
|
|
561
|
+
* §10「读形」;「没有」= 空集,不抛)取该 `abs` 名下的 designId 集;
|
|
562
|
+
* ③空集 ⇒ no-op(重开后再 close 走这条——该批槽已被上一轮消费);
|
|
563
|
+
* ④调既有 `executeConsumeDesignAction({ designIds })`——单次落盘 + 内存整体回滚 + 逐枚回执全复用
|
|
564
|
+
* (KD-BG6:不新写落盘 / 回滚逻辑,否则同一份语义写第二遍、两处必漂)。
|
|
565
|
+
* **失败语义**:④抛 ⇒ **原样上抛**(消费核已把内存槽整体回滚)⇒ 调用方被拒、档零字节变、可重试——
|
|
566
|
+
* 「已收口而凭证仍在」正是本节要消掉的窗口,不把它降级成一句回执。
|
|
567
|
+
* @param {object} agent — 含 `config.agent.engineering` 与槽状态图
|
|
568
|
+
* @param {string} abs — 本批批档绝对路径(收口目标,`pickTarget` 解析所得)
|
|
569
|
+
* @returns {{consumed: number, unattributed: number}|null} 消费枚数 ∥ 留槽无归属枚数;`null` = no-op(非工程模式)
|
|
570
|
+
*/
|
|
571
|
+
export function consumeBatchDesignTokens(agent, abs) {
|
|
572
|
+
if (!agent?.config?.agent?.engineering) return null
|
|
573
|
+
const target = batchPathKey(abs)
|
|
574
|
+
const batches = unionBatches(agent, { strict: true })
|
|
575
|
+
const ids = [...batches]
|
|
576
|
+
.filter(([, b]) => typeof b === "string" && batchPathKey(b) === target)
|
|
577
|
+
.map(([id]) => id)
|
|
578
|
+
if (ids.length === 0) return { consumed: 0, unattributed: unattributedSlotCount(agent, batches) }
|
|
579
|
+
const held = unionSlots(agent) // 消费前态:与消费核同一并集视图(消费枚数 = 交集大小)
|
|
580
|
+
const consumed = ids.filter((id) => held.has(id)).length
|
|
581
|
+
executeConsumeDesignAction({ designIds: ids }, { agent })
|
|
582
|
+
return { consumed, unattributed: unattributedSlotCount(agent, batches) }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** 收口回执的凭证段(§10 回执形:单列 `[design tokens]` 一行——消费枚数 ∥ 留槽无归属枚数);
|
|
586
|
+
* 标签形与 `[ledger reanchor]` ∥ `[doc-check gate]` 一致(换行 + 方括号前缀)。`null`(普通模式
|
|
587
|
+
* no-op)⇒ 无该段——回执逐字旧形。 */
|
|
588
|
+
function designTokensReceiptLine(result) {
|
|
589
|
+
if (!result) return ""
|
|
590
|
+
return `\n[design tokens] 消费 ${result.consumed} 枚 · 留槽无归属 ${result.unattributed} 枚`
|
|
522
591
|
}
|
|
523
592
|
|
|
524
593
|
/** 收口重锚(设计档 §6.2 · C)——收口两条通路(`close` ∥ `status` 写 §1「已收口」)共调本函数(同一单源,
|
|
525
594
|
* 不复制逻辑);写入单源 = `ledgerReanchorBatch`(本档只供 `cwd` 基准与回执形;工具层零 SQL)。
|
|
526
|
-
* `cwd` = **批档所属项目根** = `resolveLedgerRoot(dirname(<batchDoc>))?.root`(§6.1 口径 2 同一子表达式)——
|
|
527
|
-
*
|
|
595
|
+
* `cwd` = **批档所属项目根** = `resolveLedgerRoot(dirname(<batchDoc>), boundary)?.root`(§6.1 口径 2 同一子表达式)——
|
|
596
|
+
* **不用会话锚**:会话锚为多根工作区时可能收到别的项目根 ⇒ 选行零命中 ⇒ 唯一的机器写静默失效。
|
|
597
|
+
* `boundary` = 上溯上界 = **会话锚**(设计档 §3.8「界(单源)」——台账面同一枚界;缺则落进程 cwd)。
|
|
528
598
|
* 无根 ⇒ 传 `null`(核心不取行、不重锚,`failed` 点名——不落兜底基准)。
|
|
529
599
|
* `batch` = 项目根相对路径(复启句的 `<批名>` 即其文件部分;绝对路径同样可解析,但句内会带整条绝对路径)。
|
|
530
600
|
* 失败**不抛**(台账忙 ∥ 无根均不否决收口——档面写与台账库分属两存储,不做分布式事务)。 */
|
|
531
|
-
async function reanchorOnClose(abs) {
|
|
601
|
+
async function reanchorOnClose(abs, boundary) {
|
|
532
602
|
try {
|
|
533
603
|
const { ledgerReanchorBatch, resolveLedgerRoot } = await import("../ledger.mjs") // 动态 import:ledger-db 静态引 node:sqlite(W8 契约②)
|
|
534
|
-
const root = resolveLedgerRoot(dirname(abs))?.root ?? null
|
|
604
|
+
const root = resolveLedgerRoot(dirname(abs), boundary)?.root ?? null
|
|
535
605
|
// 句内路径统一正斜杠(与 `task_book` 列值同形——`<批名>` 是给人读的事实句,不是平台路径)
|
|
536
|
-
return ledgerReanchorBatch({ cwd: root, batch: root ? relative(root, abs).replace(/\\/g, "/") : abs })
|
|
606
|
+
return ledgerReanchorBatch({ cwd: root, batch: root ? relative(root, abs).replace(/\\/g, "/") : abs, boundary })
|
|
537
607
|
} catch (e) {
|
|
538
608
|
// 非预期异常(模块加载失败 ∥ 根推导内读声明抛)同样不得否决收口——降为同一返回形(台账零写)
|
|
539
609
|
return { batch: abs, reanchored: [], alreadyAnchored: [], failed: String(e?.message ?? e) }
|