@xulthekl/team-flow 0.53.0 → 0.55.0

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.
Files changed (75) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/CHANGELOG.md +133 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +1 -1
  11. package/README.md +1 -1
  12. package/agents/prototype-builder.md +6 -5
  13. package/agents/prototype-env-scout.md +7 -7
  14. package/agents/release-archivist.md +1 -0
  15. package/dist/parsing/requirement-blocks.d.ts +26 -0
  16. package/dist/parsing/requirement-blocks.js +33 -5
  17. package/dist/validation/validator.js +8 -1
  18. package/docs/README_en.md +1 -1
  19. package/gemini-extension.json +1 -1
  20. package/hooks/session-start +19 -2
  21. package/llms.txt +1 -1
  22. package/package.json +1 -1
  23. package/plugin.json +2 -2
  24. package/scripts/check-project-config.mjs +84 -0
  25. package/scripts/design-system-clone.mjs +150 -0
  26. package/scripts/design-system-import.mjs +326 -0
  27. package/scripts/gen-primer.mjs +195 -0
  28. package/scripts/guard/checks/tasks-complete.mjs +9 -4
  29. package/scripts/guard/design-token-guard.mjs +317 -77
  30. package/scripts/infer-workflow.mjs +10 -1
  31. package/scripts/lib/arch-merge.mjs +20 -6
  32. package/scripts/lib/arch-parse.mjs +5 -11
  33. package/scripts/lib/ds-inputs.mjs +125 -0
  34. package/scripts/lib/ds-parse.mjs +236 -0
  35. package/scripts/lib/execution-recommendation.mjs +10 -1
  36. package/scripts/lib/glaf4-delegation.mjs +14 -3
  37. package/scripts/lib/hash.mjs +18 -2
  38. package/scripts/lib/md-normalize.mjs +108 -0
  39. package/scripts/lib/prototype-sync.mjs +19 -1
  40. package/scripts/lib/sdd-overlay.mjs +15 -3
  41. package/scripts/lib/solutions-promote.mjs +11 -4
  42. package/scripts/lib/spec-merge.mjs +46 -11
  43. package/scripts/lib/state-loader.mjs +4 -1
  44. package/scripts/token-extract.mjs +349 -0
  45. package/skills/design-system/SKILL.md +78 -9
  46. package/skills/design-system/references/agents/design-system-architect.md +67 -17
  47. package/skills/design-system/references/creation-flow.md +56 -5
  48. package/skills/design-system/references/creation-modes.md +171 -0
  49. package/skills/design-system/references/showcase-board-b-end.md +92 -0
  50. package/skills/design-system/references/showcase-board-c-end.md +115 -0
  51. package/skills/design-system/references/token-derivation.md +34 -9
  52. package/skills/design-system/references/variant-schema.md +35 -4
  53. package/skills/prototype/SKILL.md +18 -8
  54. package/skills/prototype/references/builder-methodology.md +72 -8
  55. package/skills/prototype/references/craft/anti-ai-slop.md +1 -1
  56. package/skills/prototype/references/craft/state-coverage.md +8 -2
  57. package/skills/prototype/references/layouts.md +10 -0
  58. package/skills/prototype/references/orchestration-flow.md +20 -3
  59. package/skills/prototype/references/prototype-scaffold/assets/design-tokens.css +2 -2
  60. package/skills/prototype/references/template.html +10 -10
  61. package/skills/release-archivist/SKILL.md +10 -3
  62. package/skills/release-archivist/references/closing-procedures.md +10 -0
  63. package/skills/workflow-bootstrap/SKILL.md +24 -2
  64. package/src/parsing/requirement-blocks.ts +34 -5
  65. package/src/validation/validator.ts +8 -1
  66. package/templates/design-systems/references/claude.md +315 -0
  67. package/templates/design-systems/references/linear-app.md +370 -0
  68. package/templates/design-systems/references/notion.md +312 -0
  69. package/templates/design-systems/references/posthog.md +259 -0
  70. package/templates/design-systems/references/sentry.md +265 -0
  71. package/templates/design-systems/references/stripe.md +325 -0
  72. package/templates/design-systems/references/supabase.md +258 -0
  73. package/templates/design-systems/references/vercel.md +313 -0
  74. package/templates/design-systems/registry.json +75 -0
  75. package/templates/design-systems/styles.json +576 -0
@@ -1,27 +1,60 @@
1
1
  #!/usr/bin/env node
2
- // design-token-guard.mjs — 设计系统完整性检查(v0.18.0)
2
+ // design-token-guard.mjs — 设计系统完整性检查(v0.55.0)
3
3
  //
4
- // Validates that a design-system.md is structurally complete and that a
5
- // design-tokens.css provides the token coverage the prototype skill relies on
6
- // (A1 identity tokens, A2 derived state tokens, B-slot alias tokens).
4
+ // 两部分(v0.54.0 新增六层审计;设计 §4.1.5):
5
+ // A. 硬校验(失败 exit 1):9 段 schema / palette 5 方向 / A1 identity / A2 color-mix / B-slot 别名
6
+ // B. 六层审计报告(advisory,恒 exit 0):L0 原则治理 / L0 可访问性 / L1 Token / L1 双主题 /
7
+ // L2 组件契约 / L3 业务模式 / L4 页面范式 / L5 Primer
7
8
  //
