@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.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/CHANGELOG.md +71 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/agents/prototype-builder.md +6 -5
- package/agents/prototype-env-scout.md +4 -4
- package/agents/release-archivist.md +1 -0
- package/docs/README_en.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +2 -2
- package/scripts/design-system-import.mjs +238 -0
- package/scripts/gen-primer.mjs +143 -0
- package/scripts/guard/design-token-guard.mjs +284 -63
- package/scripts/lib/ds-parse.mjs +124 -0
- package/scripts/token-extract.mjs +257 -0
- package/skills/design-system/SKILL.md +55 -9
- package/skills/design-system/references/agents/design-system-architect.md +44 -7
- package/skills/design-system/references/creation-flow.md +39 -5
- package/skills/design-system/references/showcase-board-b-end.md +78 -0
- package/skills/design-system/references/showcase-board-c-end.md +92 -0
- package/skills/design-system/references/token-derivation.md +34 -9
- package/skills/design-system/references/variant-schema.md +14 -3
- package/skills/prototype/SKILL.md +14 -8
- package/skills/prototype/references/builder-methodology.md +62 -5
- package/skills/prototype/references/craft/anti-ai-slop.md +1 -1
- package/skills/prototype/references/craft/state-coverage.md +8 -2
- package/skills/prototype/references/orchestration-flow.md +12 -3
- package/skills/prototype/references/prototype-scaffold/assets/design-tokens.css +2 -2
- package/skills/prototype/references/template.html +10 -10
- package/skills/release-archivist/SKILL.md +10 -3
- package/skills/release-archivist/references/closing-procedures.md +10 -0
- package/skills/workflow-bootstrap/SKILL.md +14 -2
- package/templates/design-systems/references/claude.md +315 -0
- package/templates/design-systems/references/linear-app.md +370 -0
- package/templates/design-systems/references/notion.md +312 -0
- package/templates/design-systems/references/posthog.md +259 -0
- package/templates/design-systems/references/sentry.md +265 -0
- package/templates/design-systems/references/stripe.md +325 -0
- package/templates/design-systems/references/supabase.md +258 -0
- package/templates/design-systems/references/vercel.md +313 -0
- package/templates/design-systems/registry.json +75 -0
- 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.
|
|
2
|
+
// design-token-guard.mjs — 设计系统完整性检查(v0.54.0)
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
110
|
-
const
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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);
|