@qilitt-mickey/vue3-temp-skill 1.1.70 → 1.1.72
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 +3 -2
- package/package.json +1 -1
- package/parser/fixtures/verify/snapshot-latest.json +1 -1
- package/parser/index.js +51 -8
- package/references/code-quality.md +3 -2
- package/references/design-apply.md +14 -3
- package/references/steps/maintenance.md +2 -2
- package/references/steps/step1-match.md +2 -2
- package/references/steps/step2-structure.md +1 -1
- package/references/steps/step3b-shell.md +2 -2
- package/references/steps/step5-gates.md +1 -1
- package/scripts/apply-final-gate.mjs +4 -4
- package/scripts/check.mjs +81 -5
- package/scripts/design-audit.mjs +208 -14
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 必须清零;无落地项目时自动跳过)。**会话结束前必须跑通终态凭证闸**:`node scripts/apply-final-gate.mjs --project <落地项目> --container-width <内容区实际宽>`(串联 changeset-selfcheck → parser match → parser audit → design-audit 四闸,`FINAL-GATE: PASS` 才算交付完成,凭证写入 `.
|
|
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` 才算交付完成,凭证写入 `.design/ledger.json`)。
|
|
36
36
|
|
|
37
37
|
## 能力组路由
|
|
38
38
|
|
|
@@ -91,7 +91,7 @@ tags: [vue3, typescript, element-plus, pinia, vite, unocss, crud, demo, code-qua
|
|
|
91
91
|
### 4. 验证与交付
|
|
92
92
|
|
|
93
93
|
- 对照 `references/code-quality.md` 检查类型、组件复用、API、路由、权限、安全、异常反馈、响应式和可演进性。
|
|
94
|
-
- 在目标项目执行 `pnpm lint:fix`、`pnpm type-check`;改动构建配置时再执行 `pnpm build
|
|
94
|
+
- 在目标项目执行 `pnpm lint:fix`、`pnpm type-check`;改动构建配置时再执行 `pnpm build`。**本轮产出或修改设计变更集时,这两条不含还原度检查——必须按 code-quality 协议 3 同链追加终态闸 `apply-final-gate`(串联 selfcheck → match → audit → design-audit,取得 FINAL-GATE: PASS),禁止只跑 lint 链就报「全部质量闸门通过」。**
|
|
95
95
|
- **设计文件落地时追加渲染态验收**:变更集含 `oracle`(或效果图带 `data-sid`)时,跑 `node parser/index.js verify`——抽取项目同名 `data-sid` 元素的 computed style,与效果图期望值逐点 diff;缺口按四类归因(选择器落空 / 权重被压 / 语境错位 / **存在性缺口 G12**:`oracle.absent` 点位仍可见)修正,禁止用 `!important` 掩盖。多轮迭代场景每轮动工前先 `--snapshot` 导快照、交付前 `--baseline` 比对——上轮命中本轮退步 = 基线漂移,先回归上轮修法。申报过的固有残差(`oracle.residual_accepted`)不计缺口。
|
|
96
96
|
- **设计文件落地时核对登记表覆盖率**:变更集带登记表(`coverage.registry` 或 `--registry`)时,跑 `node parser/index.js match <cs.json> --registry <restore-registry.json>`——`C = A` 且无缺口才可交付;parser **当场以登记表为权威重算**,设计侧自报的 `coverage` 数字不参与。单独核验登记表对词典的通道就绪度用 `node parser/index.js registry <restore-registry.json>`(硬缺口 exit 1、通道缺口列出待补字段)。
|
|
97
97
|
- 最终说明:命中能力组、修改文件、功能验证、命令结果、未解决问题和下一步。
|
|
@@ -131,6 +131,7 @@ tags: [vue3, typescript, element-plus, pinia, vite, unocss, crud, demo, code-qua
|
|
|
131
131
|
- 功能:...
|
|
132
132
|
- `pnpm lint:fix`:通过 | 失败(原因)
|
|
133
133
|
- `pnpm type-check`:通过 | 失败(原因)
|
|
134
|
+
- 设计闸(本轮有设计变更集时必填,无则 N/A):`apply-final-gate` → FINAL-GATE: PASS ✅(凭证已写入 ledger.json)
|
|
134
135
|
- 设计验收:无 oracle 时 N/A | 逐点 diff 结果(命中 N / 缺口 M,归因与修正)
|
|
135
136
|
- 缺口回流:无 | 仅限「设计侧未给值」的维度(列出维度名,注明已回流设计侧补齐)
|
|
136
137
|
```
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generated_at": "2026-09-
|
|
2
|
+
"generated_at": "2026-09-24T07:14:25.807Z",
|
|
3
3
|
"oracle": "E:\\work\\Project\\Template\\skills\\vue3-temp-skill\\parser\\fixtures\\verify\\oracle.json",
|
|
4
4
|
"actual": "E:\\work\\Project\\Template\\skills\\vue3-temp-skill\\parser\\fixtures\\verify\\actual-clean.json",
|
|
5
5
|
"points": {
|
package/parser/index.js
CHANGED
|
@@ -111,7 +111,7 @@ function printHelp() {
|
|
|
111
111
|
|
|
112
112
|
用法:
|
|
113
113
|
node parser/index.js build [--dry] 校验映射表并生成 dist/ 产物
|
|
114
|
-
node parser/index.js match <changeset.json...>
|
|
114
|
+
node parser/index.js match <changeset.json...> 对设计变更集执行三级匹配(可多文件;mode 唯一合法值 restore——G8 模式闸强制 oracle.mockup + coverage.registry 齐备,显式 design 直接拒绝)
|
|
115
115
|
主题身份:changeset.theme(优先)→ global.brand_primary(回退);
|
|
116
116
|
基线图色板:图内 data-mockup-theme(优先)→ 文件名主题词(回退)。
|
|
117
117
|
二者不一致时须声明 oracle.theme_mapping(只做色值归一,不减声明)。
|
|
@@ -261,11 +261,18 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
261
261
|
// 「场景还原应给」的软提示升级为「mode=restore 必须给」的硬闸。
|
|
262
262
|
// 二轮强化:oracle/coverage 须有「芯」——oracle.mockup 与 coverage.registry 非空,
|
|
263
263
|
// 堵住空壳伪基线(有对象无内容)继续蒙混。
|
|
264
|
-
// mode
|
|
265
|
-
|
|
266
|
-
const mode = changeset.mode === undefined || changeset.mode === null ?
|
|
267
|
-
if (
|
|
268
|
-
console.error(`✖ mode
|
|
264
|
+
// 2026-09-24 终判:降级通道废除——mode 唯一合法值 restore,显式 "design" 直接拒绝。
|
|
265
|
+
// mode 缺省按旧协议兼容(legacy 变更集无 scheme/page_types,由下方 G8b 基线命中闸兜底)。
|
|
266
|
+
const mode = changeset.mode === undefined || changeset.mode === null ? null : changeset.mode;
|
|
267
|
+
if (mode === "design") {
|
|
268
|
+
console.error(`✖ mode="design" 不是合法交付模式:还原任务的目标是把设计 100% 落地到项目——环境受限 / 覆盖不足 / 意图不明时唯一正确动作是停下向用户多轮询问确认,不得缩水交付;用户显式只要小改动时,不产出还原变更集(按普通需求直接修改)。基线三件套(oracle.mockup + coverage.registry)是还原交付的固有组成,任何 mode 形态都不得移除。`);
|
|
269
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
270
|
+
if (!batchSummary)
|
|
271
|
+
process.exit(1);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (mode !== null && mode !== "restore") {
|
|
275
|
+
console.error(`✖ mode 应为 "restore"(唯一合法值),实际 "${mode}"`);
|
|
269
276
|
batchSummary?.invalid.push(path.basename(file));
|
|
270
277
|
if (!batchSummary)
|
|
271
278
|
process.exit(1);
|
|
@@ -292,8 +299,8 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
292
299
|
if (missing.length > 0) {
|
|
293
300
|
console.error(`\n✖ G8 模式闸:mode=restore(场景还原)但缺少 ${missing.join(" 与 ")} → 拒绝执行。`);
|
|
294
301
|
console.error(` 场景还原的完备性以登记表覆盖率为分母、以 oracle 为验收基线——缺任一项即「伪还原变更集」:`);
|
|
295
|
-
console.error(`
|
|
296
|
-
console.error(`
|
|
302
|
+
console.error(` 只换主题色也能一路绿灯落地,但布局细节点位从未被声明,还原度不可证明。`);
|
|
303
|
+
console.error(` 处置:补齐后重跑——mode=design 不是合法出口;范围确需缩小时停下向用户多轮询问确认。`);
|
|
297
304
|
batchSummary?.invalid.push(path.basename(file));
|
|
298
305
|
if (!batchSummary)
|
|
299
306
|
process.exit(1);
|
|
@@ -302,6 +309,42 @@ function matchOne(file, dataset, batchSummary, opts = {}) {
|
|
|
302
309
|
console.log(` ✔ G8 模式闸:mode=restore,oracle + coverage 齐备(基线三件套完整)`);
|
|
303
310
|
}
|
|
304
311
|
|
|
312
|
+
// G8 三轮强化(2026-09-24 孔雀绿轮实证):基线命中 ⇒ 不得以缺 mode / 降级形态移除基线三件套。
|
|
313
|
+
// 事故形态:执行会话把「环境无浏览器」读成降级许可,以 6 条品牌色变更交差,
|
|
314
|
+
// notes 自述「未强制绑定基线登记表与 oracle.json」。显式 mode=design 已被上方硬拒,
|
|
315
|
+
// 本闸兜底「缺 mode 但带 scheme + page_types」的现代形态:三件套缺一即拒。
|
|
316
|
+
if (mode !== "restore") {
|
|
317
|
+
const covG8b = changeset.coverage && typeof changeset.coverage === "object" ? changeset.coverage : {};
|
|
318
|
+
const pageTypesG8b = Array.isArray(covG8b.page_types) ? covG8b.page_types : [];
|
|
319
|
+
const hasCoreG8b =
|
|
320
|
+
typeof changeset.oracle?.mockup === "string" && changeset.oracle.mockup.trim().length > 0 &&
|
|
321
|
+
typeof covG8b.registry === "string" && covG8b.registry.trim().length > 0;
|
|
322
|
+
if (pageTypesG8b.length && changeset.scheme && !hasCoreG8b) {
|
|
323
|
+
// 方案注册表(含 baseline 登记)在设计侧技能内;跨技能同机部署时按兄弟目录解析
|
|
324
|
+
const schemeRegistryG8b = path.resolve(__dirname, "..", "..", "project-ui-design", "references", "page-specs", "styles", "registry.json");
|
|
325
|
+
let baselineHitsG8b = [];
|
|
326
|
+
try {
|
|
327
|
+
const srG8b = JSON.parse(readFileSync(schemeRegistryG8b, "utf-8"));
|
|
328
|
+
for (const schG8b of srG8b.schemes || []) {
|
|
329
|
+
if (schG8b.id !== changeset.scheme) continue;
|
|
330
|
+
for (const [ptG8b, bG8b] of Object.entries(schG8b.baseline || {})) {
|
|
331
|
+
if (ptG8b.startsWith("$") || !bG8b || typeof bG8b !== "object" || !bG8b.mockup) continue;
|
|
332
|
+
if (pageTypesG8b.includes(ptG8b)) baselineHitsG8b.push(ptG8b);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch { /* 设计侧技能未检出(非同机部署)时跳过——设计侧 D15 为权威闸 */ }
|
|
336
|
+
if (baselineHitsG8b.length) {
|
|
337
|
+
console.error(`\n✖ G8 模式闸(三轮强化):还原目标存在注册基线(scheme="${changeset.scheme}" × scene="${changeset.scene || "?"}" × 页型 ${baselineHitsG8b.join("/")}),却以 mode=${mode || "(缺省)"} 且缺基线三件套交付 → 拒绝执行。`);
|
|
338
|
+
console.error(` 环境受限 / 覆盖不足 / 意图不明时唯一正确动作是停下向用户多轮询问确认,基线三件套(oracle.mockup + coverage.registry)照交;`);
|
|
339
|
+
console.error(` 严禁借任何 mode 形态把还原缩水成主题色变更。`);
|
|
340
|
+
batchSummary?.invalid.push(path.basename(file));
|
|
341
|
+
if (!batchSummary)
|
|
342
|
+
process.exit(1);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
// G13 页面绑定闸(2026-09-22 事故机制化):效果图页面是设计角色坐标系,不是页面增量。
|
|
306
349
|
// 事故背景:效果图演示页「运单管理」被误落地为项目新菜单/新页面——效果图标题只是
|
|
307
350
|
// 设计侧为承载设计语言虚构的演示名,落地动作恒为「映射已有页面 + 在其上同步结构与样式」,
|
|
@@ -13,7 +13,7 @@ tags: [code-review, quality, checklist, lint, typescript, vue3, security, perfor
|
|
|
13
13
|
|
|
14
14
|
1. 重读本文件
|
|
15
15
|
2. 列出本轮改动路径,对照下方清单;**硬失败表命中则先改代码再继续**
|
|
16
|
-
3. 业务项目根目录必跑:`pnpm lint:fix` → `pnpm type-check`
|
|
16
|
+
3. 业务项目根目录必跑:`pnpm lint:fix` → `pnpm type-check`;**本轮产出或修改了设计变更集(`.design/changeset*.json`)时,同一条命令链追加终态闸:`node <技能目录>/scripts/apply-final-gate.mjs --project <项目根> --container-width <内容区宽>`,取得 `FINAL-GATE: PASS` 才算通过**(该命令串联 changeset-selfcheck → parser match → parser audit → design-audit 四闸,PASS 凭证写入 ledger.json;只做设计侧产出、未落项目时至少实跑 changeset-selfcheck 并附 exit 0 输出原文)。**禁止把 lint/type-check 单独当「全部质量闸门」汇报**——它们不含任何还原度检查
|
|
17
17
|
4. 失败则修代码并重跑,直至通过或写明阻塞
|
|
18
18
|
5. 最终回复追加:
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ tags: [code-review, quality, checklist, lint, typescript, vue3, security, perfor
|
|
|
22
22
|
- 闸门:已执行 code-quality(生成后)
|
|
23
23
|
- 改动文件:`path/a`、`path/b`、…
|
|
24
24
|
- 清单:选型 ✅ / 类型 ✅ / 组件 ✅ / API ✅ / 安全 ✅ / 业务 ✅ / 列表→详情 ✅或N/A / 列表布局 ✅或N/A / 风格 ✅
|
|
25
|
-
- 设计落地:通道活性 ✅ / 锚点契约 ✅ / 结构施工 ✅或N/A / 覆写提权 ✅ / 登记表覆盖率 ✅或N/A / 渲染态验收 ✅或N/A
|
|
25
|
+
- 设计落地:通道活性 ✅ / 锚点契约 ✅ / 结构施工 ✅或N/A / 覆写提权 ✅ / 登记表覆盖率 ✅或N/A(仅本轮无设计变更集时可用 N/A)/ 渲染态验收 ✅或N/A(**同左;有变更集而无 oracle = 硬失败,不得 N/A**)/ 设计闸命令链 ✅(selfcheck + match + audit 输出原文已附)或N/A(无变更集)
|
|
26
26
|
- 硬失败:无 | 已拦截并修正:…
|
|
27
27
|
- 已修正:…(无则写「无」)
|
|
28
28
|
- 命令:`pnpm lint:fix` → …;`pnpm type-check` → …
|
|
@@ -48,6 +48,7 @@ lint / type-check **过了也不能放过**下列项。发现即改代码,禁
|
|
|
48
48
|
| 列表查询区 `el-form` / 查询按钮 | 写了 `size="small"`(紧凑主题用户未要求) | 不写 size,用 default=`--control-height` |
|
|
49
49
|
| 本轮新增 addon 并引用对应 `Re*` / Vant / 微信 | 已装包但 `tsconfig exclude` 仍含该路径,或 `addon-enable` 要求的 build/src 未改 | 按 **`addon-enable.md`** 对应章节恢复后重跑 type-check |
|
|
50
50
|
| 设计层(`design-shell.scss` / 页面 `scoped`)新增规则 | 与对手**同权重**(对手为组件 `<style scoped>` 时平手即失效——组件样式在全局之后注入),或对手带 `!important` 而本条没带 | 按 `priority.strategy` 提权;**数权重,不许目测** |
|
|
51
|
+
| 本轮产出/修改了设计变更集(`.design/changeset*.json`) | ① 变更集 `mode` 非 `restore`(缺 mode / 写 design 均为硬错误——无登记表、无基线的轻量变更集落地后必然「只变主题色、布局细节零变化」);② 变更集 `coverage.page_types` 命中注册基线页型,却缺 `oracle.mockup` 或 `coverage.registry`(基线三件套是还原交付的固有组成);③ 设计闸未实跑(selfcheck exit 0 输出原文未附 / `match --registry` / `design-audit` 缺任一);④ notes 的覆盖话术与 coverage 字段矛盾(如无三件套却写「等价覆盖全量」) | 补齐三件套(oracle 值可由规范派生并在 notes 声明「未做浏览器实测」)后重跑全部设计闸;范围疑问停下向用户多轮询问确认——不存在降级出口 |
|
|
51
52
|
| 变更集带 `oracle` | ① `oracle.mockup` 不是本页型的**场景基线图**(对照注册表 `baseline.<页型>.mockup`);② 基线图色板(图内 `data-mockup-theme`)与本次主题(`changeset.theme`)不一致**且未声明** `oracle.theme_mapping`;③ `oracle.sids` 与 `changes[]` 未双向闭合(存在欠声明 / 无基线点位)。**注意:主题 ≠ 基线图色板本身不是违规**——基线图是场景基线、与主题无关,声明 `theme_mapping`(色值归一)后照常全量施工,**不得**把主题不一致当回流理由缩水成只换主题色 | 视缺口而定:缺 `theme_mapping` → 回流设计侧补声明;`sids` 未闭合 → 回流设计侧补齐后再开工,禁止直接施工 |
|
|
52
53
|
| `element-plus.scss` / `sidebar.scss` 等样式消费层 | `var(--font-size-*)` 用于 `line-height` / `height` 等非字号属性 | 查 TSB:`classifyToken` + `PROPERTY_FORBIDDEN`;`line-height` → `--line-height` 或 `--el-menu-item-height` |
|
|
53
54
|
| `element-plus.scss` 内视觉规则(shadow/hover/transition) | 出现非令牌引用的裸 hex / px / rgba 值 | 全部改为 `var()` 引用令牌 |
|
|
@@ -11,7 +11,7 @@ tags: [design-apply, changeset, semantic-id, mapping, parser, theme, layout]
|
|
|
11
11
|
|
|
12
12
|
本能力组是**双 Skill 链路的项目侧执行端**:统一获取上游设计技能产出的「语义 ID 变更集」JSON(`design.changeset/1.0`,协议技术栈中立),由本 Skill 负责翻译并落地到目标项目(vue3-web-temp 及派生项目)。上游设计技能为 **project-ui-design**,协议文档见其 `references/changeset-schema.md`。
|
|
13
13
|
|
|
14
|
-
**变更集落点唯一化(强制)**:变更集一律落在 `<落地项目>/.
|
|
14
|
+
**变更集落点唯一化(强制)**:变更集一律落在 `<落地项目>/.design/changeset-<场景>-<页型>-<主题>.json`,与交付台账 `ledger.json` 同目录;多份并存时无法判定「本轮交付物」(`design-audit.mjs` 会告警)。**落地台账**(`ledger.json`,由审计闸自动维护)记录每轮的 `covered/applicable`、设计层行数与已落地语义 ID 集合——它既是**回退检测**的依据,也是**断点续传**的入口:跨轮「继续」时按 `applicable − 已落地点数` 取剩余工作项,禁止把「继续」当成重跑一次同样的开头。
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
设计变更集 JSON(设计技能产出,中立协议)
|
|
@@ -135,16 +135,27 @@ parser match(确定性翻译)→ 施工坐标 → AI 按坐标改代码 →
|
|
|
135
135
|
|
|
136
136
|
**红线摘要(全文适用,细节在各步子文件)**:出厂中性 · 变更集驱动(上文);先结构后样式(第 2 步先于第 3 步);缺口只分「能力缺口(当场落地)」与「意图缺口(回流设计侧)」两类;**效果图/大文件只读一次**——期望值落盘 `oracle.json` 后,后续环节一律消费落盘产物,禁止反复重读。环境无 Node 时按「环境备注」(见 maintenance.md)人工执行。
|
|
137
137
|
|
|
138
|
+
## 施工标记规范(G3:@sid / @closed,design-audit E15 机检)
|
|
139
|
+
|
|
140
|
+
结构性声明(`form` / `suppress` / `suppress_inner` / `structure_ops` / `text_visible:none` / `visible:false`)**必须在设计层留机读标记**——声明了不等于落地了,没有标记就没有对账凭证(悬空声明例:`collapse_trigger.form=header_inline` 声明后壳层零规则,落地端静默无效):
|
|
141
|
+
|
|
142
|
+
| 标记 | 写法 | 作用 |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `@sid` | 规则上方 `/* @sid: <语义ID> */` | 「变更集 ↔ 设计层」逐条落点对账(@sid 强制) |
|
|
145
|
+
| `@closed` | 收口规则上方 `/* @closed: <通道名> */` | suppress 收口兑现证据(通道名 = 词典 forms/css_vars/custom_style_map 的键) |
|
|
146
|
+
|
|
147
|
+
补充证据通道:通道为 `css_vars` 键时,设计层对该 var 写关闭值(`transparent` / `none` / `0`)即视为兑现,可免 `@closed` 标记;`visible:false` 的 `display:none` 规则按 E6 口径取证。**双向对账(E15)**:设计层 `@closed` 了变更集未声明的通道 → WARN(台账失真,收口记录进不了 D13 继承链);声明了词典不认识的 form 值 / 通道名 → ERROR(悬空声明 = 无规则可挂);结构性声明在设计层零证据 → ERROR。
|
|
148
|
+
|
|
138
149
|
## 会话结束条件(终态硬闸,违反 = 交付未完成)
|
|
139
150
|
|
|
140
151
|
**落地会话不许「做一批、停下来等用户说继续」。** 「继续」只是异常中断后的恢复手段,不是交付形态——把恢复手段当成默认流程 = 把 1:1 还原降级为多次半成品。本轮做完一批点位后,**必须自动进入下一批**,直到同时满足以下三条才允许结束会话:
|
|
141
152
|
|
|
142
153
|
| # | 结束条件 | 判定方式 |
|
|
143
154
|
|---|---|---|
|
|
144
|
-
| 1 | **施工对账清零**:变更集全部已声明点位(`changes[]` ∪ `visible:false` ∪ `pending` 兑现项)全部落地 | 对照 `.
|
|
155
|
+
| 1 | **施工对账清零**:变更集全部已声明点位(`changes[]` ∪ `visible:false` ∪ `pending` 兑现项)全部落地 | 对照 `.design/ledger.json` 台账与 parser match 输出逐条核销;剩余 >0 → 继续施工 |
|
|
145
156
|
| 2 | **静态闸清零**:`parser audit` 与 `scripts/design-audit.mjs` 的 ERROR = 0 | 两闸任一 ERROR 即返工,不得带错收尾 |
|
|
146
157
|
| 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 凭证自动写入 `.
|
|
158
|
+
| 4 | **终态机读凭证**:跑通 `node <技能>/scripts/apply-final-gate.mjs --project <项目根> --container-width <内容区宽>`,取得 `FINAL-GATE: PASS` | 该命令串联四闸(changeset-selfcheck → parser match → parser audit → design-audit),任何一步非零即 `FAIL`,会话**不得以完成态结尾**;PASS 凭证自动写入 `.design/ledger.json` 的 `finalGate` 字段——终态判定以凭证为准,不以会话自述为准。**mode 唯一合法值 restore**:缺 mode / 写 design / 基线命中却缺三件套,均在此拦截 |
|
|
148
159
|
|
|
149
160
|
> 条件 4 是 1–3 的机器汇总,不替代前三条的人读核对;前 3 条满足而凭证 FAIL = 仍有静态闸未清零,按 FAIL 输出逐项修复。
|
|
150
161
|
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## 词典维护纪律
|
|
8
8
|
|
|
9
|
-
### 出厂状态漂移(词典假前提 =
|
|
9
|
+
### 出厂状态漂移(词典假前提 = 风险源,落地前必查)
|
|
10
10
|
|
|
11
|
-
**项目可能随时被还原到出厂**(design-shell 回空壳、`.changeset/*` 消失、本体系此前补的通道全部消失)。因此词典里凡「出厂已具备 X」类表述必须满足:① X 是**真出厂能力**(对照出厂源码可验证);② 由本体系补建的通道一律写成**条件式**——「若缺失,按 structure_ops / 补通道流程补建并申报」,不写「已存在」。**落地前必须跑 `node parser/index.js audit --project <dir
|
|
11
|
+
**项目可能随时被还原到出厂**(design-shell 回空壳、`.changeset/*` 消失、本体系此前补的通道全部消失)。因此词典里凡「出厂已具备 X」类表述必须满足:① X 是**真出厂能力**(对照出厂源码可验证);② 由本体系补建的通道一律写成**条件式**——「若缺失,按 structure_ops / 补通道流程补建并申报」,不写「已存在」。**落地前必须跑 `node parser/index.js audit --project <dir>`**:锚点/选择器/变量消费者存在性静态校验,会当场暴露词典假前提(例:词典写「出厂已渲染分割线 / 工具栏行已挂语义类」,项目还原后全部失真 → 规则落空)。
|
|
12
12
|
|
|
13
13
|
- mapping/ 是唯一事实源:施工前 `parser query <id>` 精读条目,**不凭记忆施工**。
|
|
14
14
|
- **禁止编造的是「既有条目的映射」**(改既有 selector / css_vars / 变量指向 = 静默篡改,绝对禁止)。**词典缺 ID / 缺字段枚举不属编造**——那是新点位,按 L3 当场落地并写**新增的**临时映射条目,交付说明申报,等人工评审转正。
|
|
@@ -29,9 +29,9 @@ node parser/index.js match --batch <changesets-dir>
|
|
|
29
29
|
| **能力缺口**(词典缺 ID / 缺字段枚举 / 缺 form) | 设计侧意图完整、值合法 | **项目侧当场落地**:按 L3 定制布局施工并写 mapping 临时映射;出厂文件仅在「补通道」时可动,交付说明单列申报。**进清单 = 把可落地项搁置 = 交付失败** |
|
|
30
30
|
| **意图缺口**(设计侧没给值 / 值非法) | 变更集里本就没有这个值 | 才可进「缺口清单」,交付说明写明「回流设计侧补齐」——**不是问用户要不要做** |
|
|
31
31
|
|
|
32
|
-
> **闸门红 = 硬停(交付缺陷,性质与上表相反)**:`parser match` **退出码非 0**(G8 模式闸 / G9 登记表身份闸 / G10 dims 闸 / G11 oracle 基线闸 / 覆盖率细闸,任一不通过)时
|
|
32
|
+
> **闸门红 = 硬停(交付缺陷,性质与上表相反)**:`parser match` **退出码非 0**(G8 模式闸 / G9 登记表身份闸 / G10 dims 闸 / G11 oracle 基线闸 / 覆盖率细闸,任一不通过)时 **禁止开工落地**。合法出路只有一条:回设计侧补齐(基线 / 登记项 / 点位)后重跑——**`mode` 唯一合法值是 `restore`**,不存在「经用户同意降级」的出口;范围确需缩小时,停下向用户多轮询问确认真实意图,按确认结论调整变更集范围后重算。
|
|
33
33
|
>
|
|
34
|
-
> **禁止「挑拣完整的那部分先落地」**:最典型的错误是闸门报错后只落全局主题令牌(改 `platform-config.json` 的 `EpThemeColor` + 设计层覆盖 `--brand-primary
|
|
34
|
+
> **禁止「挑拣完整的那部分先落地」**:最典型的错误是闸门报错后只落全局主题令牌(改 `platform-config.json` 的 `EpThemeColor` + 设计层覆盖 `--brand-primary`)再宣称完成——用户看到的是「只换了主题色,布局样式全不对」,且重建设计层时会把上一轮成果一并覆盖。上表「禁止中途停顿」**只适用于能力缺口**(词典没建 ID / 字段没枚举),**不适用于闸门红**。
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
`缺口清单` 只承载第二类,且必须给出点位 ID + 缺失字段名。
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
- 设计未声明该点位(效果图上没有这个位置的操作)→ 收口隐藏(`visible:false` 或隐藏通道),不显示演示文字;
|
|
23
23
|
- **判据一句话:还原后的页面不得残留模板的演示说明性文字与占位按钮——它们既不是设计效果,也不是交付目标。** 残留 = 把演示噪音当成了还原结果,即「效果 vs 内容」混淆的实景表现。
|
|
24
24
|
|
|
25
|
-
**结构/样式图层分工**:`form`(高度模式、形态枚举)是**结构意图**,必须落到 DOM 属性/组件开关/配置字段(如 `data-content-mode`、`ShowModel`);**禁止把结构意图降级翻译成 SCSS 变量写进样式文件**(form=scroll 只写 `--vts-margin` =
|
|
25
|
+
**结构/样式图层分工**:`form`(高度模式、形态枚举)是**结构意图**,必须落到 DOM 属性/组件开关/配置字段(如 `data-content-mode`、`ShowModel`);**禁止把结构意图降级翻译成 SCSS 变量写进样式文件**(form=scroll 只写 `--vts-margin` = 未施工)。样式文件(如 design-shell.scss)只承载 css_vars/custom_style_map 翻译产物;壳层通用文件禁止代打页面级结构属性(lay-content 硬编码 `data-content-mode="scroll"` 会压掉 `:has()` 自动定高探测,useTableSearch 高度链断裂致列表区整体崩坏,禁止此类写法)。
|
|
26
26
|
|
|
27
27
|
**结构链不可侵(高度链保命红线)**:内容区高度链是**结构层**,已与视觉解耦,两侧各有边界——
|
|
28
28
|
|
|
@@ -49,9 +49,9 @@ html body .sidebar-container { /* 令牌宿主 = 该令牌被消费的
|
|
|
49
49
|
|
|
50
50
|
- **本文件是产物,不是手改对象(流程红线)**:`design-shell.scss` 的每一行都必须由「变更集 + 词典」经本协议产出——发现缺规则时,正确动作是**补词典字段 / 前置条件 / 登记项**让下一次 design-apply 自动生成,**禁止绕过流程直接手写项目文件**。仅「补通道」(变量消费者 / form 枚举 / 组件中性默认)允许动出厂源码并单列申报。
|
|
51
51
|
|
|
52
|
-
- **累积性红线(禁止整文件重写)**:本文件的**驱动源是变更集,不是词典**——「历史规则已落词典,重生成后会自动带出」**只在「本轮变更集交齐全部适用点位」时成立**。若上游只交了部分点位(`coverage.covered < applicable`、`gaps`
|
|
52
|
+
- **累积性红线(禁止整文件重写)**:本文件的**驱动源是变更集,不是词典**——「历史规则已落词典,重生成后会自动带出」**只在「本轮变更集交齐全部适用点位」时成立**。若上游只交了部分点位(`coverage.covered < applicable`、`gaps` 非空),按「出厂空文件」重建会把上一轮已落地的规则**整段覆盖**,用户侧表现为「布局样式全不对,只剩主题色」(例:620 行 → 77 行,同时上游 covered=31/186)。因此:
|
|
53
53
|
- **重建即合并**:本轮变更集未覆盖的既有段落**原样保留**,只增删本轮涉及的段;自举(文件缺失 → 新建)后不得把既有内容当作「历史包袱」丢弃;
|
|
54
|
-
- **落地前后各记一次状态**(设计层行数 + 已落地语义 ID 集合):`node scripts/design-audit.mjs --project <落地项目>` 的 E12 会自动写 `.
|
|
54
|
+
- **落地前后各记一次状态**(设计层行数 + 已落地语义 ID 集合):`node scripts/design-audit.mjs --project <落地项目>` 的 E12 会自动写 `.design/ledger.json` 台账并与历史峰值比对,**行数或点位骤降 = 交付回退**,必须停下并向上游回流,不得继续宣称完成;
|
|
55
55
|
- 上游交付不完整时,正确动作是**拒绝落地 + 回流设计侧补齐**,**不是「挑完整的那部分先落」**(只落全局主题令牌 = 假交付)。
|
|
56
56
|
|
|
57
57
|
- 每条规则带来源标注(`/* <语义 ID> · <变更条目> */`),换设计时整段可替换;
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
- E9 形态收口缺件 → `icon_only` 只落了「显图标」而没有**在同一作用域内**收口文案(作用域外的兄弟变体收口不算成对)
|
|
51
51
|
- E10 scoped 挂空 → 组件 scoped 规则依赖运行时并不存在的类(结构重构后残留,永不命中)
|
|
52
52
|
- E11 **交付完整性** → `mode=restore` 时 `covered < applicable`(欠声明)/ `gaps` 非空 / 缺口未在 `pending[]` 台账化(下游「继续」无工作项可执行)/ 自报 `registrable` 与登记表实物不符 / `notes` 的成功话术与 `coverage` 字段互相矛盾。**该项是「只换了主题色」的第一现场**:上游残缺口 + 下游挑拣落地,两头都能被它抓住
|
|
53
|
-
- E12 **设计层累积性** → 设计层文件行数或变更集点位相比历史峰值骤降(上一轮成果被重建覆盖)。本项会写 `.
|
|
53
|
+
- E12 **设计层累积性** → 设计层文件行数或变更集点位相比历史峰值骤降(上一轮成果被重建覆盖)。本项会写 `.design/ledger.json` 台账(`--no-ledger` 可关;`--changeset <path>` / `--design-layer <path>` 可指定),**台账同时是「继续」的断点续传依据**:`applicable − 已落地点数` 即剩余工作项,不再依赖自然语言回忆
|
|
54
54
|
- E13 **固定尺寸内容度量** → 设计层出现「固定 `width` 小于内容需求、且未声明收口兜底」的规则(典型:页签位 40px 里还留着标题、图标位里塞着文案)。固定宽装不下内容时文字会换行、被压缩、甚至字符重叠渲染——**「设计说 40px 就固定 40px」必然复现**。判据:盒内还有文案却用 exact 宽度又无收口
|
|
55
55
|
- E14 **列宽预算链自洽** → 列宽(含公式推出的值)扣掉**实际生效的 td 水平内边距**后装不下该列内容(操作列 = 平铺槽数 × 槽宽);或同一列宽存在两个来源且值不一致(列定义死值 vs 模板公式)。**这是「按钮文字换行 / 重叠」的第一现场**
|
|
56
56
|
- E16 **引用完整性** → 变更集 `oracle.mockup` / `coverage.registry` 指向的实物不存在(标识改名后未同步引用,全链路静默降级为无基线还原);`mode=restore` 缺顶层 `theme` 字段
|
|
@@ -45,7 +45,7 @@ if (!PROJECT || !fs.existsSync(PROJECT)) {
|
|
|
45
45
|
const CONTAINER_WIDTH = argValue("--container-width", "1200");
|
|
46
46
|
|
|
47
47
|
// ── 变更集发现(与 design-audit 同口径:多目录 + 显式 --changeset 优先)──
|
|
48
|
-
const DESIGN_DIRS = [".
|
|
48
|
+
const DESIGN_DIRS = [".design", "design", ".changeset"];
|
|
49
49
|
let changeset = argValue("--changeset") ? path.resolve(argValue("--changeset")) : null;
|
|
50
50
|
if (!changeset) {
|
|
51
51
|
const found = [];
|
|
@@ -63,7 +63,7 @@ if (!changeset) {
|
|
|
63
63
|
}
|
|
64
64
|
if (!changeset || !fs.existsSync(changeset)) {
|
|
65
65
|
console.error(`FINAL-GATE: FAIL(无变更集)`);
|
|
66
|
-
console.error(` ${PROJECT} 下未发现变更集(查找 .
|
|
66
|
+
console.error(` ${PROJECT} 下未发现变更集(查找 .design、design、.changeset)——没有交付物就没有终态。`);
|
|
67
67
|
process.exit(1);
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -138,7 +138,7 @@ for (const s of steps) {
|
|
|
138
138
|
const status = allOk ? "PASS" : "FAIL";
|
|
139
139
|
console.log(`\nFINAL-GATE: ${status}`);
|
|
140
140
|
if (allOk) {
|
|
141
|
-
console.log(` 四闸清零,交付完成。凭证已写入 .
|
|
141
|
+
console.log(` 四闸清零,交付完成。凭证已写入 .design/ledger.json(finalGate)。`);
|
|
142
142
|
} else {
|
|
143
143
|
const firstBad = steps.find((s) => !s.ok);
|
|
144
144
|
console.log(` 终态未达成(首个未清零闸:${firstBad?.name || "?"})——会话不得以完成态结尾,须继续修复后复跑。`);
|
|
@@ -146,7 +146,7 @@ if (allOk) {
|
|
|
146
146
|
|
|
147
147
|
// 凭证写入 ledger.json(不改动 design-audit 维护的轮次历史,只追加 finalGate 字段)
|
|
148
148
|
try {
|
|
149
|
-
const ledgerPath = path.join(PROJECT, ".
|
|
149
|
+
const ledgerPath = path.join(PROJECT, ".design", "ledger.json");
|
|
150
150
|
let ledger = {};
|
|
151
151
|
if (fs.existsSync(ledgerPath)) {
|
|
152
152
|
try {
|
package/scripts/check.mjs
CHANGED
|
@@ -23,10 +23,13 @@ const PYTHON_ARGS = process.platform === "win32" && !process.env.VUE3_SKILL_PYTH
|
|
|
23
23
|
/** 设计层闭环审计的目标项目:默认当前目录,可用 VUE3_AUDIT_PROJECT 指向落地项目 */
|
|
24
24
|
function resolveAuditProject() {
|
|
25
25
|
const dir = process.env.VUE3_AUDIT_PROJECT || process.cwd();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
for (const d of [".design", "design", ".changeset"]) {
|
|
27
|
+
const designDir = path.join(dir, d);
|
|
28
|
+
if (!fs.existsSync(designDir)) continue;
|
|
29
|
+
const has = fs.readdirSync(designDir).some((f) => /^changeset.*\.json$/i.test(f));
|
|
30
|
+
if (has) return dir;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const checks = [
|
|
@@ -45,7 +48,7 @@ const checks = [
|
|
|
45
48
|
desc: "变量悬空/层错位/层叠胜负/锚点挂空/列宽预算/收口断言(无落地项目则跳过)",
|
|
46
49
|
cmd: () => {
|
|
47
50
|
const project = resolveAuditProject();
|
|
48
|
-
if (!project) return "SKIPPED 未发现落地项目(.
|
|
51
|
+
if (!project) return "SKIPPED 未发现落地项目(.design/changeset*.json,亦兼容 design/ 与 .changeset/);用 VUE3_AUDIT_PROJECT=<落地项目> 指定后复跑";
|
|
49
52
|
const args = ["--project", project];
|
|
50
53
|
if (process.env.VUE3_AUDIT_CONTAINER_WIDTH) {
|
|
51
54
|
args.push("--container-width", process.env.VUE3_AUDIT_CONTAINER_WIDTH);
|
|
@@ -53,6 +56,79 @@ const checks = [
|
|
|
53
56
|
return run(NODE, "scripts/design-audit.mjs", args);
|
|
54
57
|
},
|
|
55
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: "审计闸 G3 回归",
|
|
61
|
+
desc: "结构声明兑现闸:悬空 form / 未收口通道的假项目必须被 design-audit 拒绝,带 @sid/@closed 标记的正例必须通过",
|
|
62
|
+
cmd: () => {
|
|
63
|
+
const neg = path.join(ROOT, "tests/fixtures/audit-g3-negative");
|
|
64
|
+
const pos = path.join(ROOT, "tests/fixtures/audit-g3-positive");
|
|
65
|
+
const lines = [];
|
|
66
|
+
if (fs.existsSync(neg)) {
|
|
67
|
+
let rejected = false;
|
|
68
|
+
let out = "";
|
|
69
|
+
try {
|
|
70
|
+
out = run(NODE, "scripts/design-audit.mjs", ["--project", neg, "--no-ledger"]);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
rejected = true;
|
|
73
|
+
out = `${e.stdout || ""}${e.stderr || ""}`;
|
|
74
|
+
}
|
|
75
|
+
if (!rejected) {
|
|
76
|
+
throw Object.assign(new Error("审计负例未被拒绝"), {
|
|
77
|
+
stdout: " ✗ audit-g3-negative 竟然通过 —— E15 结构声明兑现闸判据被削弱,必须修复\n",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!/\[E15\]/.test(out)) {
|
|
81
|
+
throw Object.assign(new Error("审计负例未命中 E15"), {
|
|
82
|
+
stdout: ` ✗ audit-g3-negative 被拒绝,但未命中 [E15] —— 断言已失效\n${out.slice(-800)}\n`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
lines.push("negative 被拒绝并命中 [E15]");
|
|
86
|
+
}
|
|
87
|
+
if (fs.existsSync(pos)) {
|
|
88
|
+
let out = "";
|
|
89
|
+
try {
|
|
90
|
+
out = run(NODE, "scripts/design-audit.mjs", ["--project", pos, "--no-ledger"]);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw Object.assign(new Error("审计正例被拒绝"), {
|
|
93
|
+
stdout: ` ✗ audit-g3-positive 被拒绝(E15 存在误报,必须修复):\n${(e.stdout || "") + (e.stderr || "")}\n`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
lines.push("positive 通过");
|
|
97
|
+
}
|
|
98
|
+
if (!lines.length) return "SKIPPED 未发现审计夹具(仅源码检出时可用)";
|
|
99
|
+
return ` ${lines.join(";")}`;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "模式闸唯一值回归",
|
|
104
|
+
desc: "mode 唯一合法值 restore:显式 design 的变更集必须被 parser match 拒绝",
|
|
105
|
+
cmd: () => {
|
|
106
|
+
const cs = path.join(
|
|
107
|
+
ROOT,
|
|
108
|
+
"tests/fixtures/pattern-g15-negative/.design/changeset-g15-negative.json",
|
|
109
|
+
);
|
|
110
|
+
if (!fs.existsSync(cs)) return "SKIPPED 未发现模式闸夹具(仅源码检出时可用)";
|
|
111
|
+
let rejected = false;
|
|
112
|
+
let out = "";
|
|
113
|
+
try {
|
|
114
|
+
out = run(NODE, "parser/index.js", ["match", cs]);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
rejected = true;
|
|
117
|
+
out = `${e.stdout || ""}${e.stderr || ""}`;
|
|
118
|
+
}
|
|
119
|
+
if (!rejected) {
|
|
120
|
+
throw Object.assign(new Error("模式闸负例未被拒绝"), {
|
|
121
|
+
stdout: " ✗ pattern-g15-negative 竟然通过 —— mode 唯一合法值 restore 的判据被削弱,必须修复\n",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (!/基线三件套/.test(out)) {
|
|
125
|
+
throw Object.assign(new Error("模式闸负例未命中预期判据"), {
|
|
126
|
+
stdout: ` ✗ pattern-g15-negative 被拒绝,但未命中「基线三件套」判据 —— 断言已失效\n${out.slice(-800)}\n`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return " negative 被拒绝并命中「基线三件套」判据(mode 唯一合法值 restore)";
|
|
130
|
+
},
|
|
131
|
+
},
|
|
56
132
|
];
|
|
57
133
|
|
|
58
134
|
function run(runner, script, args = []) {
|
package/scripts/design-audit.mjs
CHANGED
|
@@ -28,10 +28,11 @@
|
|
|
28
28
|
* E9 形态收口缺件 form=icon_only 只显图标、未在同一作用域内收口文案(且不被兄弟变体规则骗过)
|
|
29
29
|
* E10 scoped 挂空 组件 scoped 规则依赖运行时并不存在的类(重构残留,永不命中)
|
|
30
30
|
* E11 交付完整性 mode=restore 时 covered<applicable / gaps 非空 / 缺口未台账化 / 分母漂移 / notes 与字段矛盾
|
|
31
|
-
* E12 设计层累积性 变更集点位或设计层行数相对历史骤降(上一轮成果被覆盖)→ 写交付台账 .
|
|
31
|
+
* E12 设计层累积性 变更集点位或设计层行数相对历史骤降(上一轮成果被覆盖)→ 写交付台账 .design/ledger.json
|
|
32
32
|
* E13 固定尺寸内容度量 固定 width 小于内容需求且无收口兜底 → 文字换行/压缩/重叠
|
|
33
33
|
* E14 列宽预算链自洽 列宽(含公式)扣掉实际 td 内边距后装不下内容 → 操作列按钮被压
|
|
34
34
|
* E16 引用完整性 变更集引用的实物不存在(改名未同步)/ restore 缺顶层 theme
|
|
35
|
+
* E15 结构声明兑现 form 值 / suppress 通道名必须在词典枚举内,且设计层有 @sid / @closed 落点证据(G3 双向对账)
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
38
|
import fs from "fs";
|
|
@@ -254,9 +255,9 @@ const designRules = DESIGN_FILES.flatMap((f) =>
|
|
|
254
255
|
const PRIMARY_LAYER = path.resolve(PROJECT, argValue("--design-layer", "src/styles/design-shell.scss"));
|
|
255
256
|
const isPrimary = (file) => path.resolve(file) === PRIMARY_LAYER;
|
|
256
257
|
|
|
257
|
-
//
|
|
258
|
-
// 硬编码单一目录的代价是「变更集搬家 →
|
|
259
|
-
const CHANGESET_DIRS = [".
|
|
258
|
+
// 变更集发现:规范落点 .design/,兼容 design/ 与 .changeset/。
|
|
259
|
+
// 硬编码单一目录的代价是「变更集搬家 → 闸门静默跳过」。
|
|
260
|
+
const CHANGESET_DIRS = [".design", "design", ".changeset"];
|
|
260
261
|
const relp = (p) => path.relative(PROJECT, p).replace(/\\/g, "/");
|
|
261
262
|
const changesetCandidates = [];
|
|
262
263
|
{
|
|
@@ -281,7 +282,7 @@ let changesetFile = null;
|
|
|
281
282
|
if (changesetCandidates.length > 1) {
|
|
282
283
|
warn(
|
|
283
284
|
"E11",
|
|
284
|
-
`多份变更集并存:${changesetCandidates.map(relp).join("、")} —— 无法判定哪份是「本轮」交付物,暂取最近修改的一份。落点应唯一为 .
|
|
285
|
+
`多份变更集并存:${changesetCandidates.map(relp).join("、")} —— 无法判定哪份是「本轮」交付物,暂取最近修改的一份。落点应唯一为 .design/ 并在交付台账登记。`,
|
|
285
286
|
);
|
|
286
287
|
}
|
|
287
288
|
if (changesetCandidates.length) {
|
|
@@ -680,7 +681,7 @@ function matchArrayLiteral(text, anchorRe) {
|
|
|
680
681
|
// ── E6 visible:false 收口断言 ──────────────────────────────
|
|
681
682
|
(function checkE6() {
|
|
682
683
|
if (!changeset) {
|
|
683
|
-
info("E6", "未发现变更集(.
|
|
684
|
+
info("E6", "未发现变更集(.design/changeset*.json),跳过收口断言");
|
|
684
685
|
return;
|
|
685
686
|
}
|
|
686
687
|
const changes = changeset.changes || [];
|
|
@@ -826,6 +827,195 @@ const lastClassOutsideNot = (sel) => {
|
|
|
826
827
|
}
|
|
827
828
|
})();
|
|
828
829
|
|
|
830
|
+
// ── E15 结构声明兑现闸(G3:@sid 强制 + form/suppress 双向对账)──
|
|
831
|
+
// 结构性声明(form / suppress / suppress_inner / structure_ops / text_visible:none /
|
|
832
|
+
// visible:false)不能停留在台账上——项目侧必须有可指认的落点证据。实证缺口
|
|
833
|
+
// (2026-09-24 孔雀绿轮):collapse_trigger.form=header_inline 声明了,但词典 forms
|
|
834
|
+
// 只有 bottom_bar / center_edge —— 壳层零规则,落地端静默无效,而 E6/E8/E9 的
|
|
835
|
+
// 特判窗口都盖不住它。
|
|
836
|
+
// 证据通道(按优先级):
|
|
837
|
+
// ① /* @sid: <语义ID> */ —— 逐条落点标记(@sid 强制的机读载体)
|
|
838
|
+
// ② /* @closed: <通道名> */ —— suppress 收口证据(与 @sid 同构)
|
|
839
|
+
// ③ 通道为 css_vars 键时,设计层对该 var 写入关闭值(transparent / none / hidden / 0)
|
|
840
|
+
// ④ visible:false 的 display:none 收口规则(E6 同源)
|
|
841
|
+
// 判定:
|
|
842
|
+
// · form 值不在词典 forms 枚举内 / suppress 通道名不是 forms∪css_vars∪custom_style_map
|
|
843
|
+
// 的键 → ERROR(声明悬空 = 项目侧无规则可挂)
|
|
844
|
+
// · suppress_rules[form] 与 suppress[] 要求的通道无任何兑现证据 → ERROR
|
|
845
|
+
// · 有落点证据但缺 @sid → WARN(补标记使逐条对账可机读)
|
|
846
|
+
// · 出厂默认形态且无其他结构载荷 → WARN(保持现状无需规则,但仍需标记)
|
|
847
|
+
// · 反向:设计层 @closed 了变更集未声明的通道 → WARN(台账失真,收口记录进不了 D13 继承链)
|
|
848
|
+
const CLOSING_VALUE_RE = /^(transparent|none|hidden|0(?:px)?)(\s|$)/i;
|
|
849
|
+
const markedSids = new Set([...designCss.matchAll(/@sid\s*:\s*([\w.$-]+)/g)].map((m) => m[1]));
|
|
850
|
+
const closedMarkers = new Set([...designCss.matchAll(/@closed\s*:\s*([\w.-]+)/g)].map((m) => m[1]));
|
|
851
|
+
|
|
852
|
+
/** 与 parser extractSuppressRules 同源:结构化 suppress_rules 优先,forms 描述「suppress 必含」兜底 */
|
|
853
|
+
function extractSuppressRulesAudit(entry) {
|
|
854
|
+
const rules = {};
|
|
855
|
+
if (entry && typeof entry.suppress_rules === "object" && entry.suppress_rules !== null) {
|
|
856
|
+
for (const [form, channels] of Object.entries(entry.suppress_rules)) {
|
|
857
|
+
rules[form] = Array.isArray(channels) ? channels.filter((c) => typeof c === "string") : [];
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
const forms = entry?.forms;
|
|
861
|
+
if (forms && typeof forms === "object" && !Array.isArray(forms)) {
|
|
862
|
+
for (const [form, desc] of Object.entries(forms)) {
|
|
863
|
+
if (typeof desc !== "string") continue;
|
|
864
|
+
for (const m of desc.matchAll(/suppress\s*必含\s*([a-z][a-z0-9_]*(?:[、,/][a-z][a-z0-9_]*)*)/gi)) {
|
|
865
|
+
const channels = m[1].split(/[、,/]/).map((s) => s.trim()).filter(Boolean);
|
|
866
|
+
rules[form] = [...new Set([...(rules[form] || []), ...channels])];
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return rules;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const channelNamesOf = (entry) =>
|
|
874
|
+
new Set([
|
|
875
|
+
...Object.keys(entry?.forms || {}),
|
|
876
|
+
...Object.keys(entry?.css_vars || {}),
|
|
877
|
+
...Object.keys(entry?.custom_style_map || {}),
|
|
878
|
+
]);
|
|
879
|
+
|
|
880
|
+
/** 通道收口证据:@closed 标记 或(css_vars 键)关闭值覆写 */
|
|
881
|
+
function channelEvidence(entry, ch) {
|
|
882
|
+
if (closedMarkers.has(ch)) return `@closed:${ch}`;
|
|
883
|
+
const varName = entry?.css_vars?.[ch];
|
|
884
|
+
if (varName) {
|
|
885
|
+
const re = new RegExp(`${varName}\\s*:\\s*([^;\\n}]+)`, "i");
|
|
886
|
+
const m = re.exec(designCss);
|
|
887
|
+
if (m && CLOSING_VALUE_RE.test(m[1].trim())) return `${varName} 关闭值覆写`;
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** visible:false 落点证据(E6 同源口径) */
|
|
893
|
+
function visibleClosureRule(change) {
|
|
894
|
+
const entry = dictSidMap.get(change.id);
|
|
895
|
+
const target = classTokens(entry?.selector || "").slice(-1)[0];
|
|
896
|
+
if (!target) return null;
|
|
897
|
+
return (
|
|
898
|
+
designRules.find((r) => r.selector.includes(`.${target}`) && /none/i.test(r.decls.display || "")) || null
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
(function checkE15() {
|
|
903
|
+
if (!changeset) {
|
|
904
|
+
info("E15", "未发现变更集,跳过结构声明兑现对账");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const isStructural = (c) =>
|
|
908
|
+
typeof c?.form === "string" ||
|
|
909
|
+
(Array.isArray(c?.suppress) && c.suppress.length > 0) ||
|
|
910
|
+
(typeof c?.suppress_inner === "string" && c.suppress_inner.trim()) ||
|
|
911
|
+
(Array.isArray(c?.structure_ops) && c.structure_ops.length > 0) ||
|
|
912
|
+
c?.style?.text_visible === "none" ||
|
|
913
|
+
c?.visible === false;
|
|
914
|
+
|
|
915
|
+
const changes = changeset.changes || [];
|
|
916
|
+
for (const change of changes) {
|
|
917
|
+
if (!isStructural(change)) continue;
|
|
918
|
+
const entry = dictSidMap.get(change.id);
|
|
919
|
+
if (!entry) {
|
|
920
|
+
warn("E15", `${change.id} 声明结构语义(form/suppress 等),但词典无该条目 —— 项目侧无挂载点,无法兑现(match 会拦截)。`);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ① form 值真实性:悬空 form 值 = 壳层零规则(header_inline 实证)
|
|
925
|
+
if (typeof change.form === "string" && change.form) {
|
|
926
|
+
const forms = entry.forms && typeof entry.forms === "object" ? Object.keys(entry.forms) : [];
|
|
927
|
+
const rules = entry.suppress_rules || {};
|
|
928
|
+
if (forms.length && !forms.includes(change.form) && !(change.form in rules)) {
|
|
929
|
+
err(
|
|
930
|
+
"E15",
|
|
931
|
+
`${change.id} 的 form="${change.form}" 不在词典 forms 枚举 [${forms.join("、")}] 内 —— 项目侧无规则可挂(壳层零规则),声明悬空。形态值只能取词典已支持的枚举;确需新形态先走 pending 补词典。`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ② suppress 通道名真实性(「声明 → 词典」对账)
|
|
937
|
+
const declared = Array.isArray(change.suppress) ? change.suppress : [];
|
|
938
|
+
const known = channelNamesOf(entry);
|
|
939
|
+
const unknown = declared.filter((ch) => !known.has(ch));
|
|
940
|
+
if (unknown.length) {
|
|
941
|
+
err(
|
|
942
|
+
"E15",
|
|
943
|
+
`${change.id}.suppress 引用了词典不认识的通道 [${unknown.join("、")}](可用通道:${[...known].join("、")})—— 通道名以该条目 forms/css_vars/custom_style_map 的键为准。`,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ③ suppress_rules[form] 与已声明通道的兑现证据(「声明 → 设计层」对账)
|
|
948
|
+
const required = extractSuppressRulesAudit(entry)[change.form] || [];
|
|
949
|
+
const demanded = [...new Set([...required, ...declared])];
|
|
950
|
+
let allClosed = true;
|
|
951
|
+
for (const ch of demanded) {
|
|
952
|
+
const ev = channelEvidence(entry, ch);
|
|
953
|
+
if (!ev) {
|
|
954
|
+
allClosed = false;
|
|
955
|
+
const varHint = entry.css_vars?.[ch]
|
|
956
|
+
? `,或(通道为 css_vars 键)在设计层覆写 ${entry.css_vars[ch]} 为关闭值(transparent / none / 0)`
|
|
957
|
+
: "";
|
|
958
|
+
err(
|
|
959
|
+
"E15",
|
|
960
|
+
`${change.id} 要求收口通道「${ch}」,但设计层无任何兑现证据 —— 需要 /* @closed: ${ch} */ 标记${varHint}。声明了不收口 = 出厂旧装饰残留(双竖条并存类症状)。`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ④ @sid 强制(零证据 = 壳层零规则;有证据缺标记 = 降级 WARN 促标记)
|
|
966
|
+
const hasSid = markedSids.has(change.id);
|
|
967
|
+
if (!hasSid) {
|
|
968
|
+
const visRule = change.visible === false ? visibleClosureRule(change) : null;
|
|
969
|
+
if (change.visible === false && !visRule) {
|
|
970
|
+
// E6 已就同一根因报 ERROR,此处不重复
|
|
971
|
+
} else if (demanded.length && allClosed) {
|
|
972
|
+
warn("E15", `${change.id} 的收口已有落点证据,但缺 /* @sid: ${change.id} */ 标记 —— 补标记使「变更集 ↔ 设计层」逐条对账可机读(G3)。`);
|
|
973
|
+
} else if (change.visible === false && visRule) {
|
|
974
|
+
warn("E15", `${change.id} 的 display:none 收口已落(E6 ✓),但缺 /* @sid: ${change.id} */ 标记 —— 补标记使逐条对账可机读。`);
|
|
975
|
+
} else {
|
|
976
|
+
const forms = entry.forms && typeof entry.forms === "object" ? entry.forms : {};
|
|
977
|
+
const isDefaultForm = typeof change.form === "string" && /出厂默认|默认形态|default/i.test(String(forms[change.form] || ""));
|
|
978
|
+
const hasOps = Array.isArray(change.structure_ops) && change.structure_ops.length > 0;
|
|
979
|
+
const hasInner = typeof change.suppress_inner === "string" && change.suppress_inner.trim().length > 0;
|
|
980
|
+
const hasTextNone = change.style?.text_visible === "none";
|
|
981
|
+
const otherPayload = hasOps || hasInner || hasTextNone || demanded.length > 0;
|
|
982
|
+
if (isDefaultForm && !otherPayload) {
|
|
983
|
+
warn("E15", `${change.id} 的 form="${change.form}" 是出厂默认形态(无需新增规则),但缺 /* @sid: ${change.id} */ 标记 —— 建议补标记。`);
|
|
984
|
+
} else if (hasOps && !otherPayload) {
|
|
985
|
+
warn("E15", `${change.id} 的 structure_ops 由 parser G2 施工证据闸取证;设计层建议补 /* @sid: ${change.id} */ 标记。`);
|
|
986
|
+
} else {
|
|
987
|
+
err(
|
|
988
|
+
"E15",
|
|
989
|
+
`${change.id} 的结构性声明在设计层零落点证据(无 @sid 标记、无 @closed 兑现标记、无收口规则)—— 壳层零规则,声明悬空(例:collapse_trigger.form=header_inline 声明后壳层无任何规则)。每条结构性声明必须在设计层留机读标记:/* @sid: <语义ID> */。`,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ⑤ 反向对账:设计层收口了变更集未声明的通道 → 台账失真
|
|
997
|
+
for (const ch of closedMarkers) {
|
|
998
|
+
const owners = changes
|
|
999
|
+
.map((c) => ({ c, e: dictSidMap.get(c?.id) }))
|
|
1000
|
+
.filter(({ e }) => e && channelNamesOf(e).has(ch));
|
|
1001
|
+
if (!owners.length) continue; // 不属于本变更集涉及的点位(历史层),不判
|
|
1002
|
+
const accounted = owners.some(
|
|
1003
|
+
({ c, e }) =>
|
|
1004
|
+
(Array.isArray(c.suppress) ? c.suppress.includes(ch) : false) ||
|
|
1005
|
+
(extractSuppressRulesAudit(e)[c.form] || []).includes(ch),
|
|
1006
|
+
);
|
|
1007
|
+
if (!accounted) {
|
|
1008
|
+
warn(
|
|
1009
|
+
"E15",
|
|
1010
|
+
`设计层 /* @closed: ${ch} */ 收口了通道,但变更集未声明该收口(涉及点位:${owners.map((o) => o.c.id).join("、")})—— 台账失真:收口记录进不了 D13 继承链,下轮漏抄即出厂形态复现。`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (!errors.some((e) => e.id === "E15") && !warnings.some((w) => w.id === "E15")) {
|
|
1015
|
+
info("E15", "结构声明兑现:form/suppress 全部落在词典已知通道且设计层有落点证据 ✓");
|
|
1016
|
+
}
|
|
1017
|
+
})();
|
|
1018
|
+
|
|
829
1019
|
// ── @sid 标记覆盖率(可选增强) ─────────────────────────────
|
|
830
1020
|
(function checkSidCoverage() {
|
|
831
1021
|
const marked = new Set([...designCss.matchAll(/@sid\s*:\s*([\w.$-]+)/g)].map((m) => m[1]));
|
|
@@ -852,7 +1042,7 @@ const lastClassOutsideNot = (sel) => {
|
|
|
852
1042
|
);
|
|
853
1043
|
return;
|
|
854
1044
|
}
|
|
855
|
-
const mode = changeset.mode
|
|
1045
|
+
const mode = changeset.mode;
|
|
856
1046
|
const cov = changeset.coverage;
|
|
857
1047
|
const gaps = (cov && cov.gaps) || [];
|
|
858
1048
|
const pending = changeset.pending || [];
|
|
@@ -860,7 +1050,11 @@ const lastClassOutsideNot = (sel) => {
|
|
|
860
1050
|
const notesText = JSON.stringify(changeset.notes || []);
|
|
861
1051
|
|
|
862
1052
|
if (mode !== "restore") {
|
|
863
|
-
|
|
1053
|
+
err(
|
|
1054
|
+
"E11",
|
|
1055
|
+
`mode 必须为 "restore"(实为 ${JSON.stringify(changeset.mode ?? null)}):` +
|
|
1056
|
+
`还原任务的目标是把设计 100% 落地到项目,环境受限 / 覆盖不足 / 意图不明时唯一正确动作是停下向用户多轮询问确认,不得缩水交付。`,
|
|
1057
|
+
);
|
|
864
1058
|
return;
|
|
865
1059
|
}
|
|
866
1060
|
if (!cov) {
|
|
@@ -882,7 +1076,7 @@ const lastClassOutsideNot = (sel) => {
|
|
|
882
1076
|
if (!pending.length) {
|
|
883
1077
|
err(
|
|
884
1078
|
"E11",
|
|
885
|
-
`缺口未台账化:gaps 有 ${gaps.length} 条理由,但 pending[] 为空 ——
|
|
1079
|
+
`缺口未台账化:gaps 有 ${gaps.length} 条理由,但 pending[] 为空 —— 缺口没有变成可执行工作项,所以「继续」时无任何东西可做,复跑也不可能收敛(症状:多轮继续后效果不变)。`,
|
|
886
1080
|
);
|
|
887
1081
|
} else {
|
|
888
1082
|
const declared = new Set(changes.map((c) => c.id));
|
|
@@ -904,7 +1098,7 @@ const lastClassOutsideNot = (sel) => {
|
|
|
904
1098
|
const reg = JSON.parse(fs.readFileSync(real, "utf-8"));
|
|
905
1099
|
const items = reg.items || reg.registry || [];
|
|
906
1100
|
if (Array.isArray(items) && items.length && items.length !== R) {
|
|
907
|
-
err("E11", `登记表分母漂移:coverage.registrable=${R},登记表实物为 ${items.length} 条 ——
|
|
1101
|
+
err("E11", `登记表分母漂移:coverage.registrable=${R},登记表实物为 ${items.length} 条 —— 自报数与实物不一致。分母错则该报的欠声明数也不可信。`);
|
|
908
1102
|
} else if (Array.isArray(items) && items.length) {
|
|
909
1103
|
info("E11", `登记表分母对账:registrable=${R} = 实物 ${items.length} 条 ✓`);
|
|
910
1104
|
}
|
|
@@ -931,7 +1125,7 @@ const lastClassOutsideNot = (sel) => {
|
|
|
931
1125
|
// ── E12 设计层累积性(跨轮回退检测 + 交付台账) ──────────────
|
|
932
1126
|
// 「继续一次处理不完、继续了也没变化」的根治:把每轮落地状态落成机器可读台账,
|
|
933
1127
|
// 使「本轮是否把上一轮成果覆盖掉」可被断言,而不是靠人记得。
|
|
934
|
-
const LEDGER_PATH = path.join(PROJECT, ".
|
|
1128
|
+
const LEDGER_PATH = path.join(PROJECT, ".design", "ledger.json");
|
|
935
1129
|
const WRITE_LEDGER = !process.argv.includes("--no-ledger");
|
|
936
1130
|
(function checkE12() {
|
|
937
1131
|
const primaryText = srcTextByFile.get(PRIMARY_LAYER) ?? readIf(PRIMARY_LAYER) ?? "";
|
|
@@ -941,7 +1135,7 @@ const WRITE_LEDGER = !process.argv.includes("--no-ledger");
|
|
|
941
1135
|
const cur = {
|
|
942
1136
|
at: new Date().toISOString(),
|
|
943
1137
|
changeset: changesetFile ? relp(changesetFile) : null,
|
|
944
|
-
mode: changeset ? changeset.mode
|
|
1138
|
+
mode: changeset ? changeset.mode ?? null : null,
|
|
945
1139
|
covered: cov ? cov.covered : null,
|
|
946
1140
|
applicable: cov ? cov.applicable : null,
|
|
947
1141
|
design_shell_lines: primaryLines,
|
|
@@ -967,7 +1161,7 @@ const WRITE_LEDGER = !process.argv.includes("--no-ledger");
|
|
|
967
1161
|
if (peakLines && primaryLines && primaryLines < peakLines * 0.6) {
|
|
968
1162
|
err(
|
|
969
1163
|
"E12",
|
|
970
|
-
`交付回退:设计层 ${argValue("--design-layer", "src/styles/design-shell.scss")} 行数 ${peakLines} → ${primaryLines}(骤降 >40%)——
|
|
1164
|
+
`交付回退:设计层 ${argValue("--design-layer", "src/styles/design-shell.scss")} 行数 ${peakLines} → ${primaryLines}(骤降 >40%)—— 上一轮已落地的规则被覆盖/丢失。设计层是累积产物,禁止按「出厂空文件」重建;重建必须合并既有规则(否则用户侧表现为「布局样式全不对,只剩主题色」)。`,
|
|
971
1165
|
);
|
|
972
1166
|
}
|
|
973
1167
|
const priorSids = new Set(priorRounds.flatMap((r) => r.applied_sids || []));
|
|
@@ -1208,7 +1402,7 @@ const MIN_ANY_BOX = 24;
|
|
|
1208
1402
|
}
|
|
1209
1403
|
// restore 模式的顶层 theme 字段:缺失会让「本次主题」回落到 global.brand_primary 反推,
|
|
1210
1404
|
// 重新引入「换主题 = 换读取路径」的分叉
|
|
1211
|
-
if (
|
|
1405
|
+
if (changeset.mode === "restore" && !changeset.theme) {
|
|
1212
1406
|
warn(
|
|
1213
1407
|
"E16",
|
|
1214
1408
|
"变更集 mode=restore 但缺顶层 `theme` 字段 —— 本次主题只能回落按 `global.brand_primary` 反推(旧口径)。补 `theme` 可让「本次主题」与「基线图色板」两个身份各自可机读、互不推断。",
|