8
- // Usage: node scripts/guard/design-token-guard.mjs <design-system.md path> [design-tokens.css path]
9
- // Exit 0: PASS — all required checks satisfied
10
- // Exit 1: FAIL — one or more issues found (listed in the report)
11
-
12
- import { readFileSync, existsSync } from 'node:fs';
9
+ // v0.54.0 新增:
10
+ // - 组件契约表检查(§4.1.1):组件数三档(<10 FAIL 标签 / 10-14 WARN / ≥15 PASS)
11
+ // + 分组 states 规则(交互 ≥3 / 轻量 ≥2 / 豁免跳过)+ variants 列非空(仅交互/轻量)
12
+ // - governance.contract 标记:v1 → 不达标标 FAIL 标签;legacy/无标记 → 降级 WARN
13
+ // - --strict:legacy 系统也按 v1 标签输出(不改变 exit code,供存量自查)
14
+ // - --json:结构化输出(供测试与工具消费)
15
+ //
16
+ // exit 语义(v1.5 设计定案):硬校验失败 → exit 1;六层报告的 WARN/FAIL 标签一律不阻断(exit 0)
17
+ //
18
+ // 输入(v0.54.0 修正):接受**设计系统目录**或单个 md——
19
+ // - 目录 / base.md → 若 base 自身不含全 9 段(拆分布局),自动合并端变体后再断言 9 段
20
+ // - 单文件(已含全 9 段,如转换器产物)→ 原样断言
21
+ // - --variant <name> 限定合并哪个端变体
22
+ //
23
+ // Usage: node scripts/guard/design-token-guard.mjs <design-system.md | dir> [design-tokens.css path] [--variant <name>] [--strict] [--json]
24
+ //
25
+ // ⚠ 前瞻风险(v0.54.0 记录):本脚本按插件内路径(${CLAUDE_PLUGIN_ROOT}/scripts/…)调用。
26
+ // design-system skill 目前不在 runtime-skill 名单(`tests/lib/platform-runtime-distribution.test.mjs`),
27
+ // 但它是被 prototype 在运行期调用的——若平台策略将其收进名单,调用方式需整体改造。
28
+
29
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
30
+ import { dirname, join, basename, sep } from 'node:path';
31
+ import {
32
+ extractH2Headers,
33
+ hasSection,
34
+ sectionBody,
35
+ sectionBodyRaw,
36
+ subsectionBodyRaw,
37
+ parseComponentsTable,
38
+ parseContract,
39
+ parsePageArchetype,
40
+ contractFormatWarning,
41
+ COMPONENT_TYPE_MIN_STATES,
42
+ COMPONENT_COUNT_PASS,
43
+ COMPONENT_COUNT_WARN,
44
+ } from '../lib/ds-parse.mjs';
45
+ import {
46
+ REQUIRED_SECTIONS,
47
+ OPTIONAL_SECTIONS,
48
+ NON_SECTION_FILES,
49
+ KNOWN_VARIANT_RE,
50
+ readFileOrNull,
51
+ designSystemMds,
52
+ variantCandidates,
53
+ resolveInputs,
54
+ } from '../lib/ds-inputs.mjs';
13
55
 
14
56
  // ── Check definitions ──
15
57
 
16
- // The 9 required ## sections in design-system.md.
17
- const REQUIRED_SECTIONS = [
18
- 'color', 'typography', 'spacing', 'layout', 'components',
19
- 'motion', 'voice', 'brand', 'anti-patterns',
20
- ];
21
-
22
- // Optional ## sections — reported for visibility, never counted as failures.
23
- const OPTIONAL_SECTIONS = ['palette', 'aliases', 'extensions'];
24
-
25
58
  // The 5 palette directions checked when a palette section exists.
26
59
  const PALETTE_DIRECTIONS = ['neutral', 'primary', 'success', 'warning', 'danger'];
27
60
 
