@qilitt-mickey/vue3-temp-skill 1.1.69 → 1.1.70

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/SKILL.md CHANGED
@@ -32,7 +32,7 @@ tags: [vue3, typescript, element-plus, pinia, vite, unocss, crud, demo, code-qua
32
32
 
33
33
  1. **项目探查**:读取目标项目的 `package.json`、路由、页面、`Re*` 组件、Hooks、API 和样式;以源码事实为准。
34
34
  2. **能力路由**:运行 `scripts/match.py "需求" --json`,**仅**按输出中的 `load_files` 阅读 reference;禁止自行展开全部 `references/`。无 Python 时按下方能力组表手动匹配。
35
- 3. **质量闸门**:代码完成后读取 `references/code-quality.md`,执行清单和项目命令。设计变更集落地后追加**设计层闭环审计**:`node scripts/design-audit.mjs --project <落地项目> --container-width <内容区实际宽>`(ERROR 必须清零;无落地项目时自动跳过)。
35
+ 3. **质量闸门**:代码完成后读取 `references/code-quality.md`,执行清单和项目命令。设计变更集落地后追加**设计层闭环审计**:`node scripts/design-audit.mjs --project <落地项目> --container-width <内容区实际宽>`(ERROR 必须清零;无落地项目时自动跳过)。**会话结束前必须跑通终态凭证闸**:`node scripts/apply-final-gate.mjs --project <落地项目> --container-width <内容区实际宽>`(串联 changeset-selfcheck → parser match → parser audit → design-audit 四闸,`FINAL-GATE: PASS` 才算交付完成,凭证写入 `.codebuddy/design/ledger.json`)。
36
36
 
37
37
  ## 能力组路由
38
38
 
@@ -51,7 +51,7 @@ tags: [vue3, typescript, element-plus, pinia, vite, unocss, crud, demo, code-qua
51
51
  | `files-and-editors` | 附件、预览、下载、富文本、二维码 | `file-management.md`、`download-export.md`、`rich-text.md`、`qrcode-barcode.md` |
52
52
  | `realtime-and-mobile` | WebSocket、移动端、微信、验证码、动效 | 对应专项 reference |
53
53
  | `data-compare` | 编辑页变更对比和汇总 | `data-compare.md` |
54
- | `design-apply` | 设计变更集落地:语义 ID 三级匹配、结构/形态/两层样式转换、锚点与结构施工、覆写提权、渲染态验收、合规闸门。**主入口 `design-apply.md`(红线+契约+步骤索引),各步骤明细见 `references/steps/`——做到哪一步读哪一份** | `design-apply.md`、`steps/`、`mapping/`、`parser/`、`scripts/design-audit.mjs` |
54
+ | `design-apply` | 设计变更集落地:语义 ID 三级匹配、结构/形态/两层样式转换、锚点与结构施工、覆写提权、渲染态验收、合规闸门。**主入口 `design-apply.md`(红线+契约+步骤索引),各步骤明细见 `references/steps/`——做到哪一步读哪一份** | `design-apply.md`、`steps/`、`mapping/`、`parser/`、`scripts/design-audit.mjs`、`scripts/apply-final-gate.mjs` |
55
55
 
56
56
  ## 工作流
57
57
 
package/bin/cli.js CHANGED
@@ -31,10 +31,11 @@ const SKILL_DIR = path.resolve(__dirname, "..");
31
31
  const SKILL_NAME = "vue3-temp-skill";
32
32
  const TRACK_FILE = path.join(HOME, ".vue3-temp-skill.json");
33
33
  // 只复制 AI 运行时真正需要的文件;validate/cli 属作者侧词典维护工具,不随技能分发。
34
- // design-audit.mjs / check.mjs 必须随包分发:E11 交付完整性 / E12 设计层累积性(ledger 台账)
35
- // 是落地会话的运行时闸——缺了它们「只挑主题令牌落地 + 覆盖历史成果」无法被静态拦截
34
+ // design-audit.mjs / check.mjs / apply-final-gate.mjs 必须随包分发:E11 交付完整性 / E12 设计层
35
+ // 累积性(ledger 台账)/ 终态机读凭证(四闸串联,FINAL-GATE: PASS)是落地会话的运行时闸——
36
+ // 缺了它们「只挑主题令牌落地 + 覆盖历史成果 + 以自述结束会话」无法被静态拦截
36
37
  // (实证:2026-09-24 孔雀绿轮,运行时副本无审计闸,残缺口变更集一路绿灯)。
37
- const RUNTIME_SCRIPT_FILES = ["match.py", "modules.json", "design-audit.mjs", "check.mjs"];
38
+ const RUNTIME_SCRIPT_FILES = ["match.py", "modules.json", "design-audit.mjs", "check.mjs", "apply-final-gate.mjs"];
38
39
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(SKILL_DIR, "package.json"), "utf-8")).version;
39
40
 
