@xulthekl/team-flow 0.53.0 → 0.54.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 (52) 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 +71 -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 +4 -4
  14. package/agents/release-archivist.md +1 -0
  15. package/docs/README_en.md +1 -1
  16. package/gemini-extension.json +1 -1
  17. package/hooks/session-start +2 -2
  18. package/llms.txt +1 -1
  19. package/package.json +1 -1
  20. package/plugin.json +2 -2
  21. package/scripts/design-system-import.mjs +238 -0
  22. package/scripts/gen-primer.mjs +143 -0
  23. package/scripts/guard/design-token-guard.mjs +284 -63
  24. package/scripts/lib/ds-parse.mjs +124 -0
  25. package/scripts/token-extract.mjs +257 -0
  26. package/skills/design-system/SKILL.md +55 -9
  27. package/skills/design-system/references/agents/design-system-architect.md +44 -7
  28. package/skills/design-system/references/creation-flow.md +39 -5
  29. package/skills/design-system/references/showcase-board-b-end.md +78 -0
  30. package/skills/design-system/references/showcase-board-c-end.md +92 -0
  31. package/skills/design-system/references/token-derivation.md +34 -9
  32. package/skills/design-system/references/variant-schema.md +14 -3
  33. package/skills/prototype/SKILL.md +14 -8
  34. package/skills/prototype/references/builder-methodology.md +62 -5
  35. package/skills/prototype/references/craft/anti-ai-slop.md +1 -1
  36. package/skills/prototype/references/craft/state-coverage.md +8 -2
  37. package/skills/prototype/references/orchestration-flow.md +12 -3
  38. package/skills/prototype/references/prototype-scaffold/assets/design-tokens.css +2 -2
  39. package/skills/prototype/references/template.html +10 -10
  40. package/skills/release-archivist/SKILL.md +10 -3
  41. package/skills/release-archivist/references/closing-procedures.md +10 -0
  42. package/skills/workflow-bootstrap/SKILL.md +14 -2
  43. package/templates/design-systems/references/claude.md +315 -0
  44. package/templates/design-systems/references/linear-app.md +370 -0
  45. package/templates/design-systems/references/notion.md +312 -0
  46. package/templates/design-systems/references/posthog.md +259 -0
  47. package/templates/design-systems/references/sentry.md +265 -0
  48. package/templates/design-systems/references/stripe.md +325 -0
  49. package/templates/design-systems/references/supabase.md +258 -0
  50. package/templates/design-systems/references/vercel.md +313 -0
  51. package/templates/design-systems/registry.json +75 -0
  52. package/templates/design-systems/styles.json +576 -0
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // gen-primer.mjs — 从 base.md 确定性生成 primer.md(v0.54.0,设计 §4.1.2)
3
+ //
4
+ // primer 是"AI 约束入口":组件白名单 + token 速查 + 硬规则,供 prototype-builder 消费。
5
+ // 确定性:除"生成时间"一行外,输出完全由 base.md 内容决定(纯字符串拼接,零 LLM)。
6
+ //
7
+ // Usage:
8
+ // node scripts/gen-primer.mjs <base.md path> [--out <primer path>]
9
+ // 生成 primer(默认写 base.md 同目录 primer.md)
10
+ // node scripts/gen-primer.mjs <base.md path> --check
11
+ // 校验同目录 primer.md 的 digest 与 base.md 是否一致
12
+ // exit 0 = 一致;exit 2 = 过期或缺失(供 builder gate 判定)
13
+ //
14
+ // 配套测试:"生成物 == 源表"(tests/lib/gen-primer.test.mjs)
15
+
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
17
+ import { createHash } from 'node:crypto';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import {
20
+ parseComponentsTable,
21
+ parseContract,
22
+ parseA1Tokens,
23
+ parseAntiPatterns,
24
+ COMPONENT_COUNT_PASS,
25
+ COMPONENT_COUNT_WARN,
26
+ } from './lib/ds-parse.mjs';
27
+
28
+ const argv = process.argv.slice(2);
29
+ const CHECK = argv.includes('--check');
30
+ const outIdx = argv.indexOf('--out');
31
+ const outArg = outIdx !== -1 ? argv[outIdx + 1] : null;
32
+ const positional = argv.filter((a, i) => !a.startsWith('--') && !(outIdx !== -1 && i === outIdx + 1));
33
+ const basePath = positional[0];
34
+
35
+ if (!basePath) {
36
+ console.error('Usage: node scripts/gen-primer.mjs <base.md path> [--out <primer path>] [--check]');
37
+ process.exit(1);
38
+ }
39
+
40
+ const baseAbs = resolve(basePath);
41
+ if (!existsSync(baseAbs)) {
42
+ console.error(`base.md not found: ${baseAbs}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const baseContent = readFileSync(baseAbs, 'utf-8');
47
+ const digest = createHash('sha256').update(baseContent, 'utf-8').digest('hex');
48
+ const digestShort = digest.slice(0, 16);
49
+ const primerPath = outArg ? resolve(outArg) : join(dirname(baseAbs), 'primer.md');
50
+ const DIGEST_RE = /sha256:([0-9a-f]{16,})/;
51
+
52
+ // ── --check 模式:校验 primer 新鲜度(builder gate 调用)──
53
+
54
+ if (CHECK) {
55
+ if (!existsSync(primerPath)) {
56
+ console.error(`STALE: primer.md 不存在(${primerPath})——请运行 design-system iterate 生成`);
57
+ process.exit(2);
58
+ }
59
+ const primerContent = readFileSync(primerPath, 'utf-8');
60
+ const m = primerContent.match(DIGEST_RE);
61
+ if (!m) {
62
+ console.error('STALE: primer.md 无 digest 头部(格式不符)——请重新生成');
63
+ process.exit(2);
64
+ }
65
+ if (m[1] !== digestShort && !digest.startsWith(m[1])) {
66
+ console.error(`STALE: primer.md 已过期(记录 ${m[1]},当前 base.md ${digestShort})——请运行 design-system iterate 重新生成`);
67
+ process.exit(2);
68
+ }
69
+ console.log(`OK: primer.md digest 一致(${digestShort})`);
70
+ process.exit(0);
71
+ }
72
+
73
+ // ── 生成模式 ──
74
+
75
+ const table = parseComponentsTable(baseContent);
76
+ if (!table) {
77
+ console.error('base.md 的 components 段未检测到组件契约表(需 | 组件 | 类型 | variants | sizes | states | 用途 | 禁止 | 表格)。');
78
+ console.error('存量系统请先运行 design-system iterate 补全契约表。');
79
+ process.exit(1);
80
+ }
81
+
82
+ const contract = parseContract(baseContent);
83
+ const a1 = parseA1Tokens(baseContent);
84
+ const antiPatterns = parseAntiPatterns(baseContent);
85
+ const componentRows = table.rows;
86
+ const count = componentRows.length;
87
+ const countLabel = count >= COMPONENT_COUNT_PASS
88
+ ? 'PASS(完整)'
89
+ : count >= COMPONENT_COUNT_WARN
90
+ ? 'WARN(可用,建议补全)'
91
+ : 'FAIL(低于起步线)';
92
+
93
+ // 白名单行:Name(variants · sizes)
94
+ const whitelistLines = componentRows.map(r => {
95
+ const variants = r.hasVariants ? r.variants.replace(/\s*\/\s*/g, '|') : '—';
96
+ const sizes = r.sizes && r.sizes !== '—' ? r.sizes.replace(/\s*\/\s*/g, '|') : '—';
97
+ const typeMark = r.type === '豁免' ? '(展示类)' : r.type === '轻量' ? '(轻量)' : '';
98
+ return `- ${r.name}${typeMark}: variants=${variants} · sizes=${sizes}`;
99
+ });
100
+
101
+ // Token 速查行
102
+ const tokenKeys = ['--bg', '--surface', '--fg', '--muted', '--border', '--accent'];
103
+ const tokenLine = tokenKeys
104
+ .filter(k => a1[k])
105
+ .map(k => `${k} ${a1[k]}`)
106
+ .join(' / ');
107
+ const fontDisplay = a1['--font-display'] ? `\n- --font-display: ${a1['--font-display']}` : '';
108
+ const fontBody = a1['--font-body'] ? `\n- --font-body: ${a1['--font-body']}` : '';
109
+
110
+ const hardRules = [
111
+ '1. 只使用上方白名单组件与 token;若需系统外元素 → 记录为新组件需求(ds_increment),不自造',
112
+ '2. 数据展示须覆盖 Loading / Empty / Error / Populated / Edge 五状态(craft/state-coverage.md)',
113
+ ...antiPatterns.map((p, i) => `${i + 3}. ${p}`),
114
+ ];
115
+
116
+ const primer = `# AI Primer — 原型生成约束
117
+
118
+ > 本文件由 scripts/gen-primer.mjs 从 base.md 确定性生成,请勿手工编辑。
119
+ > 生成时间:${new Date().toISOString()} | 来源:base.md @ sha256:${digestShort}
120
+ > 契约表:${count} 类组件(${countLabel}) | contract=${contract || 'unset'}
121
+
122
+ ## 可用组件白名单(只用这些)
123
+
124
+ ${whitelistLines.join('\n')}
125
+
126
+ ## Token 速查
127
+
128
+ ${tokenLine || '(base.md 未内联 A1 token 值,请以 design-tokens.css 为准)'}${fontDisplay}${fontBody}
129
+
130
+ ## 硬规则
131
+
132
+ ${hardRules.join('\n')}
133
+
134
+ ## 页面范式
135
+
136
+ section 骨架与页面类型节奏见 prototype skill 的 layouts.md(reference:管理后台列表页 / 表单页 / 仪表盘 / Landing / 文档索引)。
137
+ `;
138
+
139
+ mkdirSync(dirname(primerPath), { recursive: true });
140
+ writeFileSync(primerPath, primer, 'utf-8');
141
+ console.log(`primer 已生成:${primerPath}`);
142
+ console.log(` 组件白名单:${count} 类(${countLabel})`);
143
+ console.log(` digest:sha256:${digestShort}`);
@@ -1,15 +1,44 @@
1
1
  #!/usr/bin/env node
2
- // design-token-guard.mjs — 设计系统完整性检查(v0.18.0)
2
+ // design-token-guard.mjs — 设计系统完整性检查(v0.54.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
+ parseComponentsTable,
37
+ parseContract,
38
+ COMPONENT_TYPE_MIN_STATES,
39
+ COMPONENT_COUNT_PASS,
40
+ COMPONENT_COUNT_WARN,
41
+ } from '../lib/ds-parse.mjs';
13
42
 
14
43
  // ── Check definitions ──
15
44
 
@@ -20,7 +49,7 @@ const REQUIRED_SECTIONS = [
20
49
  ];
21
50
 
22
51
  // Optional ## sections — reported for visibility, never counted as failures.
23
- const OPTIONAL_SECTIONS = ['palette', 'aliases', 'extensions'];
52
+ const OPTIONAL_SECTIONS = ['palette', 'aliases', 'extensions', 'principles', 'governance'];
24
53
 
25
54
  // The 5 palette directions checked when a palette section exists.
26
55
  const PALETTE_DIRECTIONS = ['neutral', 'primary', 'success', 'warning', 'danger'];
@@ -50,36 +79,6 @@ function readFileOrNull(filePath) {
50
79
  return readFileSync(filePath, 'utf-8');
51
80
  }
52
81
 
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
82
  // Escape a token name for safe use inside a RegExp (names contain "--").
84
83
  function escapeRegExp(value) {
85
84
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -99,28 +98,127 @@ function extractVarTarget(value) {
99
98
 
100
99
  // ── Report accumulation ──
101
100
 
102
- const issues = [];
103
- function line(text = '') { console.log(text); }
101
+ const issues = []; // hard-check failures (exit 1)
102
+ const audit = []; // six-layer advisory lines
103
+ function line(text = '') { if (!JSON_OUT) console.log(text); }
104
104
  function pass(text) { line(` ✅ ${text}`); }
105
105
  function fail(text) { issues.push(text); line(` ❌ ${text}`); }
106
+ function warnLine(text) { line(` ⚠️ ${text}`); }
106
107
 
107
108
  // ── Argument parsing ──
108
109
 
109
- const mdPath = process.argv[2];
110
- const cssPath = process.argv[3];
110
+ const argv = process.argv.slice(2);
111
+ const STRICT = argv.includes('--strict');
112
+ const JSON_OUT = argv.includes('--json');
113
+ const variantIdx = argv.indexOf('--variant');
114
+ const VARIANT = variantIdx !== -1 ? argv[variantIdx + 1] : null;
115
+ const positional = argv.filter((a, i) => !a.startsWith('--') && !(variantIdx !== -1 && i === variantIdx + 1));
116
+
117
+ const mdPath = positional[0];
118
+ const cssPath = positional[1];
111
119
 
112
120
  if (!mdPath) {
113
- console.error('Usage: node scripts/guard/design-token-guard.mjs <design-system.md path> [design-tokens.css path]');
121
+ console.error('Usage: node scripts/guard/design-token-guard.mjs <design-system.md | 设计系统目录> [design-tokens.css path] [--variant <name>] [--strict] [--json]');
114
122
  process.exit(1);
115
123
  }
116
124
 
117
- const markdown = readFileOrNull(mdPath);
125
+ // ── 输入解析:单文件系统 或 base + 端变体拆分系统 ──
126
+ //
127
+ // 权威排布是「base 品牌共享层 + 端变体」(variant-schema.md):base 不含
128
+ // typography/spacing/layout/motion(这些随端变化)。故 9 段断言必须在**合并后**文本上做——
129
+ // 只校验 base.md 会把合规系统判为 FAIL 并阻断落盘(v0.54.0 P4 实证:4 项 MISSING / exit 1)。
130
+ //
131
+ // 规则(确定性,可解释):
132
+ // ① 传入目录 → 以目录内 base.md 为主体;② 传入 base.md → 以它为主体;③ 传入变体文件 → base.md 主体 + 该文件。
133
+ // ④ 主体自身已含全部 9 段(单文件系统 / 转换器产物)→ 单文件模式,不合并(避免卷入同目录无关 md)。
134
+ // ⑤ 主体不含全 9 段 → 合并**约定命名的端变体**(`*-end.md` + `variants/` 子目录内的 md);
135
+ // `--variant <name>` 可显式纳入非约定命名的变体文件(逃生通道)。
136
+ // 含必选段但未被纳入的其它 md → 告警列出(不静默补齐:一个 notes.md 不该把残缺系统伪装成 PASS)。
137
+ // 合并按「base 在前」顺序,组件表按组件名去重(base 优先),端特有差异行不会重复计数。
138
+ const NON_SECTION_FILES = new Set(['primer.md', 'pending.md', 'readme.md', 'index.md']);
139
+
140
+ // 端变体命名约定:`*-end.md`(b-end / c-end / mobile-end …)+ `variants/` 子目录内全部 md(如 dark.md)。
141
+ // 收紧为约定集而非「同目录任意 md」——否则一个含 `## spacing` 的 notes.md 就能补齐缺失段,
142
+ // 把残缺系统伪装成 PASS(v0.54.0 P4 复验观察 B 实证)。非约定名可用 `--variant <name>` 显式纳入。
143
+ const KNOWN_VARIANT_RE = /-end\.md$/i;
144
+
145
+ /** 设计系统目录内的全部候选 md(dir 与 variants/ 两层,排除 base 与非段落文件)。 */
146
+ function designSystemMds(dir) {
147
+ const out = [];
148
+ for (const d of [dir, join(dir, 'variants')]) {
149
+ if (!existsSync(d)) continue;
150
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
151
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
152
+ if (entry.name === 'base.md' || NON_SECTION_FILES.has(entry.name.toLowerCase())) continue;
153
+ out.push(join(d, entry.name));
154
+ }
155
+ }
156
+ return out.sort();
157
+ }
158
+
159
+ function variantCandidates(dir) {
160
+ return designSystemMds(dir).filter(f => KNOWN_VARIANT_RE.test(basename(f)) || f.includes(`${sep}variants${sep}`));
161
+ }
162
+
163
+ function resolveInputs(input, variantName) {
164
+ const isDir = existsSync(input) && statSync(input).isDirectory();
165
+ const dir = isDir ? input : dirname(input);
166
+ const parts = [];
167
+
168
+ if (isDir) {
169
+ parts.push(join(dir, 'base.md'));
170
+ } else if (basename(input) === 'base.md') {
171
+ parts.push(input);
172
+ } else {
173
+ parts.push(join(dir, 'base.md')); // 传入变体文件时仍以 base 为主体(base 优先)
174
+ parts.push(input);
175
+ }
176
+
177
+ const present = parts.filter(f => existsSync(f) && readFileOrNull(f) !== null);
178
+ const subject = present[0] ? readFileOrNull(present[0]) : null;
179
+ const subjectComplete = subject !== null
180
+ && REQUIRED_SECTIONS.every(s => hasSection(extractH2Headers(subject), s));
181
+
182
+ let merged = [...present];
183
+ let ignored = [];
184
+ if (!subjectComplete) {
185
+ // --variant <name> 为显式指定:在**全部**候选 md 中按名匹配(含非约定命名),作为逃生通道
186
+ const variants = variantName
187
+ ? designSystemMds(dir).filter(f => basename(f).includes(variantName))
188
+ : variantCandidates(dir);
189
+ for (const v of variants) if (!merged.includes(v)) merged.push(v);
190
+ // 观察 B 兜底:未被纳入合并、但含必选段的非约定 md —— 显式告警(不静默补齐,也不静默忽略)
191
+ if (!variantName) {
192
+ const mergedSet = new Set(merged);
193
+ ignored = designSystemMds(dir).filter(f => {
194
+ if (mergedSet.has(f)) return false;
195
+ const text = readFileOrNull(f) || '';
196
+ const headers = extractH2Headers(text);
197
+ return REQUIRED_SECTIONS.some(s => hasSection(headers, s));
198
+ });
199
+ }
200
+ }
201
+ return { dir, files: merged, singleFile: subjectComplete, ignored };
202
+ }
203
+
204
+ const { dir: dsDir, files: mdFiles, singleFile, ignored: ignoredMds } = resolveInputs(mdPath, VARIANT);
205
+ const markdown = mdFiles.length
206
+ ? mdFiles.map(f => readFileOrNull(f)).filter(t => t !== null).join('\n\n')
207
+ : null;
118
208
  const css = cssPath ? readFileOrNull(cssPath) : null;
119
209
 
120
- line('Design Token Guard v0.18.0');
210
+ line('Design Token Guard v0.54.0');
121
211
  line('==========================');
122
212
  line(`design-system.md: ${mdPath}`);
123
213
  line(`design-tokens.css: ${cssPath || 'not provided'}`);
214
+ if (mdFiles.length > 1) line(`合并输入(base + 端变体):${mdFiles.map(f => basename(f)).join(' + ')}`);
215
+ if (ignoredMds.length) {
216
+ line(` ⚠️ 未纳入合并(非约定命名的端变体命名:\`*-end.md\` 或 \`variants/\`):${ignoredMds.map(f => basename(f)).join(' / ')}`);
217
+ line(' 若其中某个确是本系统的端变体 → 改名为 <端>-end.md 或移入 variants/,或用 `--variant <name>` 显式纳入');
218
+ }
219
+ else if (singleFile) line('输入模式: 单文件(主体已含全部 9 段)');
220
+ if (VARIANT) line(`variant 过滤: ${VARIANT}`);
221
+ if (STRICT) line('mode: --strict (legacy 按 v1 标签输出)');
124
222
  line();
125
223
 
126
224
  // ── File availability ──
@@ -132,7 +230,7 @@ if (cssPath && css === null) {
132
230
  fail(`design-tokens.css not found: ${cssPath}`);
133
231
  }
134
232
 
135
- // ── 9-Section Check ──
233
+ // ── 9-Section Check (hard) ──
136
234
 
137
235
  line('## 9-Section Check');
138
236
  if (markdown === null) {
@@ -143,14 +241,18 @@ if (markdown === null) {
143
241
  if (hasSection(headers, section)) pass(section);
144
242
  else fail(`${section} — MISSING`);
145
243
  }
146
- // Optional sections: report presence only, never fail.
147
244
  for (const section of OPTIONAL_SECTIONS) {
148
245
  if (hasSection(headers, section)) line(` ℹ️ ${section} (optional, present)`);
149
246
  }
247
+ // 诊断提示(common footgun):base 品牌层不含 typography/spacing/layout/motion,
248
+ // 且目录内没有可合并的端变体 → 大概率是把「端变体承载的段」漏建了。
249
+ if (mdFiles.length === 1 && !singleFile && ['typography', 'spacing', 'layout', 'motion'].every(s => !hasSection(headers, s))) {
250
+ line(' 💡 提示:本文件是 base 品牌层(不含 typography/spacing/layout/motion,由端变体承载),但同目录未见可合并的端变体(如 b-end.md / c-end.md)——拆分系统请补变体文件,或把该文件补齐为单文件系统。');
251
+ }
150
252
  }
151
253
  line();
152
254
 
153
- // ── Palette Check (5 directions) ──
255
+ // ── Palette Check (hard when present) ──
154
256
 
155
257
  line('## Palette Check (5 directions)');
156
258
  if (markdown === null) {
@@ -168,7 +270,7 @@ if (markdown === null) {
168
270
  }
169
271
  line();
170
272
 
171
- // ── A1 Identity Tokens ──
273
+ // ── A1 Identity Tokens (hard) ──
172
274
 
173
275
  line('## A1 Identity Tokens (8 required)');
174
276
  if (css === null) {
@@ -181,7 +283,7 @@ if (css === null) {
181
283
  }
182
284
  line();
183
285
 
184
- // ── A2 Derived Tokens ──
286
+ // ── A2 Derived Tokens (hard) ──
185
287
 
186
288
  line('## A2 Derived Tokens');
187
289
  if (css === null) {
@@ -200,7 +302,7 @@ if (css === null) {
200
302
  }
201
303
  line();
202
304
 
203
- // ── B-Slot Aliases ──
305
+ // ── B-Slot Aliases (hard) ──
204
306
 
205
307
  line('## B-Slot Aliases');
206
308
  if (css === null) {
@@ -213,25 +315,144 @@ if (css === null) {
213
315
  continue;
214
316
  }
215
317
  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}))`);
318
+ if (target === base) pass(`${token} → var(${target})`);
319
+ else if (target) fail(`${token} → var(${target}) (expected var(${base}))`);
320
+ else pass(`${token} = ${value}`);
321
+ }
322
+ }
323
+ line();
324
+
325
+ // ── 六层审计报告(advisory;v0.54.0 新增,§4.1.5)──
326
+ //
327
+ // 标签规则:contract=v1(或无标记且非 strict?见下)→ 不达标标 FAIL;
328
+ // contract=legacy/无标记 → 降级 WARN;--strict → legacy 也按 v1 标签。
329
+ // 报告中所有标签均不阻断(不影响 exit code)。
330
+
331
+ const contract = markdown ? parseContract(markdown) : null;
332
+ const downgrade = !STRICT && contract !== 'v1'; // legacy 或无标记 → 降级
333
+ const labelOf = (ok) => ok ? '✅' : (downgrade ? '⚠️' : '❌');
334
+
335
+ const layerResults = {};
336
+
337
+ if (markdown === null) {
338
+ line('## 六层审计报告');
339
+ line(' ⏭️ skipped — design-system.md not found');
340
+ } else {
341
+ line('## 六层审计报告');
342
+
343
+ // L0 原则与治理
344
+ const principlesBody = sectionBodyRaw(markdown, 'principles');
345
+ const principleCount = principlesBody
346
+ ? principlesBody.split('\n').filter(l => /^\s*(\d+[.、)]|[-*])\s+\S/.test(l)).length
347
+ : 0;
348
+ const hasGovernance = sectionBodyRaw(markdown, 'governance') !== null;
349
+ const l0ok = principleCount >= 3 && hasGovernance;
350
+ layerResults['L0-原则治理'] = l0ok;
351
+ line(`L0 原则与治理 ${labelOf(l0ok)} principles(${principleCount})+governance(${hasGovernance ? 'present' : 'missing'})`);
352
+
353
+ // L0 可访问性(base.md 内声明 WCAG)
354
+ const l0a11y = /wcag/i.test(markdown);
355
+ layerResults['L0-可访问性'] = l0a11y;
356
+ line(`L0 可访问性 ${labelOf(l0a11y)} ${l0a11y ? 'WCAG 声明存在' : '缺 WCAG 声明(可从 prototype craft 层提级)'}`);
357
+
358
+ // L1 Token(复用硬校验结果)
359
+ const l1ok = issues.length === 0;
360
+ layerResults['L1-Token'] = l1ok;
361
+ line(`L1 Token ${labelOf(l1ok)} 硬校验${l1ok ? '全部通过' : `有 ${issues.length} 项失败`}`);
362
+
363
+ // L1 双主题(css 含暗色覆盖或同目录有 variants/dark.md)
364
+ const darkVariantExists = existsSync(join(dsDir, 'variants', 'dark.md'));
365
+ const cssDark = css ? /\[data-theme=["']dark["']\]|prefers-color-scheme\s*:\s*dark/i.test(css) : false;
366
+ const l1theme = darkVariantExists || cssDark;
367
+ layerResults['L1-双主题'] = l1theme;
368
+ line(`L1 双主题 ${l1theme ? '✅' : '⏭️ '} ${l1theme ? 'dark 变体或暗色覆盖存在' : '未启用(theme=light,可选)'}`);
369
+
370
+ // L2 组件契约(表格 + 三档 + 分组 states + variants 范围)
371
+ // 合并输入下按组件名去重(base 在合并顺序前 → base 行优先,端变体差异行不重复计数)
372
+ const rawTable = parseComponentsTable(markdown);
373
+ const table = rawTable && (() => {
374
+ const seen = new Map();
375
+ for (const r of rawTable.rows) if (!seen.has(r.name)) seen.set(r.name, r);
376
+ return { ...rawTable, rows: [...seen.values()] };
377
+ })();
378
+ if (!table) {
379
+ layerResults['L2-组件'] = false;
380
+ line(`L2 组件 ${labelOf(false)} 未检测到组件契约表(components 段无表格或类型列)`);
381
+ } else {
382
+ const count = table.rows.length;
383
+ const countOk = count >= COMPONENT_COUNT_PASS;
384
+ const countWarn = count >= COMPONENT_COUNT_WARN && count < COMPONENT_COUNT_PASS;
385
+ const stateViolations = table.rows.filter(r => {
386
+ const min = COMPONENT_TYPE_MIN_STATES[r.type];
387
+ return min !== null && r.statesCount < min;
388
+ });
389
+ const variantViolations = table.rows.filter(r => {
390
+ const min = COMPONENT_TYPE_MIN_STATES[r.type];
391
+ return min !== null && !r.hasVariants;
392
+ });
393
+ const l2ok = countOk && stateViolations.length === 0 && variantViolations.length === 0;
394
+ layerResults['L2-组件'] = l2ok;
395
+ if (l2ok) {
396
+ line(`L2 组件 ✅ ${count} 类契约表(PASS 档),states/variants 全部达标`);
221
397
  } else {
222
- // Defined as a literal value rather than an alias to the base token.
223
- pass(`${token} = ${value}`);
398
+ const parts = [];
399
+ if (!countOk) parts.push(countWarn ? `${count} 类(WARN 档:10-14)` : `${count} 类(低于起步线 10)`);
400
+ if (stateViolations.length) parts.push(`${stateViolations.length} 个组件 states 不足(${stateViolations.map(r => r.name).join('/')})`);
401
+ if (variantViolations.length) parts.push(`${variantViolations.length} 个组件缺 variants(${variantViolations.map(r => r.name).join('/')})`);
402
+ line(`L2 组件 ${countWarn && !stateViolations.length && !variantViolations.length ? '⚠️ ' : labelOf(false) + ' '}${parts.join(';')}`);
403
+ // WARN 档(10-14 类且其余达标)算通过(S2 验收口径);否则按降级规则标注
404
+ layerResults['L2-组件'] = countWarn && stateViolations.length === 0 && variantViolations.length === 0;
224
405
  }
225
406
  }
407
+
408
+ // L3 业务模式(页面范式的归属声明:设计系统侧只做**引用**,节奏表在 prototype skill 的 layouts.md)
409
+ // 检测面 = 合并后设计系统文本 + 同目录 primer.md(gen-primer 写入 `## 页面范式` 段)——
410
+ // v0.54.0 P4 修正:只读传入的单个 md 会让任何按规范创建的系统恒为 ❌(假红灯)。
411
+ const primerText = readFileOrNull(join(dsDir, 'primer.md')) || '';
412
+ const l3ok = /layouts|页面范式|页面节奏|page pattern/i.test(markdown + '\n' + primerText);
413
+ layerResults['L3-业务模式'] = l3ok;
414
+ line(`L3 业务模式 ${labelOf(l3ok)} ${l3ok ? '页面范式引用存在(设计系统侧只引用,节奏表在 prototype layouts.md)' : '未见页面范式引用(缺 primer.md 或引用句)'}`);
415
+
416
+ // L4 页面范式(layout 段 + 断点)
417
+ const layoutBody = sectionBodyRaw(markdown, 'layout');
418
+ const l4ok = layoutBody !== null && /断点|breakpoint|sm|md|lg|栅格|grid/i.test(layoutBody);
419
+ layerResults['L4-页面范式'] = l4ok;
420
+ line(`L4 页面范式 ${labelOf(l4ok)} ${l4ok ? 'layout 段含栅格/断点' : 'layout 段缺栅格或断点声明'}`);
421
+
422
+ // L5 Primer(同目录 primer.md 存在)
423
+ const primerExists = existsSync(join(dsDir, 'primer.md'));
424
+ layerResults['L5-Primer'] = primerExists;
425
+ line(`L5 Primer ${labelOf(primerExists)} ${primerExists ? 'primer.md 存在' : '未生成 primer.md'}`);
426
+
427
+ line('------------------------------------------');
428
+ const passed = Object.values(layerResults).filter(Boolean).length;
429
+ const total = Object.keys(layerResults).length;
430
+ const mark = (k, v) => {
431
+ if (k === 'L1-双主题' && !v) return '⏭️';
432
+ return v ? '✅' : (downgrade ? '⚠️' : '❌');
433
+ };
434
+ line(`合规层:${Object.entries(layerResults).map(([k, v]) => `${k} ${mark(k, v)}`).join(' | ')}`);
435
+ line(`(${passed}/${total} 层达标;contract=${contract || 'unset'}${downgrade ? ' → 不达标降级 WARN' : ''};不输出总分)`);
226
436
  }
227
437
  line();
228
438
 
229
- // ── Verdict ──
439
+ // ── Verdict(exit 语义:仅硬校验决定 exit code)──
440
+
441
+ if (JSON_OUT) {
442
+ const report = {
443
+ schemaVersion: '0.20.0',
444
+ hardChecks: { passed: issues.length === 0, issues },
445
+ layers: layerResults,
446
+ contract: contract || null,
447
+ strict: STRICT,
448
+ };
449
+ console.log(JSON.stringify(report, null, 2));
450
+ }
230
451
 
231
452
  if (issues.length === 0) {
232
- line('Verdict: PASS');
453
+ line('Verdict: PASS(硬校验通过;六层标签不阻断)');
233
454
  process.exit(0);
234
455
  }
235
456
 
236
- line(`Verdict: FAIL (${issues.length} issue${issues.length === 1 ? '' : 's'})`);
457
+ line(`Verdict: FAIL (${issues.length} issue${issues.length === 1 ? '' : 's'};硬校验失败)`);
237
458
  process.exit(1);