@@ -44,42 +77,6 @@ const B_SLOT_ALIASES = [
44
77
 
45
78
  // ── Helpers ──
46
79
 
47
- // Read a file as UTF-8, or return null when it does not exist.
48
- function readFileOrNull(filePath) {
49
- if (!filePath || !existsSync(filePath)) return null;
50
- return readFileSync(filePath, 'utf-8');
51
- }
52
-
53
- // Collect the lower-cased text of every level-2 ("## ") markdown header.
54
- function extractH2Headers(markdown) {
55
- return markdown
56
- .split('\n')
57
- .filter(line => /^##\s+/.test(line) && !/^###/.test(line))
58
- .map(line => line.replace(/^##\s+/, '').trim().toLowerCase());
59
- }
60
-
61
- // True when any ## header mentions the given section name.
62
- function hasSection(headers, section) {
63
- return headers.some(header => header.includes(section));
64
- }
65
-
66
- // Return the body of a section (text between its ## header and the next ##
67
- // header or EOF), lower-cased; null when the section is absent.
68
- function sectionBody(markdown, section) {
69
- const lines = markdown.split('\n');
70
- const startIdx = lines.findIndex(
71
- line => /^##\s+/.test(line) && !/^###/.test(line) &&
72
- line.replace(/^##\s+/, '').trim().toLowerCase().includes(section),
73
- );
74
- if (startIdx === -1) return null;
75
- const body = [];
76
- for (let i = startIdx + 1; i < lines.length; i++) {
77
- if (/^##\s+/.test(lines[i]) && !/^###/.test(lines[i])) break;
78
- body.push(lines[i]);
79
- }
80
- return body.join('\n').toLowerCase();
81
- }
82
-
83
80
  // Escape a token name for safe use inside a RegExp (names contain "--").
84
81
  function escapeRegExp(value) {
85
82
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -99,28 +96,62 @@ function extractVarTarget(value) {
99
96
 
100
97
  // ── Report accumulation ──
101
98
 
102
- const issues = [];
103
- function line(text = '') { console.log(text); }
99
+ const issues = []; // hard-check failures (exit 1)
100
+ const audit = []; // six-layer advisory lines
101
+ function line(text = '') { if (!JSON_OUT) console.log(text); }
104
102
  function pass(text) { line(` ✅ ${text}`); }
105
103
  function fail(text) { issues.push(text); line(` ❌ ${text}`); }
104
+ function warnLine(text) { line(` ⚠️ ${text}`); }
106
105
 
107
106
  // ── Argument parsing ──
108
107
 
109
- const mdPath = process.argv[2];
110
- const cssPath = process.argv[3];
108
+ const argv = process.argv.slice(2);
109
+ const STRICT = argv.includes('--strict');
110
+ const JSON_OUT = argv.includes('--json');
111
+ const variantIdx = argv.indexOf('--variant');
112
+ const VARIANT = variantIdx !== -1 ? argv[variantIdx + 1] : null;
113
+ const positional = argv.filter((a, i) => !a.startsWith('--') && !(variantIdx !== -1 && i === variantIdx + 1));
114
+
115
+ const mdPath = positional[0];
116
+ const cssPath = positional[1];
111
117
 
112
118
  if (!mdPath) {
113
- console.error('Usage: node scripts/guard/design-token-guard.mjs <design-system.md path> [design-tokens.css path]');
119
+ console.error('Usage: node scripts/guard/design-token-guard.mjs <design-system.md | 设计系统目录> [design-tokens.css path] [--variant <name>] [--strict] [--json]');
114
120
  process.exit(1);
115
121
  }
116
122
 
117
- const markdown = readFileOrNull(mdPath);
123
+ // ── 输入解析:已下沉至 scripts/lib/ds-inputs.mjs(v0.55.0)──
124
+ //
125
+ // 动机:gen-primer.mjs 的 digest 必须覆盖参与合并的**全部**文件(否则变体变更不判 STALE
126
+ // → prototype Step 0 gate 放行 → 原型静默用旧页面范式),它需要与 guard 同一套变体发现逻辑。
127
+ // 但 guard 是**顶层副作用模块**(顶层 argv 解析 / process.exit / 打印整份报告),
128
+ // 不能被子脚本 import(import 即先打印报告再退出)→ 纯逻辑下沉,两处 import。
129
+ // 输入规则 ①–⑤、单文件模式与 ignored 告警语义,见 ds-inputs.mjs 头注。
130
+
131
+ const { dir: dsDir, files: mdFiles, singleFile, ignored: ignoredMds } = resolveInputs(mdPath, VARIANT);
132
+ const markdown = mdFiles.length
133
+ ? mdFiles.map(f => readFileOrNull(f)).filter(t => t !== null).join('\n\n')
134
+ : null;
118
135
  const css = cssPath ? readFileOrNull(cssPath) : null;
119
136
 
120
- line('Design Token Guard v0.18.0');
137
+ line('Design Token Guard v0.55.0');
121
138
  line('==========================');
122
139
  line(`design-system.md: ${mdPath}`);
123
140
  line(`design-tokens.css: ${cssPath || 'not provided'}`);
141
+ if (mdFiles.length > 1) line(`合并输入(base + 端变体):${mdFiles.map(f => basename(f)).join(' + ')}`);
142
+ if (ignoredMds.length) {
143
+ if (singleFile) {
144
+ // v0.55.0:单文件模式原先整块跳过 → 同目录变体静默不参与判据(P1.5 一轮 C1)
145
+ line(` ⚠️ 主体已含全 9 段(单文件模式),同目录变体**未参与判据**:${ignoredMds.map(f => basename(f)).join(' / ')}`);
146
+ line(' 若该变体属于本系统 → 改走拆分布局(base.md 只留品牌层 + 端变体),或用 `--variant <name>` 显式纳入');
147
+ } else {
148
+ line(` ⚠️ 未纳入合并(非约定命名的端变体命名:\`*-end.md\` 或 \`variants/\`):${ignoredMds.map(f => basename(f)).join(' / ')}`);
149
+ line(' 若其中某个确是本系统的端变体 → 改名为 <端>-end.md 或移入 variants/,或用 `--variant <name>` 显式纳入');
150
+ }
151
+ }
152
+ else if (singleFile) line('输入模式: 单文件(主体已含全部 9 段)');
153
+ if (VARIANT) line(`variant 过滤: ${VARIANT}`);
154
+ if (STRICT) line('mode: --strict (legacy 按 v1 标签输出)');
124
155
  line();
125
156
 
126
157
  // ── File availability ──
@@ -132,7 +163,7 @@ if (cssPath && css === null) {
132
163
  fail(`design-tokens.css not found: ${cssPath}`);
133
164
  }
134
165
 
135
- // ── 9-Section Check ──
166
+ // ── 9-Section Check (hard) ──
136
167
 
137
168
  line('## 9-Section Check');
138
169
  if (markdown === null) {
@@ -143,14 +174,18 @@ if (markdown === null) {
143
174
  if (hasSection(headers, section)) pass(section);
144
175
  else fail(`${section} — MISSING`);
145
176
  }
146
- // Optional sections: report presence only, never fail.
147
177
  for (const section of OPTIONAL_SECTIONS) {
148
178
  if (hasSection(headers, section)) line(` ℹ️ ${section} (optional, present)`);
149
179
  }
180
+ // 诊断提示(common footgun):base 品牌层不含 typography/spacing/layout/motion,
181
+ // 且目录内没有可合并的端变体 → 大概率是把「端变体承载的段」漏建了。
182
+ if (mdFiles.length === 1 && !singleFile && ['typography', 'spacing', 'layout', 'motion'].every(s => !hasSection(headers, s))) {
183
+ line(' 💡 提示:本文件是 base 品牌层(不含 typography/spacing/layout/motion,由端变体承载),但同目录未见可合并的端变体(如 b-end.md / c-end.md)——拆分系统请补变体文件,或把该文件补齐为单文件系统。');
184
+ }
150
185
  }
151
186
  line();
152
187
 
153
- // ── Palette Check (5 directions) ──
188
+ // ── Palette Check (hard when present) ──
154
189
 
155
190
  line('## Palette Check (5 directions)');
156
191
  if (markdown === null) {
@@ -168,7 +203,7 @@ if (markdown === null) {
168
203
  }
169
204
  line();
170
205
 
171
- // ── A1 Identity Tokens ──
206
+ // ── A1 Identity Tokens (hard) ──
172
207
 
173
208
  line('## A1 Identity Tokens (8 required)');
174
209
  if (css === null) {
@@ -181,7 +216,7 @@ if (css === null) {
181
216
  }
182
217
  line();
183
218
 
184
- // ── A2 Derived Tokens ──
219
+ // ── A2 Derived Tokens (hard) ──
185
220
 
186
221
  line('## A2 Derived Tokens');
187
222
  if (css === null) {
@@ -200,7 +235,7 @@ if (css === null) {
200
235
  }
201
236
  line();
202
237
 
203
- // ── B-Slot Aliases ──
238
+ // ── B-Slot Aliases (hard) ──
204
239
 
205
240
  line('## B-Slot Aliases');
206
241
  if (css === null) {
@@ -213,25 +248,230 @@ if (css === null) {
213
248
  continue;
214
249
  }
215
250
  const target = extractVarTarget(value);
216
- if (target === base) {
217
- pass(`${token} → var(${target})`);
218
- } else if (target) {
219
- // Alias exists but points somewhere other than the expected base token.
220
- fail(`${token} → var(${target}) (expected var(${base}))`);
251
+ if (target === base) pass(`${token} → var(${target})`);
252
+ else if (target) fail(`${token} → var(${target}) (expected var(${base}))`);
253
+ else pass(`${token} = ${value}`);
254
+ }
255
+ }
256
+ line();
257
+
258
+ // ── 六层审计报告(advisory;v0.54.0 新增,§4.1.5;v0.55.0 三态 + L3/L4 改造)──
259
+ //
260
+ // 标签规则(v0.55.0 三态):
261
+ // `true` → ✅ / `false` → ❌(v1)或 ⚠️(legacy 降级)/ **`'warn'` → 恒 ⚠️,绕过降级逻辑**。
262
+ // `'warn'` 专用于"新增可选内容缺失":不得让既有合规系统回归 FAIL(D-15)。
263
+ // 原实现只有二值 + 按 contract 降级 → 现场 `contract: v1` 系统缺页面范式声明会打 ❌,
264
+ // 与 D-15 直接冲突;且只改明细行不改汇总行会**同一次运行出现相反标签**(P1.5 二轮实证)。
265
+ // 报告中所有标签均不阻断(不影响 exit code)。
266
+
267
+ const contract = markdown ? parseContract(markdown) : null;
268
+
269
+ // contract 值域(v0.55.0):已知 v1 / legacy;**未知值(如 v2)按最严档(等同 v1)** + WARN——
270
+ // 不再静默归入降级。未知意味着"更新的规范",从严比从宽安全。
271
+ const CONTRACT_KNOWN = ['v1', 'legacy'];
272
+ const contractUnknown = contract !== null && !CONTRACT_KNOWN.includes(contract);
273
+ if (contractUnknown) {
274
+ warnLine(`检测到未识别的 contract 取值 \`${contract}\`,按最严档(等同 v1)判定`);
275
+ }
276
+ // 格式诊断(v0.55.0):段存在 + 提到 contract + 解析不出 → 显式告警(原为静默 unset)
277
+ const contractFormatIssue = markdown ? contractFormatWarning(markdown) : null;
278
+ if (contractFormatIssue) warnLine(contractFormatIssue);
279
+
280
+ const downgrade = !STRICT && !contractUnknown && contract !== 'v1';
281
+
282
+ const labelOf = (ok) => ok ? '✅' : (downgrade ? '⚠️' : '❌');
283
+ /** 三态渲染:`'warn'` 绕过 labelOf 恒输出 ⚠️。 */
284
+ const badge = (v) => (v === 'warn' ? '⚠️' : (v === true ? '✅' : labelOf(false)));
285
+
286
+ const layerResults = {};
287
+
288
+ if (markdown === null) {
289
+ line('## 六层审计报告');
290
+ line(' ⏭️ skipped — design-system.md not found');
291
+ } else {
292
+ line('## 六层审计报告');
293
+
294
+ // L0 原则与治理
295
+ const principlesBody = sectionBodyRaw(markdown, 'principles');
296
+ const principleCount = principlesBody
297
+ ? principlesBody.split('\n').filter(l => /^\s*(\d+[.、)]|[-*])\s+\S/.test(l)).length
298
+ : 0;
299
+ const hasGovernance = sectionBodyRaw(markdown, 'governance') !== null;
300
+ const l0ok = principleCount >= 3 && hasGovernance;
301
+ layerResults['L0-原则治理'] = l0ok;
302
+ line(`L0 原则与治理 ${labelOf(l0ok)} principles(${principleCount})+governance(${hasGovernance ? 'present' : 'missing'})`);
303
+
304
+ // L0 可访问性(base.md 内声明 WCAG)
305
+ const l0a11y = /wcag/i.test(markdown);
306
+ layerResults['L0-可访问性'] = l0a11y;
307
+ line(`L0 可访问性 ${labelOf(l0a11y)} ${l0a11y ? 'WCAG 声明存在' : '缺 WCAG 声明(可从 prototype craft 层提级)'}`);
308
+
309
+ // L1 Token(复用硬校验结果)
310
+ const l1ok = issues.length === 0;
311
+ layerResults['L1-Token'] = l1ok;
312
+ line(`L1 Token ${labelOf(l1ok)} 硬校验${l1ok ? '全部通过' : `有 ${issues.length} 项失败`}`);
313
+
314
+ // L1 双主题(css 含暗色覆盖或同目录有 variants/dark.md)
315
+ const darkVariantExists = existsSync(join(dsDir, 'variants', 'dark.md'));
316
+ const cssDark = css ? /\[data-theme=["']dark["']\]|prefers-color-scheme\s*:\s*dark/i.test(css) : false;
317
+ const l1theme = darkVariantExists || cssDark;
318
+ layerResults['L1-双主题'] = l1theme;
319
+ line(`L1 双主题 ${l1theme ? '✅' : '⏭️ '} ${l1theme ? 'dark 变体或暗色覆盖存在' : '未启用(theme=light,可选)'}`);
320
+
321
+ // L2 组件契约(表格 + 三档 + 分组 states + variants 范围)
322
+ // 合并输入下按组件名去重(base 在合并顺序前 → base 行优先,端变体差异行不重复计数)
323
+ const rawTable = parseComponentsTable(markdown);
324
+ const table = rawTable && (() => {
325
+ const seen = new Map();
326
+ for (const r of rawTable.rows) if (!seen.has(r.name)) seen.set(r.name, r);
327
+ return { ...rawTable, rows: [...seen.values()] };
328
+ })();
329
+ if (!table) {
330
+ layerResults['L2-组件'] = false;
331
+ line(`L2 组件 ${labelOf(false)} 未检测到组件契约表(components 段无表格或类型列)`);
332
+ } else {
333
+ const count = table.rows.length;
334
+ const countOk = count >= COMPONENT_COUNT_PASS;
335
+ const countWarn = count >= COMPONENT_COUNT_WARN && count < COMPONENT_COUNT_PASS;
336
+ const stateViolations = table.rows.filter(r => {
337
+ const min = COMPONENT_TYPE_MIN_STATES[r.type];
338
+ return min !== null && r.statesCount < min;
339
+ });
340
+ const variantViolations = table.rows.filter(r => {
341
+ const min = COMPONENT_TYPE_MIN_STATES[r.type];
342
+ return min !== null && !r.hasVariants;
343
+ });
344
+ const l2ok = countOk && stateViolations.length === 0 && variantViolations.length === 0;
345
+ layerResults['L2-组件'] = l2ok;
346
+ if (l2ok) {
347
+ line(`L2 组件 ✅ ${count} 类契约表(PASS 档),states/variants 全部达标`);
221
348
  } else {
222
- // Defined as a literal value rather than an alias to the base token.
223
- pass(`${token} = ${value}`);
349
+ const parts = [];
350
+ if (!countOk) parts.push(countWarn ? `${count} 类(WARN 档:10-14)` : `${count} 类(低于起步线 10)`);
351
+ if (stateViolations.length) parts.push(`${stateViolations.length} 个组件 states 不足(${stateViolations.map(r => r.name).join('/')})`);
352
+ if (variantViolations.length) parts.push(`${variantViolations.length} 个组件缺 variants(${variantViolations.map(r => r.name).join('/')})`);
353
+ line(`L2 组件 ${countWarn && !stateViolations.length && !variantViolations.length ? '⚠️ ' : labelOf(false) + ' '}${parts.join(';')}`);
354
+ // WARN 档(10-14 类且其余达标)算通过(S2 验收口径);否则按降级规则标注
355
+ layerResults['L2-组件'] = countWarn && stateViolations.length === 0 && variantViolations.length === 0;
224
356
  }
225
357
  }
358
+
359
+ // ── L3 业务模式 / L4 页面范式(v0.55.0 改造,设计 §8.2.5)──
360
+ //
361
+ // 检测面 = **设计系统源文本,按端分别求值**。两个理由均为 P1.5 实证:
362
+ // ① **不再读 primer.md**:原判据 `/layouts|页面范式|页面节奏/` 对 markdown + primer 匹配,
363
+ // 而 primer 里 gen-primer 硬编码的 `## 页面范式` 段**必然命中** → L3 恒通过,
364
+ // 等价于重复断言 L5,且**用派生物测源 = 自证**。
365
+ // ② **不用合并文本**:`sectionBodyRaw` 用 `findIndex` **只取首个** `## layout`,
366
+ // 合并顺序恒为 base → b-end → c-end → **c-end 的声明永不解析**。
367
+ // `both` 是默认 target;既有纪律见 `creation-flow.md`("合并断言掩盖单端残缺")。
368
+ const endFiles = mdFiles.filter(f =>
369
+ KNOWN_VARIANT_RE.test(basename(f)) || f.includes(`${sep}variants${sep}`));
370
+ const scopes = endFiles.length >= 2
371
+ ? endFiles.map(f => ({ name: basename(f).replace(/-end\.md$/i, ''), text: readFileOrNull(f) || '' }))
372
+ : [{ name: null, text: markdown }];
373
+ const arche = scopes.map(s => ({ ...s, ...parsePageArchetype(s.text) }));
374
+
375
+ // L3 = 来源声明层(值域三值;未声明 → 'warn',**不走 FAIL** —— 新增可选内容不得让存量系统回归,D-15)
376
+ const l3vals = arche.map(a => {
377
+ if (!a.declared) return 'warn';
378
+ const s = a.source || '';
379
+ return (/^引用内置$/.test(s) || /^项目自有/.test(s) || /^同/.test(s)) ? true : 'warn';
380
+ });
381
+
382
+ // L4a = 布局基础(栅格/断点)。v0.55.0 **收紧 + 三形态**:
383
+ // 原裸子串 `/断点|breakpoint|sm|md|lg|栅格|grid/` 被 `.md` 路径文本保证命中
384
+ // (`layouts.md` ⊃ `md`,`b-end.md` 同理)→ 空检测。
385
+ // 现要求"名 + 值"配对,接受三种真实写法:
386
+ // ① 键形态:`栅格:12 列` / `断点:sm 640`
387
+ // ② 值形态:`sm 640` / `md 1024px`
388
+ // ③ **表格形态**:`| sm | 640px |` —— 现场 b-end.md 用 4 列表格声明断点,
389
+ // 只认前两种会把它判成"缺断点"(P1.5 三轮预警的假阴性,实施期实测确认)
390
+ const L4A_KEY_RE = /(栅格|断点)\s*[::]/i;
391
+ // 值形态:`sm 640` / `sm(640)` / `` `sm` `≥ 768px` ``(后接数字,容 24 字符间隔)
392
+ const L4A_VALUE_RE = /\b(xs|sm|md|lg|xl)\b[^\n]{0,24}?\d{3,4}/i;
393
+ // 表格形态:`| \`sm\` | \`≥ 768px\` |` —— 现场 b-end.md 的实际写法(反引号包裹 + ≥ 符号)
394
+ const L4A_TABLE_RE = /\|\s*`?\s*(xs|sm|md|lg|xl)\s*`?\s*\|[^|\n]*\d{3,4}/i;
395
+ const l4avals = arche.map(a => {
396
+ const body = sectionBodyRaw(a.text, 'layout');
397
+ if (body === null) return false; // 缺 layout 段是真问题(必填段)→ 走降级规则
398
+ return L4A_KEY_RE.test(body) || L4A_VALUE_RE.test(body) || L4A_TABLE_RE.test(body);
399
+ });
400
+
401
+ // L4b = 页面范式内容层:引用内置 / 同 <端> → 免检;项目自有 → **子块内**页面类型表行数 ≥3
402
+ const l4bvals = arche.map(a => {
403
+ if (!a.declared) return 'warn';
404
+ const s = a.source || '';
405
+ if (/^引用内置$/.test(s) || /^同/.test(s)) return true;
406
+ if (/^项目自有/.test(s)) return a.tableRows >= 3 ? true : 'warn';
407
+ return 'warn';
408
+ });
409
+
410
+ const agg = (vals) => vals.every(v => v === true) ? true
411
+ : (vals.some(v => v === 'warn') ? 'warn' : false);
412
+ const perEnd = (vals) => arche.map((a, i) => `${a.name} ${vals[i] === true ? '✅' : '⚠️'}`).join(' / ');
413
+
414
+ const l3 = agg(l3vals);
415
+ const l4a = agg(l4avals);
416
+ const l4b = agg(l4bvals);
417
+ const l4 = (l4a === true && l4b === true) ? true : ((l4a === 'warn' || l4b === 'warn') ? 'warn' : false);
418
+
419
+ layerResults['L3-业务模式'] = l3;
420
+ layerResults['L4-页面范式'] = l4;
421
+ if (arche.length > 1) {
422
+ line(`L3 业务模式 ${badge(l3)} ${perEnd(l3vals)}(按端求值)`);
423
+ line(`L4 页面范式 ${badge(l4)} 布局 ${perEnd(l4avals)} | 范式 ${perEnd(l4bvals)}`);
424
+ } else {
425
+ const a0 = arche[0];
426
+ line(`L3 业务模式 ${badge(l3)} ${l3 === true
427
+ ? `页面范式来源已声明(${a0.source})`
428
+ : '未声明页面范式来源 —— 可在 layout 段补 `页面范式来源:引用内置`,或经 iterate 补写(不阻断)'}`);
429
+ const rowInfo = /^项目自有/.test(a0.source || '') ? `(页面类型 ${a0.tableRows} 行)` : '';
430
+ line(`L4 页面范式 ${badge(l4)} 布局基础 ${badge(l4a)} | 页面范式内容 ${badge(l4b)}${rowInfo}`);
431
+ }
432
+
433
+ // L5 Primer(同目录 primer.md 存在)
434
+ const primerExists = existsSync(join(dsDir, 'primer.md'));
435
+ layerResults['L5-Primer'] = primerExists;
436
+ line(`L5 Primer ${labelOf(primerExists)} ${primerExists ? 'primer.md 存在' : '未生成 primer.md'}`);
437
+
438
+ line('------------------------------------------');
439
+ // v0.55.0:计数用**严格比较** `=== true`——原 `filter(Boolean)` 会把 `'warn'`(truthy)也算达标。
440
+ const vals = Object.values(layerResults);
441
+ const passed = vals.filter(v => v === true).length;
442
+ const warnCount = vals.filter(v => v === 'warn').length;
443
+ const total = vals.length;
444
+ const mark = (k, v) => {
445
+ if (k === 'L1-双主题' && !v) return '⏭️';
446
+ if (v === 'warn') return '⚠️'; // 三态:与明细行同源(否则同一次运行出现相反标签)
447
+ return v ? '✅' : (downgrade ? '⚠️' : '❌');
448
+ };
449
+ line(`合规层:${Object.entries(layerResults).map(([k, v]) => `${k} ${mark(k, v)}`).join(' | ')}`);
450
+ line(`(${passed}/${total} 层达标${warnCount ? `;${warnCount} 项待补写` : ''};contract=${contract || 'unset'}`
451
+ + `${downgrade ? ' → 不达标降级 WARN' : ''}${contractUnknown ? '(未识别取值,按最严档)' : ''};不输出总分)`);
226
452
  }
227
453
  line();
228
454
 
229
- // ── Verdict ──
455
+ // ── Verdict(exit 语义:仅硬校验决定 exit code)──
456
+
457
+ if (JSON_OUT) {
458
+ const report = {
459
+ // v0.55.0:`layers` 值域由 boolean 升为 `true | false | 'warn'` → schemaVersion 升版。
460
+ // 按端求值时(both)各层值为聚合结果,逐端明细见 stdout。
461
+ schemaVersion: '0.21.0',
462
+ hardChecks: { passed: issues.length === 0, issues },
463
+ layers: layerResults,
464
+ contract: contract || null,
465
+ contractUnknown,
466
+ strict: STRICT,
467
+ };
468
+ console.log(JSON.stringify(report, null, 2));
469
+ }
230
470
 
231
471
  if (issues.length === 0) {
232
- line('Verdict: PASS');
472
+ line('Verdict: PASS(硬校验通过;六层标签不阻断)');
233
473
  process.exit(0);
234
474
  }
235
475
 
236
- line(`Verdict: FAIL (${issues.length} issue${issues.length === 1 ? '' : 's'})`);
476
+ line(`Verdict: FAIL (${issues.length} issue${issues.length === 1 ? '' : 's'};硬校验失败)`);
237
477
  process.exit(1);
@@ -3,6 +3,7 @@
3
3
  import { existsSync, readFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { readState } from './lib/state-loader.mjs';
6
+ import { parseTaskLine } from './lib/md-normalize.mjs';
6
7
 
7
8
  const CODE_EXTS = [
8
9
  'mjs', 'js', 'ts', 'jsx', 'tsx', 'cjs',
@@ -30,8 +31,16 @@ function readText(dir, name) {
30
31
  return existsSync(p) ? readFileSync(p, 'utf-8') : '';
31
32
  }
32
33
 
34
+ /**
35
+ * tasks.md 任务行计数——**直接决定 hotfix(≤2) / tweak(≤4) / full 档位**。
36
+ *
37
+ * v0.55.0 §8.4.3 横展(FB-4,约束③ 高风险点):识别改用共享原语 `parseTaskLine`。
38
+ * 原 `/^- \[([ x])\]/gm` 的三个差异面:**只认小写 `x`**(`[X]` 漏计)、**不容缩进**、
39
+ * **不认 `*` bullet**。三者都会让 taskCount 变小 → 档位被低估(full 误判为 hotfix/tweak)。
40
+ * 差异对照见 tests/lib/checkbox-consistency.test.mjs。
41
+ */
33
42
  function countTasks(tasks) {
34
- return (tasks.match(/^- \[([ x])\]/gm) || []).length;
43
+ return tasks.split('\n').map(line => parseTaskLine(line)).filter(Boolean).length;
35
44
  }
36
45
 
37
46
  function collectFiles(text) {
@@ -207,25 +207,39 @@ function buildCurrentStateSection(aggregates) {
207
207
  * 4. 块范围 = 锚行 → 下一个块起点,块起点 ∈ {`^### change:`、`^## ` / `^# `、`^<!-- arch:`、
208
208
  * 文件末尾}。边界之外的内容(含 change 级模板残留表)由本 change 块独占,随替换清理。
209
209
  * 5. 删除死变量 `anchorRe`(`/^#{3,4}\s*.*change[:\s].*$/mi`,从未被引用)。
210
+ *
211
+ * v0.55.0 §8.4.3 横展(FB-4):锚行与日志标题容忍加粗/全角冒号等**纯格式变体**
212
+ * (`### **change:foo**`、`## **演进日志**`)。放宽前这类锚行**识别不到** →
213
+ * 走"追加到标题后"分支 → 同一 change 的块被**重复追加**(与 R11 缺陷同型的膨胀形态,
214
+ * 只是触发条件是格式变体)。提示:`change:` 关键词与 `###` 层级、change 名本身不放宽。
215
+ * 检测到同 change 锚块 >1(历史重复)时 WARN,只替换首个,不静默。
210
216
  */
217
+ const EVOLUTION_LOG_HEADING_RE = /^#{2,3}\s*\d*\.?\s*\**\s*(演进日志|Evolution Log)\**/m;
218
+
211
219
  function upsertEvolutionLog(globalContent, srcContent, changeName) {
212
220
  // 从 change 源提取演进日志条目(标题 + 正文)
213
- const logStart = srcContent.search(/^#{2,3}\s*\d*\.?\s*(演进日志|Evolution Log)/m);
221
+ const logStart = srcContent.search(EVOLUTION_LOG_HEADING_RE);
214
222
  if (logStart < 0) return globalContent;
215
223
  const logSection = srcContent.slice(logStart).trim();
216
224
  const anchor = `### change:${changeName}`;
217
- const logBlock = `${anchor}\n${logSection.replace(/^#{2,3}\s*\d*\.?\s*(演进日志|Evolution Log)/m, '').trim()}`;
225
+ const logBlock = `${anchor}\n${logSection.replace(EVOLUTION_LOG_HEADING_RE, '').trim()}`;
218
226
 
219
227
  // 已存在 → 替换整块;否则追加到演进日志段末尾
220
- const logHeadingRe = /^#{2,3}\s*\d*\.?\s*(演进日志|Evolution Log)/m;
221
- const anchorRe = new RegExp(`^### change:${escapeRegExp(changeName)}\\s*$`, 'm');
228
+ const logHeadingRe = EVOLUTION_LOG_HEADING_RE;
229
+ // 锚行:`### change:<name>` 的宽容形态(容忍 `**` 包裹、全角冒号、冒号后空格)
230
+ const anchorSrc = `^###\\s*\\**\\s*change\\s*[::]\\s*\\**\\s*${escapeRegExp(changeName)}\\s*\\**\\s*$`;
231
+ const anchorRe = new RegExp(anchorSrc, 'm');
222
232
  if (anchorRe.test(globalContent)) {
223
233
  // 块起点:下一个 change 块 / 下一个一二级标题 / arch marker / 文件末尾
224
234
  const blockRe = new RegExp(
225
- `^### change:${escapeRegExp(changeName)}\\s*$[\\s\\S]*?`
226
- + `(?=^### change:|^#{1,2}\\s|^<!-- arch:|$(?![\\s\\S]))`,
235
+ `${anchorSrc}[\\s\\S]*?`
236
+ + `(?=^###\\s*\\**\\s*change\\s*[::]|^#{1,2}\\s|^<!-- arch:|$(?![\\s\\S]))`,
227
237
  'm'
228
238
  );
239
+ const duplicates = (globalContent.match(new RegExp(anchorSrc, 'gm')) || []).length;
240
+ if (duplicates > 1) {
241
+ console.warn(` [WARN] 演进日志存在 ${duplicates} 个 change:${changeName} 锚块(历史重复追加),本次只替换第一个,请人工清理其余块`);
242
+ }
229
243
  const matched = globalContent.match(blockRe);
230
244
  if (matched) {
231
245
  // 被替换段内若含疑似人工内容(非标题/非表格行/非空行/非注释)→ 告警不静默
@@ -17,6 +17,11 @@
17
17
  // → 两重失配 → 列语义整体错位 → 全局台账被写入 `上下文=extend`、
18
18
  // `根实体=SysFrontSystem(不变)`。**这是污染而非缺失**。改判据为表头定位。
19
19
 
20
+ // v0.55.0 §8.4.3 横展(FB-4):强调剥离改为引用共享层 `md-normalize.mjs`
21
+ // (单一真相源)——本文件 v0.53.0 的实现已提升为全仓库约定,此处删除本地副本,
22
+ // 避免「两份实现各自漂移」的复发形态。
23
+ import { stripEmphasis } from './md-normalize.mjs';
24
+
20
25
  /**
21
26
  * HTTP 方法词表——**三个方法正则的唯一事实源**。
22
27
  *
@@ -54,17 +59,6 @@ const PATH_PREFIX_RE = /^\/[\w\-/{},.]*/;
54
59
  */
55
60
  export const ENDPOINT_HEADERS = new Set(['端点', 'Endpoint', 'API', '接口']);
56
61
 
57
- /** 强调标记剥离:`**x**` / `__x__` / `*x*` / `_x_` → `x`(首尾成对才剥离;长的优先)。 */
58
- function stripEmphasis(s) {
59
- let out = s;
60
- for (const [open, close] of [['**', '**'], ['__', '__'], ['*', '*'], ['_', '_']]) {
61
- if (out.length > open.length + close.length && out.startsWith(open) && out.endsWith(close)) {
62
- out = out.slice(open.length, -close.length).trim();
63
- }
64
- }
65
- return out;
66
- }
67
-
68
62
  /**
69
63
  * 解析一行 markdown 表格为单元格数组(去反引号 + 剥离强调标记 + trim)。非表格行返回 null。
70
64
  *