40
41
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qilitt-mickey/vue3-temp-skill",
3
- "version": "1.1.69",
3
+ "version": "1.1.70",
4
4
  "type": "module",
5
5
  "description": "Vue 3 企业级中后台项目开发规范技能包 — core-kernel 架构、按需功能模块",
6
6
  "bin": {
@@ -31,6 +31,7 @@
31
31
  "prepack": "node scripts/check.mjs",
32
32
  "validate": "node scripts/validate.mjs",
33
33
  "audit": "node scripts/design-audit.mjs",
34
+ "final-gate": "node scripts/apply-final-gate.mjs",
34
35
  "match": "python scripts/match.py",
35
36
  "match:batch": "python scripts/match.py --batch",
36
37
  "mapping:build": "node parser/index.js build",
package/parser/index.js CHANGED
@@ -569,7 +569,8 @@ function checkBannedWords(changeset) {
569
569
 
570
570
  for (const [i, c] of (changeset.changes || []).entries()) {
571
571
  const label = `changes[${i}]`;
572
- if (!c.id || !ID_RE.test(c.id))
572
+ // global 为设计侧伪点位(对应变更集顶层 global 节点,同 match 路径口径),豁免 ID 命名规范
573
+ if (!c.id || (c.id !== GLOBAL_PSEUDO_ID && !ID_RE.test(c.id)))
573
574
  violations.push(`${label}.id "${c.id}" 不符合语义 ID 命名规范(小写下划线、2~4 段点分层级)`);
574
575
  if (c.position !== undefined && c.position !== null)
575
576
  scan(`${label}.position`, c.position);
@@ -1177,11 +1178,24 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
1177
1178
  console.log(` ${item.rule} ${registrySids(item).join(" + ")} 缺 dims=[${missing.join(",")}]`);
1178
1179
  }
1179
1180
 
1180
- // 实物校验:标注缺口的权威来源是效果图 HTML 里的 data-sid,而不是自报的 oracle.sids
1181
+ // 实物校验:标注缺口的权威来源是效果图 HTML 里的 data-sid,而不是自报的 oracle.sids。
1182
+ // 两类合法豁免(与 mockup-extraction-spec §7.1 / 设计侧 D3b 同源):
1183
+ // ① hiddenSids:visible:false 隐藏点设计本身不渲染,基线图上无元素可标;
1184
+ // ② 令牌通道 sid 无元素锚点,由同登记项内已标注的兄弟 sid 以 some 语义覆盖。
1181
1185
  let physicalGap = false;
1186
+ const hiddenSids = new Set(plan.filter(p => p.change && p.change.visible === false).map(p => p.id));
1187
+ const sidCoveredByItem = new Map();
1188
+ for (const it of items) {
1189
+ const sids = registrySids(it);
1190
+ const anyAnnotated = sids.some(s => mockupSids.has(s));
1191
+ for (const s of sids)
1192
+ sidCoveredByItem.set(s, anyAnnotated);
1193
+ }
1182
1194
  if (physicalSids) {
1183
1195
  console.log(` 效果图实物:${mockupFile}(实测 data-sid ${physicalSids.size} 个)`);
1184
- const declaredNoPhysical = [...declaredSids].filter(s => !physicalSids.has(s));
1196
+ const declaredNoPhysical = [...declaredSids].filter(
1197
+ s => !physicalSids.has(s) && !hiddenSids.has(s) && !sidCoveredByItem.get(s),
1198
+ );
1185
1199
  const physicalNoDeclared = [...physicalSids].filter(s => !declaredSids.has(s));
1186
1200
  if (declaredNoPhysical.length > 0) {
1187
1201
  console.log(` ⚠ oracle.sids 声明了 ${declaredNoPhysical.length} 个点位,但效果图实物里没有对应 data-sid(无基线):${declaredNoPhysical.join("、")}`);
@@ -1195,7 +1209,11 @@ function checkRegistryCoverage(changeset, plan, opts = {}) {
1195
1209
  }
1196
1210
 
1197
1211
  // 效果图标注完整性:恒存在的子项必须能在效果图上找到同名 data-sid,否则基线根本抽不出来
1198
- const unannotated = items.filter(it => it.always === true && !registrySids(it).some(s => mockupSids.has(s)));
1212
+ const unannotated = items.filter(
1213
+ it => it.always === true
1214
+ && !registrySids(it).every(s => hiddenSids.has(s))
1215
+ && !registrySids(it).some(s => mockupSids.has(s)),
1216
+ );
1199
1217
  if (unannotated.length > 0) {
1200
1218
  const bySid = new Map();
1201
1219
  for (const it of unannotated) {
@@ -1910,6 +1928,12 @@ function cmdAudit(args, dataset) {
1910
1928
  let problems = 0;
1911
1929
  const clean = [];
1912
1930
  for (const id of targetIds) {
1931
+ if (id === GLOBAL_PSEUDO_ID) {
1932
+ // 与 match 路径同口径:伪点位对应变更集顶层 global 节点(brand_primary 等基座令牌),
1933
+ // 走 :root/design-tokens 令牌通道对账,不入词典、不做词典点检。
1934
+ console.log(`\nℹ ${id} —— 设计侧伪点位(变更集顶层 global 节点,不入词典):基座令牌走 T 通道对账,词典点检豁免`);
1935
+ continue;
1936
+ }
1913
1937
  const entry = index[id];
1914
1938
  if (!entry) {
1915
1939
  console.log(`\n✘ ${id} —— 词典中无此条目(L3 新增点位,须当场落地并回写临时映射)`);
@@ -2112,6 +2136,8 @@ function cmdVerify(args, dataset) {
2112
2136
  console.log(`selector 挂载点存在性由 audit 静态校验;本计划只回答「按什么定位」。\n`);
2113
2137
  let missing = 0;
2114
2138
  for (const sid of sids) {
2139
+ if (sid === GLOBAL_PSEUDO_ID)
2140
+ continue; // 伪点位不入词典(同 match / audit 路径口径),基座令牌不走 selector 抽取
2115
2141
  const entry = index[sid];
2116
2142
  if (!entry) {
2117
2143
  console.log(` ✘ ${sid} —— 词典无此条目(L3 新点位,先落地并回写临时映射)`);
@@ -144,6 +144,9 @@ parser match(确定性翻译)→ 施工坐标 → AI 按坐标改代码 →
144
144
  | 1 | **施工对账清零**:变更集全部已声明点位(`changes[]` ∪ `visible:false` ∪ `pending` 兑现项)全部落地 | 对照 `.codebuddy/design/ledger.json` 台账与 parser match 输出逐条核销;剩余 >0 → 继续施工 |
145
145
  | 2 | **静态闸清零**:`parser audit` 与 `scripts/design-audit.mjs` 的 ERROR = 0 | 两闸任一 ERROR 即返工,不得带错收尾 |
146
146
  | 3 | **验收闭环**:变更集含 oracle 时跑第 6 步 verify(本环境无浏览器 → 渲染态抽取显式移交用户执行,并说明移交项) | verify 全绿或移交说明已给出 |
147
+ | 4 | **终态机读凭证**:跑通 `node <技能>/scripts/apply-final-gate.mjs --project <项目根> --container-width <内容区宽>`,取得 `FINAL-GATE: PASS` | 该命令串联四闸(changeset-selfcheck → parser match → parser audit → design-audit),任何一步非零即 `FAIL`,会话**不得以完成态结尾**;PASS 凭证自动写入 `.codebuddy/design/ledger.json` 的 `finalGate` 字段——终态判定以凭证为准,不以会话自述为准 |
148
+
149
+ > 条件 4 是 1–3 的机器汇总,不替代前三条的人读核对;前 3 条满足而凭证 FAIL = 仍有静态闸未清零,按 FAIL 输出逐项修复。
147
150
 
148
151
  **禁止的收尾话术(出现即判定为本轮交付失败,必须继续)**:
149
152
  - 「全量覆盖是迭代项 / 随迭代补齐 / 后续迭代再补其余细项 / 不阻塞已落地代码」——**覆盖率欠账 = 交付未完成**,不是迭代计划;设计侧变更集 `coverage.note` 出现同类表述同样视为交付失败(设计侧闸 D4/D9 会先拦,话术绕过闸的在这里兜底)。
@@ -19,7 +19,7 @@ node parser/index.js match --batch <changesets-dir>
19
19
  |------|------|------|
20
20
  | **L1 完整命中** | ID 在词典中直接命中 | 按条目施工坐标直接落地(预期 100% 还原) |
21
21
  | **L2 父级命中** | ID 的父模块命中 | 继承父模块样式约定,按子级定位施工(85-90% 还原) |
22
- | **L3 未命中** | 词典无此 ID(新增点位) | **当场按第 4 步定制布局落地**并写入 mapping 临时映射;只有设计侧未给完整意图(无值 / 值非法)才进缺口清单 |
22
+ | **L3 未命中** | 词典无此 ID(新增点位) | **当场按第 4 步定制布局落地**并写入 mapping 临时映射;只有设计侧未给完整意图(无值 / 值非法)才进缺口清单。**例外**:`global` 为设计侧伪点位(对应变更集顶层 global 节点的基座令牌,不入词典),按 T 通道对账,不按 L3 处理 |
23
23
  | **字段级未命中** | ID 命中但 `style` 某字段两层映射都查不到 | 值合法且选择器可定位 → **当场生成作用域内自定义规则落设计层文件并申报**;只有「值非法 / 无法定位」才进缺口清单 |
24
24
 
25
25
  **零缺口交付(禁止中途停顿)**:落地一次跑完,**不得因「词典没建这个 ID / 这个字段没枚举 / 这个值要不要确认」而停下来问用户**。缺口只有两类,去向各不相同:
@@ -16,7 +16,7 @@ node parser/index.js audit --project <目标项目根目录> [<changeset.json…
16
16
  |---|---|---|
17
17
  | **挂载点缺失 / 可疑** | `selector` / `anchors.target` 里的类名在项目里**只在样式文件出现、模板没挂**,或**完全找不到** | 列未绑 `class-name` → 金额列右对齐 / 缩略图尺寸永不生效;操作栏规则有定义无引用 |
18
18
  | **死通道** | `anchors` 里的 CSS 变量在项目中**无 `var()` 消费者**(两类豁免:`--el-*` 组件库变量由 EP 运行时消费;设计层已显式接管定义的变量属故意 suppress) | 变量有定义无引用,翻译再准也不生效 |
19
- | **词典无此条目** | 变更集引用了词典未建的 ID | L3 新增点位须当场落地并回写临时映射 |
19
+ | **词典无此条目** | 变更集引用了词典未建的 ID | L3 新增点位须当场落地并回写临时映射。**例外**:`global` 为设计侧伪点位(对应变更集顶层 global 节点的基座令牌),与 match 路径同口径不入词典、不做词典点检 |
20
20
  | **G2 structure_ops 施工证据** | 变更集声明 `structure_ops` 的点位,按词典 anchors/selector 语义类名 + `op.node` 提取探针,在**非设计层结构源码**中查证施工痕迹;零命中 = 纯 CSS 冒充结构施工 | 操作列分割线 `add_node` 声明了却从未施工 |
21
21
  | **G4 T 通道对账** | `global.brand_primary`(语义值解析 hex)↔ `platform-config.EpThemeColor`;页签 `form` ↔ `ShowModel`;`visible:false` 点位的 `visible_prop` 开关 ↔ platform-config 实值 | 变更集孔雀绿、platform-config 残留旧主题;flat 页签叠加 chrome 结构 |
22
22
 
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 终态机读凭证闸(apply-final-gate)
5
+ *
6
+ * 定位:design-apply 会话的**唯一合法结束凭证**。「会话结束条件」过去是文档条款(人读),
7
+ * 执行会话可以不跑、绕过、或以自述结束——孔雀绿轮实证:项目侧审计 ERROR 28 的交付物
8
+ * 以「已完成」收尾。本脚本把终态判定权交给机器:
9
+ *
10
+ * 串联四道闸 → 全部清零才输出 FINAL-GATE: PASS 并把凭证写入交付台账;
11
+ * 任何一步非零 → FINAL-GATE: FAIL,会话不得以完成态结尾,必须继续修复。
12
+ *
13
+ * 闸序(前置闸先跑,贵的后跑):
14
+ * 1. changeset-selfcheck(设计侧交付闸 D0–D13,含 G2 还原语义覆盖现算)
15
+ * 2. parser match(通道 / 挂载点 / G 闸)
16
+ * 3. parser audit(挂载点存在性 + 死通道静态审计)
17
+ * 4. design-audit(设计层闭环审计 E0–E16,含层叠胜负真算与列宽预算链)
18
+ *
19
+ * 用法:
20
+ * node scripts/apply-final-gate.mjs --project <落地项目根目录> [--container-width <px>] [--changeset <变更集.json>]
21
+ *
22
+ * 退出码:0 = PASS(凭证已写入 ledger.json);1 = FAIL;2 = 用法/环境错误。
23
+ */
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import fs from "node:fs";
27
+ import path from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
31
+ const NODE = process.execPath;
32
+
33
+ const argv = process.argv.slice(2);
34
+ const argValue = (flag, fallback = null) => {
35
+ const i = argv.indexOf(flag);
36
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback;
37
+ };
38
+
39
+ const PROJECT = path.resolve(argValue("--project", "") || "");
40
+ if (!PROJECT || !fs.existsSync(PROJECT)) {
41
+ console.error("用法:node scripts/apply-final-gate.mjs --project <落地项目根目录> [--container-width <px>] [--changeset <变更集.json>]");
42
+ console.error(` 项目不存在:${PROJECT || "(未提供 --project)"}`);
43
+ process.exit(2);
44
+ }
45
+ const CONTAINER_WIDTH = argValue("--container-width", "1200");
46
+
47
+ // ── 变更集发现(与 design-audit 同口径:多目录 + 显式 --changeset 优先)──
48
+ const DESIGN_DIRS = [".codebuddy/design", "design", ".changeset"];
49
+ let changeset = argValue("--changeset") ? path.resolve(argValue("--changeset")) : null;
50
+ if (!changeset) {
51
+ const found = [];
52
+ for (const d of DESIGN_DIRS) {
53
+ const full = path.join(PROJECT, d);
54
+ if (!fs.existsSync(full)) continue;
55
+ for (const f of fs.readdirSync(full)) {
56
+ if (!/changeset.*\.json$/i.test(f) || /^_/.test(f)) continue;
57
+ const fullFile = path.join(full, f);
58
+ if (fs.statSync(fullFile).isFile()) found.push({ file: fullFile, mtime: fs.statSync(fullFile).mtimeMs });
59
+ }
60
+ }
61
+ found.sort((a, b) => b.mtime - a.mtime);
62
+ changeset = found[0]?.file || null;
63
+ }
64
+ if (!changeset || !fs.existsSync(changeset)) {
65
+ console.error(`FINAL-GATE: FAIL(无变更集)`);
66
+ console.error(` ${PROJECT} 下未发现变更集(查找 .codebuddy/design、design、.changeset)——没有交付物就没有终态。`);
67
+ process.exit(1);
68
+ }
69
+
70
+ // ── 跨技能脚本定位(部署副本是同级技能目录;开发期 E 盘源同构)──
71
+ const UI_DESIGN_ROOT =
72
+ argValue("--ui-design-root") ||
73
+ [path.resolve(__dirname, "..", "..", "project-ui-design"), path.resolve(__dirname, "..", "project-ui-design")].find((p) =>
74
+ fs.existsSync(path.join(p, "scripts", "changeset-selfcheck.mjs")),
75
+ ) ||
76
+ null;
77
+ const SELFCHECK = UI_DESIGN_ROOT ? path.join(UI_DESIGN_ROOT, "scripts", "changeset-selfcheck.mjs") : null;
78
+ const PARSER = path.resolve(__dirname, "..", "parser", "index.js");
79
+ const DESIGN_AUDIT = path.join(__dirname, "design-audit.mjs");
80
+
81
+ const steps = [];
82
+ const run = (name, cmd, args) => {
83
+ const label = `${name}(${args.filter((a) => !a.startsWith("--") || a === "--project" || a === "--registry").map((a) => (a.length > 60 ? a.slice(0, 57) + "…" : a)).join(" ")})`;
84
+ try {
85
+ const out = execFileSync(cmd, args, { encoding: "buffer", timeout: 300000 }).toString("utf-8");
86
+ steps.push({ name, ok: true, label, output: out });
87
+ return true;
88
+ } catch (e) {
89
+ const out = `${e.stdout ? e.stdout.toString("utf-8") : ""}${e.stderr ? e.stderr.toString("utf-8") : ""}`;
90
+ steps.push({ name, ok: false, label, output: out });
91
+ return false;
92
+ }
93
+ };
94
+
95
+ console.log(`\n=== 终态机读凭证闸(apply-final-gate)===`);
96
+ console.log(` 项目:${PROJECT}`);
97
+ console.log(` 变更集:${path.relative(PROJECT, changeset).replace(/\\/g, "/")}\n`);
98
+
99
+ let allOk = true;
100
+
101
+ // 1. 设计侧交付闸
102
+ if (SELFCHECK) {
103
+ allOk = run("① 交付闸 changeset-selfcheck", NODE, [SELFCHECK, "--changeset", changeset]) && allOk;
104
+ } else {
105
+ steps.push({
106
+ name: "① 交付闸 changeset-selfcheck",
107
+ ok: false,
108
+ label: "未找到 project-ui-design 技能",
109
+ output: "",
110
+ });
111
+ console.error(" ✖ 未定位到 project-ui-design/scripts/changeset-selfcheck.mjs——部署不完整,按失败处理(fail-closed)。");
112
+ allOk = false;
113
+ }
114
+
115
+ // 2/3. parser 闸
116
+ if (fs.existsSync(PARSER)) {
117
+ allOk = run("② parser match(通道与 G 闸)", NODE, [PARSER, "match", changeset]) && allOk;
118
+ allOk = run("③ parser audit(挂载点静态审计)", NODE, [PARSER, "audit", "--project", PROJECT, changeset]) && allOk;
119
+ } else {
120
+ console.error(" ✖ 未定位到 parser/index.js——技能部署不完整,按失败处理(fail-closed)。");
121
+ steps.push({ name: "②/③ parser 闸", ok: false, label: "parser/index.js 缺失", output: "" });
122
+ allOk = false;
123
+ }
124
+
125
+ // 4. 设计层闭环审计(由它维护 ledger 轮次;--changeset 固定同一份,避免多份并存时各闸看错对象)
126
+ allOk = run("④ design-audit(设计层闭环审计)", NODE, [DESIGN_AUDIT, "--project", PROJECT, "--container-width", CONTAINER_WIDTH, "--changeset", changeset]) && allOk;
127
+
128
+ // ── 汇总 + 凭证落账 ─────────────────────────────────────────
129
+ console.log(`\n${"=".repeat(56)}`);
130
+ for (const s of steps) {
131
+ console.log(` ${s.ok ? "✓" : "✗"} ${s.name}`);
132
+ if (!s.ok) {
133
+ const tail = s.output.trim().split("\n").slice(-14).join("\n ");
134
+ if (tail) console.log(` ${tail}`);
135
+ }
136
+ }
137
+
138
+ const status = allOk ? "PASS" : "FAIL";
139
+ console.log(`\nFINAL-GATE: ${status}`);
140
+ if (allOk) {
141
+ console.log(` 四闸清零,交付完成。凭证已写入 .codebuddy/design/ledger.json(finalGate)。`);
142
+ } else {
143
+ const firstBad = steps.find((s) => !s.ok);
144
+ console.log(` 终态未达成(首个未清零闸:${firstBad?.name || "?"})——会话不得以完成态结尾,须继续修复后复跑。`);
145
+ }
146
+
147
+ // 凭证写入 ledger.json(不改动 design-audit 维护的轮次历史,只追加 finalGate 字段)
148
+ try {
149
+ const ledgerPath = path.join(PROJECT, ".codebuddy", "design", "ledger.json");
150
+ let ledger = {};
151
+ if (fs.existsSync(ledgerPath)) {
152
+ try {
153
+ ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf-8"));
154
+ } catch {
155
+ ledger = {};
156
+ }
157
+ }
158
+ ledger.finalGate = {
159
+ status,
160
+ at: new Date().toISOString(),
161
+ changeset: path.relative(PROJECT, changeset).replace(/\\/g, "/"),
162
+ steps: steps.map((s) => ({ name: s.name, ok: s.ok })),
163
+ };
164
+ fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
165
+ fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2) + "\n", "utf-8");
166
+ } catch {
167
+ /* 台账写入失败不改变终态判定,凭证以本命令退出码为准 */
168
+ }
169
+
170
+ process.exit(allOk ? 0 : 1);