@xulthekl/team-flow 0.52.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/AGENTS.md +3 -3
- package/CHANGELOG.md +140 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +11 -0
- package/INSTALL.md +1 -1
- package/README.md +2 -2
- package/agents/prototype-builder.md +6 -5
- package/agents/prototype-env-scout.md +4 -4
- package/agents/release-archivist.md +5 -4
- package/docs/README_en.md +1 -1
- package/docs/usage-guide.md +4 -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/checks/arch-merged.mjs +101 -0
- package/scripts/guard/checks/arch-snapshot.mjs +16 -3
- package/scripts/guard/design-token-guard.mjs +284 -63
- package/scripts/guard/guard.mjs +9 -1
- package/scripts/lib/arch-merge.mjs +407 -47
- package/scripts/lib/arch-parse.mjs +304 -26
- package/scripts/lib/cmd-arch.mjs +29 -1
- package/scripts/lib/cmd-publish.mjs +53 -6
- package/scripts/lib/cmd-state.mjs +2 -0
- package/scripts/lib/ds-parse.mjs +124 -0
- package/scripts/lib/prototype-sync.mjs +2 -1
- package/scripts/lib/state-loader.mjs +10 -0
- package/scripts/lib/test-merge.mjs +1 -1
- package/scripts/team-flow.mjs +14 -1
- package/scripts/token-extract.mjs +257 -0
- package/skills/architecture-design/templates/api.md +10 -4
- 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 +16 -10
- 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 +50 -33
- package/skills/release-archivist/references/closing-procedures.md +24 -4
- package/skills/workflow-bootstrap/SKILL.md +41 -20
- package/skills/workflow-orchestrator/references/state-model.md +3 -0
- 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,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// design-system-import.mjs — open-design DESIGN.md → team-flow base.md 转换器(v0.54.0,设计 §4.5.2)
|
|
3
|
+
//
|
|
4
|
+
// 解析 open-design 九节 DESIGN.md,映射到 team-flow 的 9 段 schema(A1/A2/B-slot + 组件契约表 + principles/governance)。
|
|
5
|
+
// 关键约束:**A 类转换后必须补齐完整度**(palette 阶梯 / A2 派生公式 / B-slot 别名 / 组件契约表基线)——
|
|
6
|
+
// open-design 原文没有这些,转换器按 team-flow 规则生成。
|
|
7
|
+
//
|
|
8
|
+
// Usage: node scripts/design-system-import.mjs <DESIGN.md 路径> [--out <base.md 路径>] [--id <标识>]
|
|
9
|
+
// 输出后建议运行:node scripts/guard/design-token-guard.mjs <base.md>
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { resolve, dirname, basename, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const argv = process.argv.slice(2);
|
|
16
|
+
const outIdx = argv.indexOf('--out');
|
|
17
|
+
const outArg = outIdx !== -1 ? argv[outIdx + 1] : null;
|
|
18
|
+
const idIdx = argv.indexOf('--id');
|
|
19
|
+
const idArg = idIdx !== -1 ? argv[idIdx + 1] : null;
|
|
20
|
+
const positional = argv.filter((a, i) => !a.startsWith('--') && !(outIdx !== -1 && i === outIdx + 1) && !(idIdx !== -1 && i === idIdx + 1));
|
|
21
|
+
const srcPath = positional[0];
|
|
22
|
+
|
|
23
|
+
if (!srcPath) {
|
|
24
|
+
console.error('Usage: node scripts/design-system-import.mjs <DESIGN.md 路径> [--out <base.md 路径>] [--id <标识>]');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const srcAbs = resolve(srcPath);
|
|
28
|
+
if (!existsSync(srcAbs)) {
|
|
29
|
+
console.error(`DESIGN.md not found: ${srcAbs}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const src = readFileSync(srcAbs, 'utf-8');
|
|
34
|
+
const id = idArg || basename(dirname(srcAbs));
|
|
35
|
+
|
|
36
|
+
// ── 解析 open-design 九节 ──
|
|
37
|
+
|
|
38
|
+
function grab(pattern) {
|
|
39
|
+
const m = src.match(pattern);
|
|
40
|
+
return m ? m[1].trim() : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const title = grab(/^#\s+(.+)$/m) || `Design System Inspired by ${id}`;
|
|
44
|
+
const category = grab(/> Category:\s*(.+)/) || 'Uncategorized';
|
|
45
|
+
const summary = grab(/>\s*(?!Category)(.+\S)/) || '';
|
|
46
|
+
|
|
47
|
+
// A 类模板格式:`**Primary:** \`#hex\``
|
|
48
|
+
let primary = grab(/\*\*Primary:\*\*\s*`(#[0-9a-fA-F]{3,8})`/);
|
|
49
|
+
let surface = grab(/\*\*Surface:\*\*\s*`(#[0-9a-fA-F]{3,8})`/);
|
|
50
|
+
let text = grab(/\*\*Text:\*\*\s*`(#[0-9a-fA-F]{3,8})`/);
|
|
51
|
+
|
|
52
|
+
// B 类精细格式 fallback:找 brand/accent/primary 上下文附近的 hex(自动提取,需人工核对)
|
|
53
|
+
let autoExtracted = false;
|
|
54
|
+
if (!primary) {
|
|
55
|
+
const m = src.match(/(?:brand|accent|primary)[^\n]{0,100}?(#[0-9a-fA-F]{6})/i);
|
|
56
|
+
if (m) { primary = m[1]; autoExtracted = true; }
|
|
57
|
+
}
|
|
58
|
+
if (!surface) {
|
|
59
|
+
const m = src.match(/(?:background|surface|canvas)[^\n]{0,100}?(#[0-9a-fA-F]{6})/i);
|
|
60
|
+
surface = m ? m[1] : '#FFFFFF';
|
|
61
|
+
if (m) autoExtracted = true;
|
|
62
|
+
}
|
|
63
|
+
if (!text) {
|
|
64
|
+
const m = src.match(/(?:text|foreground)[^\n]{0,100}?(#[0-9a-fA-F]{6})/i);
|
|
65
|
+
text = m ? m[1] : '#111827';
|
|
66
|
+
if (m) autoExtracted = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const secondary = grab(/\*\*Secondary:\*\*\s*`(#[0-9a-fA-F]{3,8})`/);
|
|
70
|
+
const success = grab(/\*\*Success:\*\*\s*`(#[0-9a-fA-F]{3,8})`/) || '#16A34A';
|
|
71
|
+
const warning = grab(/\*\*Warning:\*\*\s*`(#[0-9a-fA-F]{3,8})`/) || '#D97706';
|
|
72
|
+
const danger = grab(/\*\*Danger:\*\*\s*`(#[0-9a-fA-F]{3,8})`/) || '#DC2626';
|
|
73
|
+
const families = grab(/\*\*Families:\*\*\s*(.+)/) || 'primary=Inter, display=Inter, mono=ui-monospace';
|
|
74
|
+
const scale = grab(/\*\*Scale:\*\*\s*(.+)/) || '12/14/16/20/24/32';
|
|
75
|
+
const spacingScale = grab(/\*\*Spacing scale:\*\*\s*(.+)/) || '4/8/12/16/24/32';
|
|
76
|
+
|
|
77
|
+
if (!primary) {
|
|
78
|
+
console.error(`未能从 ${srcAbs} 解析出 Primary 色。`);
|
|
79
|
+
console.error('A 类格式需 `**Primary:** \\`#hex\\``;B 类格式自动提取失败。');
|
|
80
|
+
console.error('B 类设计系统建议走人工路径:浏览参考文档 → 提取配色 → 用 create-from-code 或手工创建。');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (autoExtracted) {
|
|
84
|
+
console.warn('⚠️ B 类格式:配色为自动提取(brand/surface/text 上下文匹配),**请人工核对后再采用**。');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 字体族解析:primary=X, display=Y, mono=Z → 系统栈兜底(零依赖:不引 webfont,仅保留字体名首选 + 系统栈)
|
|
88
|
+
function fontStack(family) {
|
|
89
|
+
const f = (family || 'Inter').trim().replace(/^['"]|['"]$/g, '');
|
|
90
|
+
const isMono = /mono|code|menlo|consolas/i.test(f);
|
|
91
|
+
if (isMono) return `'${f}', ui-monospace, SFMono-Regular, Menlo, monospace`;
|
|
92
|
+
return `'${f}', -apple-system, 'Segoe UI', system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif`;
|
|
93
|
+
}
|
|
94
|
+
const famMap = Object.fromEntries(families.split(',').map(p => {
|
|
95
|
+
const [k, v] = p.split('=').map(x => (x || '').trim());
|
|
96
|
+
return [k, v];
|
|
97
|
+
}));
|
|
98
|
+
const fontDisplay = fontStack(famMap.display || famMap.primary);
|
|
99
|
+
const fontBody = fontStack(famMap.primary);
|
|
100
|
+
const fontMono = fontStack(famMap.mono || 'ui-monospace');
|
|
101
|
+
|
|
102
|
+
// 字号阶梯:open-design 的 "12/14/16/20/24/32" → type scale 名称映射
|
|
103
|
+
const scaleNums = scale.split('/').map(s => s.trim()).filter(Boolean);
|
|
104
|
+
const typeScale = scaleNums.slice(0, 7).map((v, i) => {
|
|
105
|
+
const names = ['--text-xs', '--text-sm', '--text-base', '--text-lg', '--text-xl', '--text-2xl', '--text-3xl'];
|
|
106
|
+
return `${names[i]}: ${v}px`;
|
|
107
|
+
}).join(';') || '--text-xs: 12px;--text-sm: 14px;--text-base: 16px;--text-lg: 20px;--text-xl: 24px;--text-2xl: 32px';
|
|
108
|
+
|
|
109
|
+
// ── 组件契约表基线(20 类,来自 design-system-architect.md 的完整基线)──
|
|
110
|
+
|
|
111
|
+
const CONTRACT_ROWS = [
|
|
112
|
+
'| Button | 交互 | primary / secondary / ghost / danger | sm / md / lg | default / hover / focus-visible / active / disabled / loading | 触发动作 | 用颜色单独传状态 |',
|
|
113
|
+
'| Input | 交互 | default / error / success | sm / md / lg | default / focus / disabled / readonly / error / filled | 文本录入 | 无 label |',
|
|
114
|
+
'| Select | 交互 | default / multiple / searchable | sm / md / lg | default / focus / disabled / error / open | 选项选择 | 超 10 项不用搜索 |',
|
|
115
|
+
'| Checkbox | 交互 | default / indeterminate | sm / md | default / hover / focus / disabled / checked | 多选 | — |',
|
|
116
|
+
'| Radio | 交互 | default | sm / md | default / focus / disabled / selected | 单选 | 超 7 项改用 Select |',
|
|
117
|
+
'| Switch | 交互 | default | md | default / focus / disabled / on / off | 二元开关 | 高危操作须确认 |',
|
|
118
|
+
'| Tag | 轻量 | default / success / warning / danger / info | sm / md | default / hover / closable¹ | 状态标记 | 用作按钮 |',
|
|
119
|
+
'| Icon | 豁免 | — | sm / md / lg | — | 语义图示 | emoji |',
|
|
120
|
+
'| Avatar | 轻量 | circle / square | sm / md / lg | default / fallback / loading | 用户标识 | — |',
|
|
121
|
+
'| Tooltip | 轻量 | default | — | hidden / visible | 补充说明 | 承载关键操作 |',
|
|
122
|
+
'| Modal | 交互 | default / confirm / destructive | sm / md / lg | open / loading / error / closing | 阻断式交互 | 嵌套 Modal |',
|
|
123
|
+
'| Drawer | 交互 | left / right / bottom | sm / md / lg | open / loading / error | 侧边任务面板 | — |',
|
|
124
|
+
'| Tabs | 交互 | line / pill | — | default / active / disabled / loading | 内容分组 | 超 7 个 tab |',
|
|
125
|
+
'| Table | 交互 | default / compact / striped | — | default / loading / empty / error / selected | 结构化数据 | 无分页超长列表 |',
|
|
126
|
+
'| Form | 交互 | single / two-column | — | untouched / dirty / submitted-pending / error | 字段集合 | 单页超 20 字段 |',
|
|
127
|
+
'| Card | 交互 | default / interactive / stat | — | default / hover / selected / loading | 内容容器 | 嵌套超 2 层 |',
|
|
128
|
+
'| Pagination | 交互 | default / simple | sm / md | default / disabled / loading | 分页导航 | — |',
|
|
129
|
+
'| FilterBar | 交互 | default / collapsible | — | default / expanded / applied | 筛选条件集合 | — |',
|
|
130
|
+
'| EmptyState | 豁免 | default / filtered / error | — | — | 空态承载 | 只有"暂无数据" |',
|
|
131
|
+
'| Toast | 交互 | success / warning / error / info | — | entering / visible / exiting | 轻量反馈 | 承载需用户操作的信息 |',
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
// ── 生成 base.md ──
|
|
135
|
+
|
|
136
|
+
const base = `# ${title}
|
|
137
|
+
|
|
138
|
+
> a11y: WCAG 2.2 AA(对比度 4.5:1 / 大字 3:1 / 焦点可见 / 键盘可达)
|
|
139
|
+
|
|
140
|
+
> 由 open-design 设计系统 \`${id}\`(Category: ${category})转换生成(scripts/design-system-import.mjs v0.54.0)。
|
|
141
|
+
> ${summary}
|
|
142
|
+
> 来源归因:open-design(nexu-io)MIT License,"inspired by" 逆向工程描述,非官方资产。
|
|
143
|
+
|
|
144
|
+
## color
|
|
145
|
+
|
|
146
|
+
**A1-identity(8 必选)**
|
|
147
|
+
- \`--bg\`: color-mix(in oklab, var(--surface), var(--fg) 3%)
|
|
148
|
+
- \`--surface\`: ${surface}
|
|
149
|
+
- \`--fg\`: ${text}
|
|
150
|
+
- \`--muted\`: color-mix(in oklab, var(--fg), var(--surface) 45%)
|
|
151
|
+
- \`--border\`: color-mix(in oklab, var(--fg), var(--surface) 88%)
|
|
152
|
+
- \`--accent\`: ${primary}
|
|
153
|
+
- \`--font-display\`: ${fontDisplay}
|
|
154
|
+
- \`--font-body\`: ${fontBody}
|
|
155
|
+
|
|
156
|
+
**语义色**:success ${success} / warning ${warning} / danger ${danger}${secondary ? ` / secondary ${secondary}` : ''}
|
|
157
|
+
|
|
158
|
+
**A2-derived(color-mix 公式)**:--accent-hover / --accent-active / --focus-ring / --elev-raised(公式见 token-derivation.md)
|
|
159
|
+
|
|
160
|
+
**B-slot 别名**:--fg-2 → var(--fg) / --meta → var(--muted) / --border-soft → var(--border) / --surface-warm → var(--surface)
|
|
161
|
+
|
|
162
|
+
## typography
|
|
163
|
+
|
|
164
|
+
- 字阶:${typeScale}
|
|
165
|
+
- 行高:正文 1.5 / 标题 1.2
|
|
166
|
+
- 字重:400(正文)/ 500-600(标题)
|
|
167
|
+
|
|
168
|
+
## spacing
|
|
169
|
+
|
|
170
|
+
- 阶梯(4 基数):${spacingScale.split('/').map((v, i) => `${v.trim()}px`).join(' / ')}
|
|
171
|
+
|
|
172
|
+
## layout
|
|
173
|
+
|
|
174
|
+
- 栅格 12 列 / 断点 sm(640) md(1024) lg(1440) / 容器 max-width 1200px
|
|
175
|
+
|
|
176
|
+
## components
|
|
177
|
+
|
|
178
|
+
| 组件 | 类型 | variants | sizes | states | 用途 | 禁止 |
|
|
179
|
+
|------|------|----------|-------|--------|------|------|
|
|
180
|
+
${CONTRACT_ROWS.join('\n')}
|
|
181
|
+
|
|
182
|
+
## motion
|
|
183
|
+
|
|
184
|
+
- duration 150–250ms / easing ease-out(\`cubic-bezier(0.2, 0, 0, 1)\`)
|
|
185
|
+
|
|
186
|
+
## voice
|
|
187
|
+
|
|
188
|
+
- 语气:${category} 语境的简洁专业表达——具体文案要求由项目 PRD 补充。
|
|
189
|
+
|
|
190
|
+
## brand
|
|
191
|
+
|
|
192
|
+
- 品牌主色:${primary}(accent 用法:主 CTA / 链接 / 选中态)
|
|
193
|
+
|
|
194
|
+
## anti-patterns
|
|
195
|
+
|
|
196
|
+
1. 禁止内联样式漂移
|
|
197
|
+
2. 禁止非 token 颜色
|
|
198
|
+
3. ${category} 语境:不得引入与来源风格冲突的装饰效果
|
|
199
|
+
|
|
200
|
+
## palette
|
|
201
|
+
|
|
202
|
+
- neutral / primary / success / warning / danger 各 50–900 阶梯(primary 以 ${primary} 为 500 锚点,公式见 token-derivation.md)
|
|
203
|
+
|
|
204
|
+
## principles
|
|
205
|
+
|
|
206
|
+
1. 一致性优先于局部创意——同类信息用同一呈现
|
|
207
|
+
2. 清晰优于装饰——信息层级先于视觉效果
|
|
208
|
+
3. 可访问性默认开启——所有交互元素满足 WCAG AA
|
|
209
|
+
|
|
210
|
+
## governance
|
|
211
|
+
|
|
212
|
+
- contract: v1
|
|
213
|
+
- version: 0.1.0
|
|
214
|
+
- 负责人: {项目角色}
|
|
215
|
+
- 弃用策略: 组件弃用需在 changelog 记录 + 保留 1 个迭代周期
|
|
216
|
+
- changelog: 见文末"变更履历"段
|
|
217
|
+
|
|
218
|
+
## 变更履历
|
|
219
|
+
|
|
220
|
+
| 时间 | 变更内容 | 来源 |
|
|
221
|
+
|------|---------|------|
|
|
222
|
+
| ${new Date().toISOString().slice(0, 10)} | 从 open-design \`${id}\` 转换生成 | design-system-import.mjs |
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const outPath = outArg ? resolve(outArg) : resolve(`.team-flow/design-system/base.md`);
|
|
226
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
227
|
+
writeFileSync(outPath, base, 'utf-8');
|
|
228
|
+
|
|
229
|
+
console.log(`base.md 已生成:${outPath}`);
|
|
230
|
+
console.log(` 来源:open-design ${id}(${category})`);
|
|
231
|
+
console.log(` accent=${primary} / surface=${surface} / text=${text}`);
|
|
232
|
+
console.log(` 组件契约表:20 类基线(待审阅调整)`);
|
|
233
|
+
// 提示中的脚本路径用**本脚本所在插件目录**解析(绝对路径,可直接复制执行)——
|
|
234
|
+
// 裸 `node scripts/...` 在插件零拷贝安装下不解析(v0.54.0 P4 根因)。
|
|
235
|
+
const pluginDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
236
|
+
console.log('\n下一步(由 design-system skill 的 Step 5 评审 → Step 6 落盘承接):');
|
|
237
|
+
console.log(` 1) node ${join(pluginDir, 'scripts', 'gen-primer.mjs')} ${outPath} # 生成 primer(组件白名单 + digest,缺失会阻断 prototype Step 0)`);
|
|
238
|
+
console.log(` 2) node ${join(pluginDir, 'scripts', 'guard', 'design-token-guard.mjs')} ${dirname(outPath)} # 硬校验 + 六层审计`);
|
|
@@ -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}`);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// scripts/guard/checks/arch-merged.mjs — arch-merged 门禁(v0.53.0,设计增强方案 v0.25 §110)
|
|
2
|
+
//
|
|
3
|
+
// 挂 `executing:closing`:本 change 的架构增量必须已回写到全局 docs/architecture/。
|
|
4
|
+
//
|
|
5
|
+
// **背景(§101.4 根因 II)**:team-flow 对「代码→测试」「specs→merge」「tasks→complete」
|
|
6
|
+
// 都挂了门禁,唯独「架构增量→台账」没有——`executing:closing` 原挂 9 个维度、无 arch-merge;
|
|
7
|
+
// 且 `VALID_STATES` 无 `closed`(`isChangeClosed` 的 `state === 'closed'` 是死分支),
|
|
8
|
+
// **arch-merge 在状态机上没有任何锚点**。
|
|
9
|
+
//
|
|
10
|
+
// **B' 方案(§110.2)**:arch-merge 前移到状态转换**之前**执行,本维度即该转换的前置条件。
|
|
11
|
+
// 时序:`tests/reviews 通过 → tf arch-merge → tf state transition closing → prototype-sync`。
|
|
12
|
+
// 语义上可行——`executing→closing` 是一个**原子门禁**,所有维度一起 PASS 才放行,
|
|
13
|
+
// 把「回写」纳入门禁校验范围不改变「验证通过后才归档」的语义。
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { findProjectRoot, readArchState, isLegacyArch } from './arch-gate-exemptions.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 读 change 的 .team-flow.yaml 顶层标量字段。
|
|
20
|
+
*
|
|
21
|
+
* ⚠ **未设置字段在 state 文件中序列化为字符串 `'null'` 而非 JS `null`**——这是项目
|
|
22
|
+
* 既定约定(`writeState` 用 `state.x ?? 'null'` 拼接)。`arch-design.mjs` /
|
|
23
|
+
* `compound-captured.mjs` 均显式归一化(`val === 'null' ? null : val`)。**本 check
|
|
24
|
+
* 初版漏了这一步**:test 夹具未设 `arch_design_decision` 时读到的 `'null'` 是真值,
|
|
25
|
+
* 于是「无架构增量」被误判为「需要回写」→ 门禁误 FAIL。
|
|
26
|
+
*/
|
|
27
|
+
function readChangeState(changeDir) {
|
|
28
|
+
const p = path.join(changeDir, '.team-flow.yaml');
|
|
29
|
+
if (!fs.existsSync(p)) return {};
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const line of fs.readFileSync(p, 'utf-8').split('\n')) {
|
|
32
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
33
|
+
if (m) out[m[1]] = (m[2] === 'null' || m[2] === '') ? null : m[2].trim();
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} changeDir - change 目录路径
|
|
40
|
+
* @returns {{ pass: boolean, failures: string[] }}
|
|
41
|
+
*/
|
|
42
|
+
export function checkArchMerged(changeDir) {
|
|
43
|
+
const changeName = path.basename(path.resolve(changeDir).replace(/\/$/, ''));
|
|
44
|
+
const state = readChangeState(changeDir);
|
|
45
|
+
|
|
46
|
+
// ① 无架构增量 → 豁免(arch_design_decision 缺失或 skipped)
|
|
47
|
+
const decision = state.arch_design_decision;
|
|
48
|
+
if (!decision || decision === 'skipped') return { pass: true, failures: [] };
|
|
49
|
+
|
|
50
|
+
// ② 显式跳过键(§110.2 加固 iii)——与 tasks_skipped / test_matrix_skipped 同一模式。
|
|
51
|
+
// 用于 arch-merge 确为 no-op 的场景(如 architecture.md 无演进日志段且无聚合)。
|
|
52
|
+
if (state.arch_merge_skipped === 'true') {
|
|
53
|
+
console.warn(` [WARN] arch-merged: 显式跳过(reason: ${state.arch_merge_skip_reason || '<未填写>'})`);
|
|
54
|
+
return { pass: true, failures: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ③ legacy 项目豁免(复用 arch-gate-exemptions 唯一真相源,防判定漂移)
|
|
58
|
+
const root = findProjectRoot(changeDir);
|
|
59
|
+
if (!root) return { pass: true, failures: [] };
|
|
60
|
+
if (isLegacyArch(readArchState(root))) {
|
|
61
|
+
console.warn(' [WARN] arch-merged: 项目架构基线未建立(arch_baseline 缺失)— legacy 豁免');
|
|
62
|
+
return { pass: true, failures: [] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const archPath = path.join(root, 'docs', 'architecture', 'ARCHITECTURE.md');
|
|
66
|
+
if (!fs.existsSync(archPath)) {
|
|
67
|
+
return {
|
|
68
|
+
pass: false,
|
|
69
|
+
failures: [`全局 docs/architecture/ARCHITECTURE.md 不存在 — 运行 tf arch-merge ${changeDir}`],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const content = fs.readFileSync(archPath, 'utf-8');
|
|
73
|
+
|
|
74
|
+
// 双通道判据(**缺一则门禁可能永久 FAIL 无出路**,见 §115.1 P0-2):
|
|
75
|
+
//
|
|
76
|
+
// 通道 ①:marker 区来源列的单元格**精确等于** `change:<name>`
|
|
77
|
+
// —— 必须按单元格精确匹配:`content.includes('change:' + name)` 会被
|
|
78
|
+
// `change:v1-C1-other` 这类前缀相同的邻项误命中。
|
|
79
|
+
//
|
|
80
|
+
// 通道 ②:演进日志锚 `### change:<name>`
|
|
81
|
+
// —— **为何必须有此通道**:`upsertEvolutionLog` 在 change 的 architecture.md
|
|
82
|
+
// **没有演进日志段**时首行即 `return globalContent` 静默早退,全局永远不会出现
|
|
83
|
+
// 该锚;而零聚合 change 的 marker 区来源列也不会出现(marker 区来自已合并聚合)。
|
|
84
|
+
// 只认单通道 → 这两类 change **永久 FAIL 且重跑 arch-merge 也不会改善**。
|
|
85
|
+
const viaRegistry = content.split('\n').some(line =>
|
|
86
|
+
line.split('|').some(cell => cell.trim() === `change:${changeName}`));
|
|
87
|
+
const escaped = changeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
88
|
+
const viaLog = new RegExp(`^### change:${escaped}\\s*$`, 'm').test(content);
|
|
89
|
+
|
|
90
|
+
if (viaRegistry || viaLog) return { pass: true, failures: [] };
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
pass: false,
|
|
94
|
+
failures: [
|
|
95
|
+
`本 change 的架构增量未回写全局台账(marker 区来源列与演进日志锚均无 change:${changeName})`
|
|
96
|
+
+ ` — 运行 tf arch-merge ${changeDir}。`
|
|
97
|
+
+ `B' 时序:arch-merge 在 executing→closing 转换**之前**执行(设计增强方案 v0.25 §110.2)。`
|
|
98
|
+
+ `若本 change 确无架构增量可回写,显式置 arch_merge_skipped: true + arch_merge_skip_reason。`,
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
// scripts/guard/checks/arch-snapshot.mjs — 架构快照门禁(v0.35.0,v0.14 §59.4)
|
|
2
|
-
// 挂 executing:closing
|
|
2
|
+
// 挂 executing:closing:本轮迭代产品级架构快照必须已落盘("先快照后回写"强制化)。
|
|
3
|
+
//
|
|
4
|
+
// v0.53.0 注释修正:原写「(先于 arch-merge)」。B' 方案(设计增强方案 v0.25 §110)已把
|
|
5
|
+
// arch-merge 前移到本转换**之前**执行,"先于 arch-merge"不再成立。二者现同挂本转换且
|
|
6
|
+
// **无数据依赖**:本 check 只读 `docs/architecture/iterations/`,而 arch-merge 全流程不触碰
|
|
7
|
+
// 该目录(已逐行核对)。"先快照后回写"仍是语义前提(快照在 ARCH 阶段产出,早于整个 closing)。
|
|
3
8
|
//
|
|
4
9
|
// 规则:
|
|
5
10
|
// - arch_baseline == null(在途/存量)→ PASS + WARN(legacy 豁免)
|
|
6
11
|
// - skip 已物化(iterations/vN/SKIPPED)→ PASS
|
|
7
12
|
// - iterations/<latest>/architecture.md 存在 → PASS
|
|
8
13
|
// - 否则 → FAIL
|
|
14
|
+
//
|
|
15
|
+
// v0.53.0 §115.1 P2-5 修复:本文件使用 `join` / `existsSync` 但**从未导入**
|
|
16
|
+
// `node:path` / `node:fs` → 走到兜底分支时抛 `ReferenceError`(运行期崩溃)。
|
|
17
|
+
// 此前未暴露是因为前三个早退分支(legacy / skip / 有快照)覆盖了绝大多数场景;
|
|
18
|
+
// 只有「非 legacy + 无 skip + 无快照」的项目才会走到第 35 行。横展核查全部 22 个
|
|
19
|
+
// guard check 文件(该目录 .mjs 文件数,随本轮新增 arch-merged.mjs 由 21 增至 22),仅此一处。
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
9
22
|
import {
|
|
10
23
|
findProjectRoot, readArchState, isLegacyArch,
|
|
11
24
|
findLatestIterationSnapshot, isArchSkipMaterialized, ARCH_READINESS_FAIL_HINT,
|
|
@@ -32,8 +45,8 @@ export function checkArchSnapshot(changeDir) {
|
|
|
32
45
|
if (snap) return { pass: true, failures: [] };
|
|
33
46
|
|
|
34
47
|
// v0.36.3 §63.4 在途 change 兜底:快照缺失但 change 已有增量产物 → 以 change 产物为实际态(存量升级场景)
|
|
35
|
-
const archDir = join(changeDir, 'architecture');
|
|
36
|
-
if (existsSync(archDir) && existsSync(join(archDir, 'architecture.md'))) {
|
|
48
|
+
const archDir = path.join(changeDir, 'architecture');
|
|
49
|
+
if (fs.existsSync(archDir) && fs.existsSync(path.join(archDir, 'architecture.md'))) {
|
|
37
50
|
console.warn(' [WARN] arch-snapshot: 产品级快照缺失,以 change 增量产物兜底(存量升级在途 change,快照后补为 v0)');
|
|
38
51
|
return { pass: true, failures: [] };
|
|
39
52
|
}
|