@thincoder/core 0.10.11 → 0.10.12
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 +8 -0
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent-tools/batch-gate.mjs +65 -49
- package/agent-tools/batch-lifecycle.mjs +66 -9
- package/agent-tools/batch-paths.mjs +10 -8
- 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/ledger-cmd.mjs +39 -37
- 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 +11 -13
- 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 +21 -11
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/session-slot-write.mjs +21 -1
- package/session-slots-manifest.mjs +21 -4
- package/token-ttl.mjs +68 -4
- package/tool-docs/wait_for.md +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.12] — 2026-10-26
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **台账在无根目录从「静默空集」改为「抛明」**:项目根推导不出来时,`query` / `count` / `add` / `update` / `close` 一律抛错并说明「自哪个路径、向上找到哪里为止」,不再返回空集。空集与「记到别的项目」在回执上分不开,静默空集等于让人以为没有台账。
|
|
11
|
+
- **多根工作区下建批档:报明项目并给路径,不再落锚目录**:工作目录本身没有项目、直接子目录里有项目(1 个同判、多个同判),此时建批档会列全下辖项目并给显式路径指引;目标属于某个项目时照常落该项目的声明基底。
|
|
12
|
+
- **项目根解析:多根工作区一律「≥1 同判」**:取消此前「恰一个子项目」与「多个」的区别——工作目录下直接子目录带项目清单,不论 1 个还是多个,都是多根工作区,逻辑一致。
|
|
13
|
+
|
|
6
14
|
## [0.10.11] — 2026-10-25
|
|
7
15
|
|
|
8
16
|
### 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) {
|
|
@@ -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
|
}
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* 收口重锚(§6.2 · C · AC-M2-30):收口有两条通路——`close` ∥ `status` 写 §1「已收口」——两路都冻结档、
|
|
29
29
|
* 都让批「收口了」,故**两路同调**单源 `reanchorOnClose`(本档下方),调点均在状态行写盘之后。
|
|
30
30
|
* 一次收口只出一份 `[ledger reanchor]` 回执段(判据 = 前态未冻结:已冻结档在两路都被冻结门早拒)。
|
|
31
|
+
*
|
|
32
|
+
* 收口自动消费本批设计凭证(DESIGN-TOKEN-SETTLEMENT §10 · 2026-10-26):同一两路在**写状态行之前**
|
|
33
|
+
* 调 `consumeBatchDesignTokens`(判据 = 槽的批次归属旁列;口径单源见该档 §10)。消费失败 ⇒ 原样上抛、
|
|
34
|
+
* close 被拒、档零字节变——位次与重锚(写盘之后)相反,这是机制本体(KD-BG3)。
|
|
31
35
|
*/
|
|
32
36
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
33
37
|
import { dirname, relative, resolve } from "node:path"
|
|
@@ -39,6 +43,7 @@ import {
|
|
|
39
43
|
} from "./batch-skeleton.mjs"
|
|
40
44
|
import { resolveBatchCreatePath } from "./batch-paths.mjs"
|
|
41
45
|
import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
|
|
46
|
+
import { batchPathKey, executeConsumeDesignAction, unionBatches, unionSlots } from "./design-slots.mjs"
|
|
42
47
|
|
|
43
48
|
/** 可读文件判据(存在且为文件——目录/缺失同判不可读;与主档同名 helper 同型——KD-4 单向
|
|
44
49
|
* 依赖下各自持有,三行谓词不构成第二权威源)。 */
|
|
@@ -474,6 +479,8 @@ export async function statusBatchRecord({ args, ctx, review, pickTarget, onWritt
|
|
|
474
479
|
// REFUSED(K ≥ 1)∥ ERROR ⇒ throw(§1 状态行字节不变);其余值零挂(回执逐字旧形)。
|
|
475
480
|
const gate = closingValue ? runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" }) : null
|
|
476
481
|
if (gate && (gate.state === "refused" || gate.state === "error")) throw new Error(gateRefusalMessage(gate, "status"))
|
|
482
|
+
// 收口自动消费(§10——第二通路与 close 同效:位次 = 上两道门之后、状态行写盘之前)
|
|
483
|
+
const designTokens = closingValue ? consumeBatchDesignTokens(ctx?.agent ?? {}, abs) : null
|
|
477
484
|
const written = updateSectionStatusLine(src, seg, lineValue)
|
|
478
485
|
writeFileSync(abs, written)
|
|
479
486
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
@@ -483,8 +490,8 @@ export async function statusBatchRecord({ args, ctx, review, pickTarget, onWritt
|
|
|
483
490
|
// **一次收口只出一份清单**:触发 = `closingValue` ∧ 前态未冻结;`batch` 一次调用只分派一个 action
|
|
484
491
|
// (close ∥ status 不会同拍)⇒ 同一拍至多一次重锚、至多一份 `[ledger reanchor]`。前一合取项 =
|
|
485
492
|
// **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)}`
|
|
493
|
+
const reanchor = closingValue && readBatchStatusLine(src) !== "closed" ? await reanchorOnClose(abs, ctx?.agent?.cwd ?? process.cwd()) : null
|
|
494
|
+
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
495
|
}
|
|
489
496
|
|
|
490
497
|
/**
|
|
@@ -511,29 +518,79 @@ export async function closeBatchRecord({ args, ctx, review, pickTarget, onWritte
|
|
|
511
518
|
// ⇒ throw(fail-closed,不落「已收口」);PASS / 未跑(无声明面)⇒ 落盘 + 回执明示。
|
|
512
519
|
const gate = runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" })
|
|
513
520
|
if (gate.state === "refused" || gate.state === "error") throw new Error(gateRefusalMessage(gate, "close"))
|
|
521
|
+
// 收口自动消费本批设计凭证(§10——位次 = 全部拒因之后、写盘之前;失败 ⇒ 原样上抛、本函数拒)
|
|
522
|
+
const designTokens = consumeBatchDesignTokens(ctx?.agent ?? {}, abs)
|
|
514
523
|
const date = todayLocal()
|
|
515
524
|
const written = updateSectionStatusLine(src, 1, `已收口 ${date}`)
|
|
516
525
|
writeFileSync(abs, written)
|
|
517
526
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
518
527
|
// 收口重锚(§6.2 · C · AC-M2-30):顺序位 = 全部拒因通过、§1 状态行写盘**之后**(上两行)——close 被拒
|
|
519
528
|
//(身份 ∥ 冻结 ∥ 链完整性门 ∥ 文档机检门)⇒ 台账零写(被拒的 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)}`
|
|
529
|
+
const reanchor = await reanchorOnClose(abs, ctx?.agent?.cwd ?? process.cwd())
|
|
530
|
+
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)}`
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ─── 收口自动消费本批设计凭证(DESIGN-TOKEN-SETTLEMENT §10——单源,close ∥ status 两通路同调)──
|
|
534
|
+
|
|
535
|
+
/** 「留槽无归属」枚数(§10 观测量):并集视图里在旁列**无归属行**的槽数——含过期项(清点面
|
|
536
|
+
* 照列;过期槽同样撑开写门,正是敞口本身)。只认旁列:注册表反查是显示面兜底、不是消费判据
|
|
537
|
+
* (旧槽不猜归属——KD-BG4)。 */
|
|
538
|
+
function unattributedSlotCount(agent, batches) {
|
|
539
|
+
let n = 0
|
|
540
|
+
for (const id of unionSlots(agent).keys()) if (!batches.has(id)) n++
|
|
541
|
+
return n
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* 收口自动消费(DESIGN-TOKEN-SETTLEMENT §10——两通路共调本函数,不复制逻辑)。四步:
|
|
546
|
+
* ①非工程模式 ⇒ no-op(普通模式批无设计槽——槽只在工程模式评审 settle 时铸,`executeConsumeDesignAction` 同门);
|
|
547
|
+
* ②反查 = 内存 Map ∪ 槽文件旁列(`unionBatches`,**严格读** `{strict:true}`——旁列读不到即抛 ⇒ close 被拒,
|
|
548
|
+
* §10「读形」;「没有」= 空集,不抛)取该 `abs` 名下的 designId 集;
|
|
549
|
+
* ③空集 ⇒ no-op(重开后再 close 走这条——该批槽已被上一轮消费);
|
|
550
|
+
* ④调既有 `executeConsumeDesignAction({ designIds })`——单次落盘 + 内存整体回滚 + 逐枚回执全复用
|
|
551
|
+
* (KD-BG6:不新写落盘 / 回滚逻辑,否则同一份语义写第二遍、两处必漂)。
|
|
552
|
+
* **失败语义**:④抛 ⇒ **原样上抛**(消费核已把内存槽整体回滚)⇒ 调用方被拒、档零字节变、可重试——
|
|
553
|
+
* 「已收口而凭证仍在」正是本节要消掉的窗口,不把它降级成一句回执。
|
|
554
|
+
* @param {object} agent — 含 `config.agent.engineering` 与槽状态图
|
|
555
|
+
* @param {string} abs — 本批批档绝对路径(收口目标,`pickTarget` 解析所得)
|
|
556
|
+
* @returns {{consumed: number, unattributed: number}|null} 消费枚数 ∥ 留槽无归属枚数;`null` = no-op(非工程模式)
|
|
557
|
+
*/
|
|
558
|
+
export function consumeBatchDesignTokens(agent, abs) {
|
|
559
|
+
if (!agent?.config?.agent?.engineering) return null
|
|
560
|
+
const target = batchPathKey(abs)
|
|
561
|
+
const batches = unionBatches(agent, { strict: true })
|
|
562
|
+
const ids = [...batches]
|
|
563
|
+
.filter(([, b]) => typeof b === "string" && batchPathKey(b) === target)
|
|
564
|
+
.map(([id]) => id)
|
|
565
|
+
if (ids.length === 0) return { consumed: 0, unattributed: unattributedSlotCount(agent, batches) }
|
|
566
|
+
const held = unionSlots(agent) // 消费前态:与消费核同一并集视图(消费枚数 = 交集大小)
|
|
567
|
+
const consumed = ids.filter((id) => held.has(id)).length
|
|
568
|
+
executeConsumeDesignAction({ designIds: ids }, { agent })
|
|
569
|
+
return { consumed, unattributed: unattributedSlotCount(agent, batches) }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** 收口回执的凭证段(§10 回执形:单列 `[design tokens]` 一行——消费枚数 ∥ 留槽无归属枚数);
|
|
573
|
+
* 标签形与 `[ledger reanchor]` ∥ `[doc-check gate]` 一致(换行 + 方括号前缀)。`null`(普通模式
|
|
574
|
+
* no-op)⇒ 无该段——回执逐字旧形。 */
|
|
575
|
+
function designTokensReceiptLine(result) {
|
|
576
|
+
if (!result) return ""
|
|
577
|
+
return `\n[design tokens] 消费 ${result.consumed} 枚 · 留槽无归属 ${result.unattributed} 枚`
|
|
522
578
|
}
|
|
523
579
|
|
|
524
580
|
/** 收口重锚(设计档 §6.2 · C)——收口两条通路(`close` ∥ `status` 写 §1「已收口」)共调本函数(同一单源,
|
|
525
581
|
* 不复制逻辑);写入单源 = `ledgerReanchorBatch`(本档只供 `cwd` 基准与回执形;工具层零 SQL)。
|
|
526
|
-
* `cwd` = **批档所属项目根** = `resolveLedgerRoot(dirname(<batchDoc>))?.root`(§6.1 口径 2 同一子表达式)——
|
|
527
|
-
*
|
|
582
|
+
* `cwd` = **批档所属项目根** = `resolveLedgerRoot(dirname(<batchDoc>), boundary)?.root`(§6.1 口径 2 同一子表达式)——
|
|
583
|
+
* **不用会话锚**:会话锚为多根工作区时可能收到别的项目根 ⇒ 选行零命中 ⇒ 唯一的机器写静默失效。
|
|
584
|
+
* `boundary` = 上溯上界 = **会话锚**(设计档 §3.8「界(单源)」——台账面同一枚界;缺则落进程 cwd)。
|
|
528
585
|
* 无根 ⇒ 传 `null`(核心不取行、不重锚,`failed` 点名——不落兜底基准)。
|
|
529
586
|
* `batch` = 项目根相对路径(复启句的 `<批名>` 即其文件部分;绝对路径同样可解析,但句内会带整条绝对路径)。
|
|
530
587
|
* 失败**不抛**(台账忙 ∥ 无根均不否决收口——档面写与台账库分属两存储,不做分布式事务)。 */
|
|
531
|
-
async function reanchorOnClose(abs) {
|
|
588
|
+
async function reanchorOnClose(abs, boundary) {
|
|
532
589
|
try {
|
|
533
590
|
const { ledgerReanchorBatch, resolveLedgerRoot } = await import("../ledger.mjs") // 动态 import:ledger-db 静态引 node:sqlite(W8 契约②)
|
|
534
|
-
const root = resolveLedgerRoot(dirname(abs))?.root ?? null
|
|
591
|
+
const root = resolveLedgerRoot(dirname(abs), boundary)?.root ?? null
|
|
535
592
|
// 句内路径统一正斜杠(与 `task_book` 列值同形——`<批名>` 是给人读的事实句,不是平台路径)
|
|
536
|
-
return ledgerReanchorBatch({ cwd: root, batch: root ? relative(root, abs).replace(/\\/g, "/") : abs })
|
|
593
|
+
return ledgerReanchorBatch({ cwd: root, batch: root ? relative(root, abs).replace(/\\/g, "/") : abs, boundary })
|
|
537
594
|
} catch (e) {
|
|
538
595
|
// 非预期异常(模块加载失败 ∥ 根推导内读声明抛)同样不得否决收口——降为同一返回形(台账零写)
|
|
539
596
|
return { batch: abs, reanchored: [], alreadyAnchored: [], failed: String(e?.message ?? e) }
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 契约(§4.15 逐字):**候选序** = cwd → 项目根 → 逐基底(基底 = `batchDocBases` 声明面单源);
|
|
8
8
|
* **判据分野** = 读面取首个**可读**文件 / create 取首个**落基底根内**者;**锚定防嵌套** = 串以任一基底的
|
|
9
9
|
* 「项目根相对前缀」打头且前缀后为**路径段边界** ⇒ 只解析项目根形(不二次拼接;不落基底内 ⇒ fail-closed);
|
|
10
|
-
* **工作区锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥
|
|
10
|
+
* **工作区锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属**报明 + 可执行下一步**(下辖项目全列 + 显式路径指引);读面候选分支追加;
|
|
11
11
|
* 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
|
|
12
12
|
* 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
|
|
13
13
|
* **基底自有项目根分支 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对分支限单段(多段串不二次拼接)。
|
|
@@ -35,8 +35,9 @@ function samePath(a, b) {
|
|
|
35
35
|
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** 「项目根」取根单源(§4.15 条 1
|
|
39
|
-
*
|
|
38
|
+
/** 「项目根」取根单源(§4.15 条 1——**归属形 + 起点兜底**:判据单源 = `resolveProjectRoot` → `owningProject`;
|
|
39
|
+
* 无项目 ⇒ `resolve(cwd ?? ".")` 兜底且**单值**):同式 = 台账库键 / 写门指针基准的同一子表达式
|
|
40
|
+
* (`design/LEDGER.md` §6.1 口径 2)。 */
|
|
40
41
|
export function batchProjectRoot(cwd) {
|
|
41
42
|
return resolveProjectRoot(cwd) ?? resolve(cwd ?? ".")
|
|
42
43
|
}
|
|
@@ -97,8 +98,9 @@ function insideBases(abs, bases) {
|
|
|
97
98
|
const outsideBasesError = (raw) => new Error(`batch: create path resolves outside the batch-record base roots — a batch record must live under the declared docRoot.batches base (fail-closed). Path: ${raw}`)
|
|
98
99
|
/** create 锚定但落基底外(§4.15 新文案逐字——不二次拼接)。 */
|
|
99
100
|
const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored at a batch base root but does not resolve under the declared docRoot.batches root(s) — refusing to nest it (fail-closed). Path: ${raw}`)
|
|
100
|
-
/** 工作区锚 create
|
|
101
|
-
|
|
101
|
+
/** 工作区锚 create 报明面(**文案逐字** = `BATCH-RECORD.md` §4.15 条 5——机检锚 `belongs to no project`
|
|
102
|
+
* ∧ `Pass an explicit path under a project`;下辖项目按名排序绝对路径全列,单句对 1 ∥ ≥2 皆真)。 */
|
|
103
|
+
const workspaceCreateError = (raw, candidates) => new Error(`batch: create target "${raw}" belongs to no project — the session anchor holds ${candidates.length} project(s) below it: ${candidates.join(", ")}. Pass an explicit path under a project (e.g. "${candidates[0]}/docs/batches/<file>.md").`)
|
|
102
104
|
const ambiguousLandingError = (raw, landings) => new Error(`batch: create path resolves under more than one declared batch-record base — refusing to pick one (fail-closed); pass an explicit path (absolute preferred). Path: ${raw}. Candidates: ${landings.join(", ")}`)
|
|
103
105
|
|
|
104
106
|
/** 工作区锚读面候选分支(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
|
|
@@ -153,8 +155,8 @@ export function resolveBatchDocPath(cwd, given) {
|
|
|
153
155
|
/**
|
|
154
156
|
* **create 面**解析(§4.15 条 2/3/6):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
|
|
155
157
|
* 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
|
|
156
|
-
* **工作区锚**(条 5):目标所属项目(`owningProject
|
|
157
|
-
* 越出 ⇒ 既有 fail-closed);无所属 ⇒
|
|
158
|
+
* **工作区锚**(条 5):目标所属项目(`owningProject`——界 = 会话锚)∈ 锚候选集 ⇒ 基底 = 该项目声明面(落其基底内;
|
|
159
|
+
* 越出 ⇒ 既有 fail-closed);无所属 ⇒ **报明 + 可执行下一步**(列候选 + 显式路径指引——不静默落锚)。
|
|
158
160
|
* @param {string} cwd 会话锚
|
|
159
161
|
* @param {string} given create `path` 原值
|
|
160
162
|
* @param {string[]} bases 已解析的基底根数组(`batchDocBases` 单源;调用方传入以免重复读档)
|
|
@@ -167,7 +169,7 @@ export function resolveBatchCreatePath(cwd, given, bases) {
|
|
|
167
169
|
let roots = bases
|
|
168
170
|
const ws = workspaceCandidates(base)
|
|
169
171
|
if (ws) {
|
|
170
|
-
const owner = owningProject(isAbsolute(raw) ? resolve(raw) : resolve(base, p))
|
|
172
|
+
const owner = owningProject(isAbsolute(raw) ? resolve(raw) : resolve(base, p), base)
|
|
171
173
|
if (!owner || !ws.some((c) => samePath(c, owner))) throw workspaceCreateError(raw, ws)
|
|
172
174
|
roots = batchDocBases(owner)
|
|
173
175
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* design-slots.mjs — design-token 槽位清点 + 消费核(2026-10-07 批 ledger-tool · 台账 #927 并入)。
|
|
3
|
-
* 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §9(F-SL1 清点面 / F-SL2
|
|
3
|
+
* 设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §9(F-SL1 清点面 / F-SL2 批量消费)∥
|
|
4
|
+
* §10(批归属派生 · 收口反查并集——`batchAbsPath` / `batchPathKey` / `unionBatches` 三面)。
|
|
4
5
|
*
|
|
5
6
|
* 面一 `executeDesignSlotsAction`(`subagent` action `design-slots`——只读):
|
|
6
7
|
* 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
|
|
7
8
|
* 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
|
|
8
9
|
* (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批 ∥ designToken(末位,恒携);龄序 = 最老在前)
|
|
9
|
-
* + 计数尾行;「批」=
|
|
10
|
-
*
|
|
10
|
+
* + 计数尾行;「批」= 批次归属旁列(§10——settle 时按 `docSetKey` 的 `batches/` 段档落盘的绝对路径)
|
|
11
|
+
* 取 basename,旁列缺失 ⇒ `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档兜底
|
|
12
|
+
* (KD-SL2);两者皆不可得 ⇒ `—`。
|
|
11
13
|
* `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);缺省 = 现行清点面逐字零变;零写同前。
|
|
12
14
|
* 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
|
|
13
15
|
* `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
|
|
@@ -19,15 +21,19 @@
|
|
|
19
21
|
* 签发链行为零改(mint / settle / TTL / persist 存储形——#868 面另论);工程模式父侧(depth-0)限定
|
|
20
22
|
* (门 = 本档两执行器;depth 面由装配与受限变体门承载)。
|
|
21
23
|
*/
|
|
22
|
-
import {
|
|
24
|
+
import { resolve } from "node:path"
|
|
25
|
+
|
|
26
|
+
import { persistEngTokens, readEngTokensFromSlot, readEngTokenBatchesFromSlot, reconcileEngTokensFromSlot, removeDesignTokenSlot, tokenExpired, tokenExpiryMs } from "../token-ttl.mjs"
|
|
27
|
+
// 本档 ⇄ `design-token.mjs` 为**静态环**(批归属派生 ⇄ TTL 读取):两侧绑定只在函数体内运行时使用,
|
|
28
|
+
// 任一侧**顶层**不得调用对方绑定(函数声明已提升,但 const 会落 TDZ)——与 token-ttl ⇄ session 同构。
|
|
23
29
|
import { effectiveTokenTtlMs } from "./design-token.mjs"
|
|
24
30
|
import { advisorRuns } from "./advisor-async.mjs"
|
|
25
31
|
|
|
26
32
|
const DAY_MS = 86400000
|
|
27
33
|
|
|
28
34
|
/** 槽并集视图(F-SL1 数据源 / F-SL2 目标集解析):内存 Map 先行、槽文件权威表覆盖(同 id 冲突以槽
|
|
29
|
-
* 为准——D-S3
|
|
30
|
-
function unionSlots(agent) {
|
|
35
|
+
* 为准——D-S3 同源);纯读——零回写、零落盘。收口自动消费(§10)同源取用(消费计数的前态集)。 */
|
|
36
|
+
export function unionSlots(agent) {
|
|
31
37
|
const merged = new Map()
|
|
32
38
|
const mem = agent?._engDesignTokens
|
|
33
39
|
if (mem instanceof Map) for (const [id, t] of mem) merged.set(id, t)
|
|
@@ -36,10 +42,25 @@ function unionSlots(agent) {
|
|
|
36
42
|
return merged
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
45
|
+
/** 批次归属并集视图(§10 收口反查单源):内存 Map 先行、槽文件旁列覆盖(同 `unionSlots` 形——
|
|
46
|
+
* 槽 = 权威,D-S3 同源);纯读。返回 designId → 批档绝对路径;`strict` 透传读形(§10「读形」)——清点面缺省宽容(读不到 ⇒ 空集),收口面 `{strict:true}` ⇒ 读不到即抛(close 被拒)。 */
|
|
47
|
+
export function unionBatches(agent, { strict = false } = {}) {
|
|
48
|
+
const merged = new Map()
|
|
49
|
+
const mem = agent?._engDesignTokenBatches
|
|
50
|
+
if (mem instanceof Map) for (const [id, b] of mem) merged.set(id, b)
|
|
51
|
+
const file = readEngTokenBatchesFromSlot(agent, { strict })
|
|
52
|
+
if (file) for (const [id, b] of Object.entries(file)) merged.set(id, b)
|
|
53
|
+
return merged
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 「批」列派生(KD-SL2):批次归属旁列(§10)优先——取该绝对路径文件的 basename;旁列缺失
|
|
57
|
+
* (旁列启用前签发的旧槽)⇒ 会话内评审实例注册表 `_advisorRuns` 按 designId 命中的**最新** run
|
|
58
|
+
* ⇒ `docSetKey`(被审文档绝对路径集 JSON)中 `batches/` 段档取 basename;皆不可得(跨重启 /
|
|
41
59
|
* 模式重进 / 注册表回收)⇒ `—`(如实降级)。 */
|
|
42
|
-
function batchOf(agent, designId) {
|
|
60
|
+
function batchOf(agent, designId, batches) {
|
|
61
|
+
const fromSide = batches instanceof Map ? batches.get(designId) : undefined
|
|
62
|
+
const sideName = typeof fromSide === "string" ? basenameOfPath(fromSide) : null
|
|
63
|
+
if (sideName) return sideName
|
|
43
64
|
const runs = advisorRuns(agent)
|
|
44
65
|
if (!(runs instanceof Map)) return "—"
|
|
45
66
|
for (const run of [...runs.values()].reverse()) {
|
|
@@ -50,6 +71,12 @@ function batchOf(agent, designId) {
|
|
|
50
71
|
return "—"
|
|
51
72
|
}
|
|
52
73
|
|
|
74
|
+
/** 绝对路径 → 末段名(分隔符无关:`/` 与 `\` 都切——旁列值可来自任一端写法);空串 ⇒ null。 */
|
|
75
|
+
function basenameOfPath(p) {
|
|
76
|
+
const parts = String(p).split(/[\\/]/).filter(Boolean)
|
|
77
|
+
return parts.length ? parts[parts.length - 1] : null
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/** docSetKey(JSON 串)→ `batches/` 段档 basename(首命中);不可解析 / 无命中 ⇒ null。 */
|
|
54
81
|
function batchBasename(docSetKey) {
|
|
55
82
|
let list = null
|
|
@@ -64,17 +91,49 @@ function batchBasename(docSetKey) {
|
|
|
64
91
|
return null
|
|
65
92
|
}
|
|
66
93
|
|
|
94
|
+
/** docSetKey(JSON 串)→ `batches/` 段档的**绝对路径**(与 `batchBasename` 同一 `lastIndexOf("batches")`
|
|
95
|
+
* 判据);不可解析 / 无命中 ⇒ null。settle 时刻的批归属派生单源(§10「批次标识的来源」——
|
|
96
|
+
* 零新输入:判据与清点面同源)。`resolve` = 归一(分隔符 ∥ `.`/`..`);落盘值即此形。
|
|
97
|
+
* **归属不唯一 ⇒ 不猜**:文档集里出现**多份不同的**批档(收口消费以批为键,猜错 = 消费别批的
|
|
98
|
+
* 授权)⇒ 返回 null(归属 = 无,与「不含批档」同一降级层——§10 同段「比留下一个可解释的
|
|
99
|
+
* 滞留槽更坏」的排序)。同一档的两种写法(分隔符 / 大小写)去重,不算两份。 */
|
|
100
|
+
export function batchAbsPath(docSetKey) {
|
|
101
|
+
if (typeof docSetKey !== "string") return null
|
|
102
|
+
let list = null
|
|
103
|
+
try { list = JSON.parse(docSetKey) } catch { return null }
|
|
104
|
+
if (!Array.isArray(list)) return null
|
|
105
|
+
const hits = []
|
|
106
|
+
for (const p of list) {
|
|
107
|
+
if (typeof p !== "string") continue
|
|
108
|
+
const parts = p.split(/[\\/]/)
|
|
109
|
+
const i = parts.lastIndexOf("batches")
|
|
110
|
+
if (i < 0 || i >= parts.length - 1 || !parts[i + 1]) continue
|
|
111
|
+
const abs = resolve(p)
|
|
112
|
+
if (!hits.some((h) => batchPathKey(h) === batchPathKey(abs))) hits.push(abs)
|
|
113
|
+
}
|
|
114
|
+
return hits.length === 1 ? hits[0] : null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 批档身份键(§10「匹配」):两侧(旁列值 ∥ 收口目标 `abs`)都是 `node:path` 解析后的绝对路径;
|
|
118
|
+
* 比较前分隔符归一、`win32` 下不区分大小写(宿主事实)。出处:`docSetKey` 的 `normAbs` 对绝对
|
|
119
|
+
* 输入原样保留、不归一分隔符,而 `documents` 同时受理项目根相对形与绝对形 ⇒ 同一批档在
|
|
120
|
+
* `docSetKey` 里可带正斜杠或反斜杠。 */
|
|
121
|
+
export function batchPathKey(p) {
|
|
122
|
+
const s = resolve(String(p)).replace(/[\\/]+/g, "/")
|
|
123
|
+
return process.platform === "win32" ? s.toLowerCase() : s
|
|
124
|
+
}
|
|
125
|
+
|
|
67
126
|
/** 槽事实(清点列与选择器共用):状态 = `tokenExpired` 单源;`ageDays` = now − (expiresAt −
|
|
68
127
|
* `effectiveTokenTtlMs`) 派生(TTL 配置曾改时为近似——如实注);`expiresAt` 精确;畸形 / 格式外
|
|
69
128
|
* token ⇒ 两项 `—`(不可判龄)。 */
|
|
70
|
-
function slotFacts(agent, designId, token, now, ttlMs) {
|
|
129
|
+
function slotFacts(agent, designId, token, now, ttlMs, batches) {
|
|
71
130
|
const expiry = typeof token === "string" ? tokenExpiryMs(token) : null
|
|
72
131
|
return {
|
|
73
132
|
designId, token,
|
|
74
133
|
expired: typeof token === "string" && tokenExpired(token, now),
|
|
75
134
|
ageDays: expiry === null ? null : (now - (expiry - ttlMs)) / DAY_MS,
|
|
76
135
|
expiresAt: expiry,
|
|
77
|
-
batch: batchOf(agent, designId),
|
|
136
|
+
batch: batchOf(agent, designId, batches),
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
139
|
|
|
@@ -95,7 +154,8 @@ export function executeDesignSlotsAction(args, ctx) {
|
|
|
95
154
|
}
|
|
96
155
|
const now = Date.now()
|
|
97
156
|
const ttlMs = effectiveTokenTtlMs(parent)
|
|
98
|
-
const
|
|
157
|
+
const batches = unionBatches(parent) // 一次读入(逐行各读一次 = N 次文件解析)
|
|
158
|
+
const entries = [...unionSlots(parent)].map(([id, t]) => slotFacts(parent, id, t, now, ttlMs, batches))
|
|
99
159
|
entries.sort(byOldest)
|
|
100
160
|
// 参数面(fail-closed——不猜):`designId` trim 后精确匹配、空串视同缺省;未知 ⇒ 拒 + 列在场 id。
|
|
101
161
|
const rawId = args?.designId
|