@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
@@ -22,6 +22,7 @@ import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } fr
22
22
  import { join, basename } from 'node:path';
23
23
  import { pathToFileURL } from 'node:url';
24
24
  import { meetsMinSeverity, nextSeverity } from './severity.mjs';
25
+ import { normalizeInline } from './md-normalize.mjs';
25
26
 
26
27
  /**
27
28
  * 剥离 YAML 行尾注释(空格 + `#` 起始)——v0.23 §91 硬化:
@@ -39,9 +40,12 @@ function parseFrontmatter(content) {
39
40
  if (!match) return null;
40
41
  const fm = {};
41
42
  for (const line of match[1].split('\n')) {
42
- const idx = line.indexOf(':');
43
+ // v0.55.0 §8.4.3 横展(FB-4):容忍全角冒号、`**key**` 加粗与反引号
44
+ // (走共享层 `normalizeInline`)。原实现只认半角 `:` + 裸键名,
45
+ // `severity:high` / `**severity**: high` 会整键丢失 → 该条静默不晋升。
46
+ const idx = line.search(/[::]/);
43
47
  if (idx > 0) {
44
- fm[line.slice(0, idx).trim()] = stripInlineComment(line.slice(idx + 1));
48
+ fm[normalizeInline(line.slice(0, idx))] = normalizeInline(stripInlineComment(line.slice(idx + 1)));
45
49
  }
46
50
  }
47
51
  return fm;
@@ -100,10 +104,13 @@ function markEntryConfirmed(filePath) {
100
104
  const content = readFileSync(filePath, 'utf-8');
101
105
  const block = content.match(/^---\n([\s\S]*?)\n---/);
102
106
  if (!block) return;
103
- const counter = block[1].match(/^confirmed:\s*(\d+)\s*$/m);
107
+ // v0.55.0 §8.4.3 横展(FB-4):容忍全角冒号与 `**confirmed**` 加粗。
108
+ // 放宽前这类行匹配不到 → 走追加分支 → frontmatter 出现**重复 confirmed 键**
109
+ // (计数错乱且静默)。命中后统一改回规范形态 `confirmed: N`。
110
+ const counter = block[1].match(/^\**confirmed\**\s*[::]\s*(\d+)\s*$/m);
104
111
  const next = counter ? parseInt(counter[1], 10) + 1 : 2;
105
112
  const frontmatter = counter
106
- ? block[1].replace(/^confirmed:\s*\d+\s*$/m, `confirmed: ${next}`)
113
+ ? block[1].replace(/^\**confirmed\**\s*[::]\s*\d+\s*$/m, `confirmed: ${next}`)
107
114
  : `${block[1]}\nconfirmed: ${next}`;
108
115
  writeFileSync(filePath, content.replace(block[0], `---\n${frontmatter}\n---`), 'utf-8');
109
116
  }
@@ -14,8 +14,40 @@
14
14
  // 5. 主基格式不做规范化:按 `### Requirement:` 为块边界解析,兼容既有主基的
15
15
  // `## ADDED Requirements` 容器形态(不改写既有段落结构)。
16
16
 
17
- const REQ_RE = /^###\s*Requirement:\s*(.+?)\s*$/;
17
+ import { stripInlineEmphasis } from './md-normalize.mjs';
18
+
19
+ // v0.55.0 §8.4.3 横展(FB-4):标题行先过 `stripInlineEmphasis` 再匹配——容忍
20
+ // `### **Requirement**: X` 这类手写加粗;同时容忍全角冒号与冒号前空格
21
+ // (`md-normalize.matchKeyValue` 的同一约定)。**不放宽层级**:仍是恰好 `###`。
22
+ const REQ_RE = /^###\s*Requirement\s*[::]\s*(.+?)\s*$/;
18
23
  const H2_RE = /^##\s+/;
24
+ /** 形似 Requirement 标题(带分隔符)——用于「解析失败不静默」告警。 */
25
+ const REQUIREMENT_LIKE_RE = /^#{1,6}\s*\**\s*Requirement\s*\**\s*[::]/i;
26
+
27
+ /** 归一化后匹配 requirement 标题:容忍加粗等纯格式变体(FB-4 横展)。 */
28
+ function matchRequirement(line) {
29
+ return stripInlineEmphasis(line).match(REQ_RE);
30
+ }
31
+
32
+ /** 解析失败不静默(约束②):形似 requirement 标题但未被接受的 → WARN(不报错)。 */
33
+ function warnUnparsedRequirementHeadings(content) {
34
+ const lines = content.split('\n');
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (!REQUIREMENT_LIKE_RE.test(lines[i]) || matchRequirement(lines[i])) continue;
37
+ console.warn(` [WARN] spec-merge: 第 ${i + 1} 行形似 Requirement 标题但无法解析(按非标题处理):${lines[i].trim().slice(0, 80)}`);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 名称比较键:容忍加粗/反引号/首尾空白等**纯格式**差异。
43
+ *
44
+ * 必要性:delta 侧解析器(`src/parsing/requirement-blocks.ts` 的
45
+ * `normalizeRequirementName`)**只做 trim**,主基侧若单边归一化,`**Foo**` 与 `Foo`
46
+ * 会被判为不同名 → ADDED 静默追加重复块 / MODIFIED 误报「不存在」。故比较时两侧同键。
47
+ */
48
+ function nameKey(name) {
49
+ return stripInlineEmphasis(String(name ?? '').replace(/`/g, '')).trim();
50
+ }
19
51
 
20
52
  /** dist 解析器惰性加载(与 cmd-sync 导入 Validator 同源)。 */
21
53
  let _parseDeltaSpec = null;
@@ -39,7 +71,7 @@ function blockBody(blockLines) {
39
71
 
40
72
  /** 切掉 `#### Previous version` 子节(及其后内容)——用于幂等比对与旧内容提取。 */
41
73
  function stripPreviousVersion(body) {
42
- const idx = body.search(/^####\s+Previous version\b/m);
74
+ const idx = body.search(/^####\s+\**\s*Previous version\b/m);
43
75
  return idx === -1 ? body : body.slice(0, idx).replace(/\s+$/, '');
44
76
  }
45
77
 
@@ -53,8 +85,8 @@ function escapeRe(s) {
53
85
  * `v1-C3-session-governance` 的注记)。
54
86
  */
55
87
  function hasMergedNote(body, changeName) {
56
- const re = new RegExp(`^####\\s+Previous version\\b.*(?<![\\w-])${escapeRe(changeName)}(?![\\w-])`, 'm');
57
- return re.test(body);
88
+ const re = new RegExp(`^####\\s+\\**\\s*Previous version\\b.*(?<![\\w-])${escapeRe(changeName)}(?![\\w-])`, 'm');
89
+ return re.test(stripInlineEmphasis(body));
58
90
  }
59
91
 
60
92
  function renameNote(from, changeName) {
@@ -62,7 +94,7 @@ function renameNote(from, changeName) {
62
94
  }
63
95
 
64
96
  function isRemovedSection(section) {
65
- return typeof section === 'string' && /^Removed\b/i.test(section.trim());
97
+ return typeof section === 'string' && /^Removed\b/i.test(stripInlineEmphasis(section).trim());
66
98
  }
67
99
 
68
100
  /**
@@ -81,14 +113,14 @@ function parseMain(content) {
81
113
  i++;
82
114
  continue;
83
115
  }
84
- const m = lines[i].match(REQ_RE);
116
+ const m = matchRequirement(lines[i]);
85
117
  if (!m) {
86
118
  i++;
87
119
  continue;
88
120
  }
89
121
  let end = lines.length;
90
122
  for (let j = i + 1; j < lines.length; j++) {
91
- if (REQ_RE.test(lines[j]) || H2_RE.test(lines[j])) {
123
+ if (matchRequirement(lines[j]) || H2_RE.test(lines[j])) {
92
124
  end = j;
93
125
  break;
94
126
  }
@@ -99,9 +131,10 @@ function parseMain(content) {
99
131
  return { lines, blocks };
100
132
  }
101
133
 
102
- /** 在正常区(非 `## Removed` 段)按名查找 requirement 块。 */
134
+ /** 在正常区(非 `## Removed` 段)按名查找 requirement 块(名称按 `nameKey` 宽松比较)。 */
103
135
  function findBlock(parsed, name) {
104
- return parsed.blocks.find(b => b.name === name && !isRemovedSection(b.section)) || null;
136
+ const key = nameKey(name);
137
+ return parsed.blocks.find(b => nameKey(b.name) === key && !isRemovedSection(b.section)) || null;
105
138
  }
106
139
 
107
140
  /**
@@ -122,7 +155,7 @@ function findAppendIndex(parsed) {
122
155
 
123
156
  /** 定位 `## Removed` 段的 [start, end)(end = 下一个二级标题或文件末尾);无则 -1。 */
124
157
  function findRemovedSectionRange(parsed) {
125
- const start = parsed.lines.findIndex(l => /^##\s+Removed\s*$/i.test(l));
158
+ const start = parsed.lines.findIndex(l => /^##\s+\**\s*Removed\**\s*$/i.test(stripInlineEmphasis(l)));
126
159
  if (start === -1) return { start: -1, end: -1 };
127
160
  let end = parsed.lines.length;
128
161
  for (let j = start + 1; j < parsed.lines.length; j++) {
@@ -235,7 +268,8 @@ function applyRemoved(content, plan, changeName, today, report) {
235
268
 
236
269
  if (!target) {
237
270
  // 幂等:该名已出现在 `## Removed` 段
238
- const inRemoved = parsed.blocks.some(b => b.name === name && isRemovedSection(b.section));
271
+ const key = nameKey(name);
272
+ const inRemoved = parsed.blocks.some(b => nameKey(b.name) === key && isRemovedSection(b.section));
239
273
  if (inRemoved) {
240
274
  report.skipped++;
241
275
  continue;
@@ -300,6 +334,7 @@ export async function mergeMainSpec(mainContent, deltaContent, changeName, optio
300
334
  }
301
335
 
302
336
  const report = { renamed: 0, modified: 0, added: 0, removed: 0, skipped: 0, noOps: false };
337
+ warnUnparsedRequirementHeadings(mainContent);
303
338
  let content = mainContent;
304
339
  content = applyRenamed(content, plan, changeName, report);
305
340
  content = applyModified(content, plan, changeName, today, report);
@@ -249,7 +249,10 @@ function parseYaml(content) {
249
249
  for (const line of content.split('\n')) {
250
250
  const trimmed = line.trim();
251
251
  if (!trimmed || trimmed.startsWith('#')) continue;
252
- const match = trimmed.match(/^(\w[\w_]*):\s*(.*)/);
252
+ // v0.55.0 §8.4.3 横展(FB-4):只额外容忍**全角冒号**(`state:executing`)。
253
+ // 不得收紧:`line.trim()` 已容忍缩进,保持现状(约束①:缩进在 YAML 是语义,
254
+ // 本层不新增缩进容忍,也不拿掉既有 trim);字段缺失不报错(约束②)。
255
+ const match = trimmed.match(/^(\w[\w_]*)[::]\s*(.*)/);
253
256
  if (match) {
254
257
  const val = match[2].trim();
255
258
  if (val === 'null' || val === '') {
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env node
2
+ // token-extract.mjs — 逆向建库的确定性证据提取器(v0.54.0,设计 §4.1.6 降级版)
3
+ //
4
+ // 从既有代码目录提取设计 token 证据:8 组正则证据机 → source-tokens.json + 统计报告。
5
+ // **零 LLM、零语义推断**——只给"频次 + 位置分布"证据,语义角色(谁是 primary)由人工策展指定。
6
+ //
7
+ // Usage:
8
+ // node scripts/token-extract.mjs <代码目录> [--out <source-tokens.json 路径>] [--budget-ms 60000]
9
+ // node scripts/token-extract.mjs <代码目录> --report # 只输出统计报告
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
12
+ import { join, resolve, dirname, extname, relative } from 'node:path';
13
+
14
+ // ── 配置 ──
15
+
16
+ const SCAN_EXTS = new Set(['.css', '.scss', '.less', '.html', '.htm', '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', '.json']);
17
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache', 'vendor', '__pycache__', '.turbo', 'out', 'target']);
18
+ const MAX_FILE_SIZE = 512 * 1024; // 512 KiB——超过只登记不扫描
19
+ const DEFAULT_BUDGET_MS = 60_000;
20
+
21
+ // ── 证据机(8 组正则,借鉴 open-design token-evidence)──
22
+
23
+ const EVIDENCE = [
24
+ { kind: 'color', re: /#[0-9a-fA-F]{8}\b|#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b/g },
25
+ { kind: 'color', re: /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+[^)]*\)/g },
26
+ { kind: 'color', re: /hsla?\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*[\d.]+%[^)]*\)/g },
27
+ { kind: 'custom-prop', re: /--[a-zA-Z][\w-]*\s*:\s*([^;{}]+)[;}]/g, captureValue: true, nameCapture: /(--[a-zA-Z][\w-]*)/ },
28
+ { kind: 'font', re: /font-family\s*:\s*([^;{}]+)[;}]/gi, captureValue: true },
29
+ { kind: 'spacing', re: /(?:padding|margin|gap|inset|top|right|bottom|left)\s*:\s*([^;{}]+)[;}]/gi, captureValue: true },
30
+ { kind: 'radius', re: /border(?:-[a-z]+)?-radius\s*:\s*([^;{}]+)[;}]/gi, captureValue: true },
31
+ { kind: 'shadow', re: /box-shadow\s*:\s*([^;{}]+)[;}]/gi, captureValue: true },
32
+ { kind: 'tailwind', re: /\b(?:bg|text|border|from|to|via|ring)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)\b/g },
33
+ ];
34
+
35
+ // ── Markdown 规范树证据机(v0.55.0,设计 §8.2.1)──
36
+ //
37
+ // 定位:**证据报告器,不做取值裁决**。P1.5 反方实证——真实规范树的 token 主形态是
38
+ // **Markdown 表格行**(`| \`--color-primary\` | \`#3A94DD\` |`),而上方的代码用证据机
39
+ // (`custom-prop`)要求行尾 `;` 或 `}` → 表格行**永不匹配**,只剩裸 hex 机(有值无名);
40
+ // 且按频次排序会选出**错的主色**(现场:`#1890ff` ×155 vs 正确值 `#3a94dd` ×62 的跨期矛盾)。
41
+ // 故这里只**如实呈现证据与冲突**(见下方的 conflicts 段),语义角色由人裁决——
42
+ // 与既有原则"语义角色由人定,不由 LLM 定"一致。
43
+ const DOC_EVIDENCE = [
44
+ // 表格行里的 token 名 + 值:| `--color-primary` | `#3A94DD` | ...
45
+ { kind: 'doc-table-token', re: /\|\s*`?(--[a-zA-Z][\w-]*)`?\s*\|\s*`?([^|`]+?)`?\s*(?=\|)/g, tokenPair: true },
46
+ // 键值行:- `--color-primary`: #3A94DD
47
+ { kind: 'doc-kv-token', re: /(--[a-zA-Z][\w-]*)\s*[::]\s*([^\n|]+)/g, tokenPair: true },
48
+ // 标题(结构证据:组件名 / 页面类型候选)
49
+ { kind: 'doc-heading', re: /^#{2,4}\s+(.+?)\s*$/g },
50
+ ];
51
+
52
+ /** "看起来是值"的形态白名单——用于过滤表格里与 token 名相邻的说明文字。 */
53
+ const VALUE_LIKE_RE = /^(#[0-9a-fA-F]{3,8}|var\(|rgba?\(|hsla?\(|cubic-bezier\(|\d+(\.\d+)?(px|rem|em|%|ms|s)?$|[a-z-]+\([^)]*\)$)/;
54
+
55
+ // ── 参数 ──
56
+
57
+ const argv = process.argv.slice(2);
58
+ const REPORT_ONLY = argv.includes('--report');
59
+ const DOCS = argv.includes('--docs'); // v0.55.0:Markdown 规范树模式(create-from-docs)
60
+ const outIdx = argv.indexOf('--out');
61
+ const outArg = outIdx !== -1 ? argv[outIdx + 1] : null;
62
+ const budgetIdx = argv.indexOf('--budget-ms');
63
+ const budgetMs = budgetIdx !== -1 ? Number(argv[budgetIdx + 1]) || DEFAULT_BUDGET_MS : DEFAULT_BUDGET_MS;
64
+ const positional = argv.filter((a, i) => !a.startsWith('--') && !(outIdx !== -1 && i === outIdx + 1) && !(budgetIdx !== -1 && i === budgetIdx + 1));
65
+ const rootArg = positional[0];
66
+
67
+ if (!rootArg) {
68
+ console.error('Usage: node scripts/token-extract.mjs <目录> [--out <json 路径>] [--budget-ms 60000] [--report] [--docs]');
69
+ console.error(' --docs:Markdown 规范树模式(.md 纳入扫描;输出证据报告 + 冲突呈现,不做取值裁决)');
70
+ process.exit(1);
71
+ }
72
+ // v0.55.0 修正:原实现 `positional[0]` 静默丢弃其余源路径——多仓场景下用户以为处理了全部。
73
+ if (positional.length > 1) {
74
+ console.error(`❌ 收到 ${positional.length} 个源路径,但本工具一次只支持一个:${positional.join(' / ')}`);
75
+ console.error(' (多仓请分批调用,或先合并到一个目录)');
76
+ process.exit(1);
77
+ }
78
+ const root = resolve(rootArg);
79
+ if (!existsSync(root) || !statSync(root).isDirectory()) {
80
+ console.error(`目录不存在或不可读:${root}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ // docs 模式:纳入 .md;并把 .team-flow 加入跳过(否则产物落进被扫描树内部 → 二次运行自吞)
85
+ if (DOCS) SCAN_EXTS.add('.md');
86
+ SKIP_DIRS.add('.team-flow');
87
+
88
+ // ── 走树(迭代式,预算控制)──
89
+
90
+ const started = Date.now();
91
+ const files = [];
92
+ const skipped = [];
93
+ const queue = [root];
94
+ while (queue.length > 0) {
95
+ if (Date.now() - started > budgetMs) {
96
+ skipped.push({ reason: 'budget-exceeded', remaining: queue.length });
97
+ break;
98
+ }
99
+ const dir = queue.shift();
100
+ let entries;
101
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { skipped.push({ path: dir, reason: 'unreadable' }); continue; }
102
+ for (const e of entries) {
103
+ const full = join(dir, e.name);
104
+ if (e.isSymbolicLink()) { skipped.push({ path: full, reason: 'symlink' }); continue; }
105
+ if (e.isDirectory()) {
106
+ if (SKIP_DIRS.has(e.name)) { skipped.push({ path: full, reason: 'directory-skiplist' }); continue; }
107
+ queue.push(full);
108
+ continue;
109
+ }
110
+ if (!SCAN_EXTS.has(extname(e.name).toLowerCase())) continue;
111
+ let size = 0;
112
+ try { size = statSync(full).size; } catch { continue; }
113
+ if (size > MAX_FILE_SIZE) { skipped.push({ path: full, reason: 'large-file', size }); continue; }
114
+ files.push(full);
115
+ }
116
+ }
117
+
118
+ // ── 证据采集 ──
119
+
120
+ const evidence = new Map(); // key: `${kind}:${normalizedValue}` → { kind, value, count, sources[], usage:Set }
121
+
122
+ function record(kind, value, file, line) {
123
+ const v = String(value).trim().replace(/\s+/g, ' ');
124
+ if (!v || v.length > 120) return;
125
+ const key = `${kind}:${v.toLowerCase()}`;
126
+ let e = evidence.get(key);
127
+ if (!e) { e = { kind, value: v, count: 0, sources: [], usage: new Set() }; evidence.set(key, e); }
128
+ e.count += 1;
129
+ if (e.sources.length < 20) e.sources.push(`${relative(root, file)}:${line}`);
130
+ e.usage.add(relative(root, file));
131
+ }
132
+
133
+ let scanned = 0;
134
+ // docs 模式追加 Markdown 证据机(结构证据 + token 对),代码证据机同时保留
135
+ const ENGINES = DOCS ? [...EVIDENCE, ...DOC_EVIDENCE] : EVIDENCE;
136
+ for (const file of files) {
137
+ if (Date.now() - started > budgetMs) { skipped.push({ reason: 'budget-exceeded-during-scan' }); break; }
138
+ let content;
139
+ try { content = readFileSync(file, 'utf-8'); } catch { continue; }
140
+ scanned += 1;
141
+ const lines = content.split('\n');
142
+ // 逐行扫(拿行号)
143
+ for (let i = 0; i < lines.length; i++) {
144
+ const line = lines[i];
145
+ for (const ev of ENGINES) {
146
+ ev.re.lastIndex = 0;
147
+ let m;
148
+ while ((m = ev.re.exec(line)) !== null) {
149
+ if (ev.tokenPair) {
150
+ // v0.55.0:Markdown 的 token 对(名=值),不做语义解释。
151
+ // **值形态过滤**:真实规范树的表格列序不固定(实测 3 种,含交错列),
152
+ // 与 token 名相邻的格未必是"值"——不过滤会把说明文字("不变"/"悬停态"/"—")
153
+ // 当成 token 值,实测制造 185 条噪音冲突。故只记"看起来是值"的形态,
154
+ // 其余存为位置证据(kind=doc-table-ctx,不参与冲突检测)。
155
+ const val = m[2].trim().replace(/[;;]\s*$/, '');
156
+ if (VALUE_LIKE_RE.test(val)) record(ev.kind, `${m[1]}=${val}`, file, i + 1);
157
+ else record('doc-table-ctx', `${m[1]}~${val}`, file, i + 1);
158
+ } else if (ev.captureValue) {
159
+ const raw = m[1] !== undefined ? m[1] : m[0];
160
+ // 多值拆分**仅适用于空格分隔的短值**(如 `padding: 8px 16px`)。
161
+ // v0.55.0 修正:含括号/逗号的值(`rgba(...)` / `calc(...)` / `color-mix(...)`)
162
+ // 是**单个值**,拆分会产生 `rgba(0` / `0.05)` 这类碎片——实测把
163
+ // `--color-bg-hover: rgba(0, 0, 0, 0.05)` 记成 3 个不同值,制造假冲突。
164
+ const parts = /[(),]/.test(raw) ? [raw] : raw.split(/[\s,]+/);
165
+ for (const part of parts) {
166
+ const t = part.trim();
167
+ if (!t || t === '0' || t === 'auto' || t === 'inherit' || t === 'initial') continue;
168
+ if (ev.kind === 'custom-prop') {
169
+ const nameM = line.match(ev.nameCapture);
170
+ record(ev.kind, `${nameM ? nameM[1] : '--?'}=${t}`, file, i + 1);
171
+ } else {
172
+ record(ev.kind, t, file, i + 1);
173
+ }
174
+ }
175
+ } else {
176
+ record(ev.kind, m[0], file, i + 1);
177
+ }
178
+ if (m.index === ev.re.lastIndex) ev.re.lastIndex += 1; // 防零宽死循环
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ // ── 值归一化 + 相似聚类(确定性)──
185
+
186
+ function normalizeHex(v) {
187
+ const m = v.match(/^#([0-9a-fA-F]{3})$/);
188
+ if (m) return '#' + m[1].split('').map(c => c + c).join('').toLowerCase();
189
+ const m6 = v.match(/^#([0-9a-fA-F]{6})$/);
190
+ if (m6) return '#' + m6[1].toLowerCase();
191
+ return v.toLowerCase();
192
+ }
193
+
194
+ function hexToRgb(hex) {
195
+ const h = normalizeHex(hex).replace('#', '');
196
+ if (h.length !== 6) return null;
197
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
198
+ }
199
+
200
+ // 距离近似(RGB 欧氏;ΔE≈2 量级约为 RGB 距离 ~12)
201
+ function rgbDistance(a, b) {
202
+ return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2);
203
+ }
204
+
205
+ function cluster(list, isSimilar) {
206
+ const sorted = [...list].sort((x, y) => y.count - x.count);
207
+ const clusters = [];
208
+ for (const item of sorted) {
209
+ const target = clusters.find(c => isSimilar(c.representative, item));
210
+ if (target) {
211
+ target.members.push(item);
212
+ target.count += item.count;
213
+ for (const s of item.sources) if (target.sources.length < 20) target.sources.push(s);
214
+ for (const u of item.usage) target.usage.add(u);
215
+ } else {
216
+ clusters.push({ representative: item, members: [item], count: item.count, sources: [...item.sources], usage: new Set(item.usage) });
217
+ }
218
+ }
219
+ return clusters;
220
+ }
221
+
222
+ const allTokens = [...evidence.values()];
223
+
224
+ // 颜色:hex 归并(ΔE<2 ≈ RGB 距离 <12)
225
+ const colorTokens = allTokens.filter(t => t.kind === 'color' && hexToRgb(t.value));
226
+ const nonHexColors = allTokens.filter(t => t.kind === 'color' && !hexToRgb(t.value));
227
+ const colorClusters = cluster(colorTokens, (a, b) => {
228
+ const ra = hexToRgb(a.value), rb = hexToRgb(b.value);
229
+ return ra && rb && rgbDistance(ra, rb) < 12;
230
+ });
231
+
232
+ // 尺寸类:±1px 归并
233
+ const sizeKinds = ['spacing', 'radius'];
234
+ const sizeClusters = {};
235
+ for (const kind of sizeKinds) {
236
+ const items = allTokens.filter(t => t.kind === kind);
237
+ sizeClusters[kind] = cluster(items, (a, b) => {
238
+ const na = parseFloat(a.value), nb = parseFloat(b.value);
239
+ if (!Number.isFinite(na) || !Number.isFinite(nb)) return a.value === b.value;
240
+ const ua = a.value.replace(/[\d.\s-]/g, ''), ub = b.value.replace(/[\d.\s-]/g, '');
241
+ return ua === ub && Math.abs(na - nb) <= 1;
242
+ });
243
+ }
244
+
245
+ const passthrough = ['font', 'shadow', 'custom-prop', 'tailwind'];
246
+ const passthroughTokens = {};
247
+ for (const kind of passthrough) {
248
+ passthroughTokens[kind] = allTokens.filter(t => t.kind === kind).sort((a, b) => b.count - a.count);
249
+ }
250
+
251
+ // ── 输出 ──
252
+
253
+ const sourceTokens = {
254
+ scan: { root, filesDiscovered: files.length, filesScanned: scanned, generatedAt: new Date().toISOString(), budgetMs },
255
+ tokens: {
256
+ color: colorClusters.map(c => ({
257
+ value: c.representative.value,
258
+ count: c.count,
259
+ mergedFrom: c.members.length > 1 ? c.members.map(m => m.value) : undefined,
260
+ sources: c.sources,
261
+ usage: [...c.usage].slice(0, 20),
262
+ })),
263
+ colorRaw: nonHexColors.sort((a, b) => b.count - a.count).map(t => ({ value: t.value, count: t.count, sources: t.sources })),
264
+ spacing: sizeClusters.spacing.map(c => ({ value: c.representative.value, count: c.count, sources: c.sources })),
265
+ radius: sizeClusters.radius.map(c => ({ value: c.representative.value, count: c.count, sources: c.sources })),
266
+ ...Object.fromEntries(passthrough.map(k => [k, passthroughTokens[k].map(t => ({ name: t.value, count: t.count, sources: t.sources.slice(0, 5) }))])),
267
+ },
268
+ skipped,
269
+ };
270
+
271
+ const colorCount = sourceTokens.tokens.color.length;
272
+ const spacingCount = sourceTokens.tokens.spacing.length;
273
+ const radiusCount = sourceTokens.tokens.radius.length;
274
+ const fontCount = sourceTokens.tokens.font.length;
275
+
276
+ // ── 冲突呈现(v0.55.0,docs 模式)──
277
+ // 同一 token 名出现多个不同值 → 显式并列。这不是"工具推断哪个对",而是把**跨期/跨源矛盾**
278
+ // 摊开给人工裁决(现场实测:`primary` 同时有 `#3A94DD` ×62 与 `#1890ff` ×155 两套,
279
+ // 两期各自自洽、互未发现)。
280
+ const conflicts = [];
281
+ if (DOCS) {
282
+ const byName = new Map();
283
+ for (const e of evidence.values()) {
284
+ const idx = e.value.indexOf('=');
285
+ if (idx <= 0) continue;
286
+ const name = e.value.slice(0, idx);
287
+ const val = e.value.slice(idx + 1);
288
+ if (!byName.has(name)) byName.set(name, new Map());
289
+ const vm = byName.get(name);
290
+ vm.set(val, (vm.get(val) || 0) + e.count);
291
+ }
292
+ for (const [name, vm] of byName) {
293
+ if (vm.size > 1) {
294
+ conflicts.push({
295
+ token: name,
296
+ values: [...vm.entries()].sort((a, b) => b[1] - a[1]).map(([value, count]) => ({ value, count })),
297
+ });
298
+ }
299
+ }
300
+ conflicts.sort((a, b) => b.values[0].count - a.values[0].count);
301
+ }
302
+
303
+ // 统计报告(stdout)
304
+ const report = [];
305
+ report.push(`=== Token 提取报告(确定性,零 LLM${DOCS ? ' · Markdown 规范树模式' : ''})===`);
306
+ report.push(`根目录:${root}`);
307
+ report.push(`发现文件:${files.length} | 已扫描:${scanned}${skipped.length ? ` | 跳过:${skipped.length}` : ''}`);
308
+ report.push('');
309
+ report.push(`颜色:${colorCount} 个聚类(归并 ${colorTokens.length - colorCount} 个近似值)`);
310
+ for (const c of sourceTokens.tokens.color.slice(0, 8)) {
311
+ report.push(` ${c.value.padEnd(10)} ×${String(c.count).padStart(4)}${c.mergedFrom ? ` (含 ${c.mergedFrom.length - 1} 个近似写法)` : ''}`);
312
+ }
313
+ report.push('');
314
+ report.push(`间距:${spacingCount} 个聚类 | 圆角:${radiusCount} 个 | 字体栈:${fontCount} 个`);
315
+ for (const c of sourceTokens.tokens.spacing.slice(0, 5)) report.push(` spacing ${c.value} ×${c.count}`);
316
+ for (const c of sourceTokens.tokens.radius.slice(0, 4)) report.push(` radius ${c.value} ×${c.count}`);
317
+ report.push('');
318
+ if (DOCS) {
319
+ report.push('');
320
+ report.push(`⚠️ 冲突呈现:${conflicts.length} 个 token 名存在多值(**跨期/跨源矛盾,需人工裁决**)`);
321
+ for (const c of conflicts.slice(0, 6)) {
322
+ report.push(` ${c.token}: ${c.values.map(v => `${v.value} ×${v.count}`).join(' | ')}`);
323
+ }
324
+ if (conflicts.length === 0) report.push(' (无冲突)');
325
+ }
326
+ report.push('');
327
+ report.push('⚠️ 以上仅为「频次 + 位置」证据——语义角色(哪个是 primary / border / font-display)**需要人工策展指定**,');
328
+ report.push(' 本工具不做自动推断(设计 §4.1.6:避免"垃圾设计系统 + 满分审计")。');
329
+ report.push('');
330
+ report.push('下一步:将本报告与 source-tokens.json 呈交用户,由用户指定主色/中性色/语义色/字体后,');
331
+ report.push(' 由 design-system skill 的 create-from-code 流程补齐 A1/A2/B-slot 并落盘。');
332
+
333
+ console.log(report.join('\n'));
334
+
335
+ if (!REPORT_ONLY) {
336
+ // v0.55.0:docs 模式默认落**调用方工作目录**(workspace 根)。若沿用 `join(root, …)`,
337
+ // 而 root 恰是被扫描的规范树 → 产物落进被扫描树内部 → 二次运行自吞(实测 ×1 → ×3)。
338
+ // 同时 `.team-flow` 已加入 SKIP_DIRS 兜底。
339
+ const outPath = outArg
340
+ ? resolve(outArg)
341
+ : DOCS
342
+ ? join(process.cwd(), '.team-flow', 'token-extract', 'source-docs.json')
343
+ : join(root, '.team-flow', 'token-extract', 'source-tokens.json');
344
+ mkdirSync(dirname(outPath), { recursive: true });
345
+ const payload = { ...sourceTokens, mode: DOCS ? 'docs' : 'code' };
346
+ if (DOCS) payload.conflicts = conflicts;
347
+ writeFileSync(outPath, JSON.stringify(payload, null, 2), 'utf-8');
348
+ console.log(`\n${DOCS ? 'source-docs.json' : 'source-tokens.json'} 已写入:${outPath}`);
349
+ }
@@ -1,10 +1,12 @@
1
1
  ---
2
2
  name: design-system
3
3
  description: >-
4
- 设计系统独立创建与维护 skill。通过用户主导的交互流程(LLM 推荐+用户确认)创建
5
- 项目级 design-system(base 品牌共享层 + B端/C端变体),产出确定性 token 体系 +
6
- 预览画廊。支持独立调用或 prototype skill 内部编排调用。当用户需要创建设计系统、
7
- 迭代设计系统、或原型流程发现设计系统缺失时使用。
4
+ 设计系统独立创建与维护 skill,产出落 .team-flow/design-system/(base 品牌共享层 +
5
+ B端/C端变体 + primer + 预览画廊)。六条创建入口:从已有项目移植(clone)、
6
+ 从内置模板库导入、通用起点(--profile antd)、从既有规范文档导入(create-from-docs)、
7
+ 从既有代码逆向建库(create-from-code)、交互式从零创建;已有设计系统走 iterate。
8
+ 支持独立调用或 prototype skill 内部编排调用。当用户需要创建设计系统、迭代设计系统、
9
+ 刷新 primer 组件白名单、或原型流程发现设计系统缺失时使用。
8
10
  不适用于:原型绘制(用 prototype)、纯架构设计(用 architecture-design)。
9
11
  ---
10
12
 
@@ -23,6 +25,34 @@ Do NOT invoke for:
23
25
 
24
26
  ## 交互创建流程(6 步)
25
27
 
28
+ ### Step 0: 起点选择(v0.55.0:7 条,全部展示)
29
+
30
+ 若 `.team-flow/design-system/` 不存在(无设计系统),先询问起点。
31
+ **每条都要展示,并带"适用场景"一句话**——能力存在但用户不知道 = 能力不存在;
32
+ 按**资产就绪度降序**排列(从最厚的已有资产到最薄的空白起点):
33
+
34
+ | # | 选项 | 适用场景(对用户的一句话) | 动作 |
35
+ |---|------|---------------------------|------|
36
+ | 1 | **移植已有设计系统** | "你们公司**另一个后台项目**已经建过设计系统 → 直接复制过来改" | `clone --from <源路径> --to <目标路径>`(**两参数均必填**,流程见 `references/creation-modes.md` §1) |
37
+ | 2 | **从模板库选择** | "想参考某个成熟产品(Linear / Stripe / Vercel…)的视觉风格" | 展示 `${CLAUDE_PLUGIN_ROOT}/templates/design-systems/registry.json`(8 个参考)→ `node ${CLAUDE_PLUGIN_ROOT}/scripts/design-system-import.mjs <reference.md> --out .team-flow/design-system/base.md` → **续跑 Step 5 评审 → Step 6 落盘** |
38
+ | 3 | **通用起点(Ant Design 规格)** | "全新项目、**没有任何规范**,先要一套像 Ant Design 的合规底座" | `node ${CLAUDE_PLUGIN_ROOT}/scripts/design-system-import.mjs --profile antd --out .team-flow/design-system/base.md` → 续跑 Step 5 → Step 6 |
39
+ | 4 | **从文档规范导入** | "手里有**一套写好的 UI 规范**(Markdown / 规范树)" | `create-from-docs`(流程见 `references/creation-modes.md` §4) |
40
+ | 5 | **从代码逆向建库** | "项目里**已有符合规范的原型代码**" | `create-from-code`(见下方「逆向建库模式」) |
41
+ | 6 | **从零交互创建** | "什么都没有,边聊边定" | 直接进 Step 1(交互式 6 步) |
42
+ | 7 | 已有设计系统 → **iterate** | (非新建) | 见下方「迭代模式」 |
43
+
44
+ > **入口同步(两处声明 + 一处事实传达,v0.55.0 校正)**:
45
+ > ① 本文件 Step 0 —— **无条件展示全部 7 条**(这是模式可达性的**唯一保证**);
46
+ > ② `workflow-bootstrap` B4.6 —— 六项 + 跳过,逐行带适用场景与委托参数;
47
+ > ③ prototype 内部编排(`orchestration-flow.md` §①b)—— 只传达 scout 简报中的**资产事实**(如"已有符合规范的原型代码"),**不预选起点**。
48
+ >
49
+ > ③ 不做 mode 预选是**有意设计**:prototype 无从判断用户资产状况,预选会让其余 6 条不可见。
50
+ > 故**新模式可达性由 ① 的无条件展示保证,不依赖各入口的正确预选**——任何入口下用户都能看到全 7 条。
51
+
52
+ > B 类参考(如 linear-app)转换为**自动提取配色**,须提示用户人工核对(转换器会输出该警告)。
53
+
54
+ > **风格种子(styles.json)**:`${CLAUDE_PLUGIN_ROOT}/templates/design-systems/styles.json` 提供 57 个风格型设计系统的配色/字体元数据(如"想要 X 风格")——Step 1 需求收集时可作为 mood/品牌主色的参考依据,用户选定后以 `brand_color` 传入(不扩充 5 维封闭词汇,保持核心交互稳定)。
55
+
26
56
  ### Step 1: 需求收集(≤5 个预填推荐问题)
27
57
  用 AskUserQuestion 收集 5 个维度(预填 LLM 推荐值,用户原样确认或调整):
28
58
  1. 品牌调性(封闭词汇:professional_minimal / warm_approachable / technical_dense / editorial / brutalist)
@@ -42,29 +72,63 @@ Do NOT invoke for:
42
72
  ### Step 4: 预览生成
43
73
  生成 `preview.html`(自包含 HTML 画廊:色板+排版+间距+组件+明暗切换)。用 `references/preview-template.html` 模板填充 token 值。
44
74
 
75
+ ### Step 4.5: Design Showcase(可选,v0.54.0)
76
+ 询问用户是否产出**基准原型**(判断设计系统实际效果):派 `prototype-builder`(`mode: showcase`)用内置车企 brief(`references/showcase-board-{b,c}-end.md`,按 target)产出 1-3 页 Board 到 scratch。产出前明示预估耗时;可跳过。详见 `references/creation-flow.md` Step 4.5。
77
+
45
78
  ### Step 5: 用户评审 + 确认
46
- 呈现:token 摘要表 + 预览路径 + 默认值透明报告。AskUserQuestion:确认 / 调整 / 重新来。
79
+ 呈现:token 摘要表 + 预览路径 + **showcase 路径(如产出)** + 默认值透明报告。AskUserQuestion:确认 / 调整 / 重新来。
47
80
 
48
81
  ### Step 6: 落盘 + guard 校验
49
- 写入 `.team-flow/design-system/`,运行 `scripts/guard/design-token-guard.mjs` 校验。
82
+ 写入 `.team-flow/design-system/`(base + 变体 + **primer**(`${CLAUDE_PLUGIN_ROOT}/scripts/gen-primer.mjs`)+ preview + **showcase 从 scratch 复制**),运行 `node ${CLAUDE_PLUGIN_ROOT}/scripts/guard/design-token-guard.mjs <设计系统目录>`(v0.54.0:硬校验 + 六层审计;传目录会自动合并 base + 端变体再断言 9 段——base 品牌层本身不含 typography/spacing/layout/motion)。**target=both 时按端各跑一次**(`--variant b-end` / `--variant c-end`)——合并断言掩盖单端残缺。
50
83
 
51
84
  ## 存储位置
52
85
  ```
53
86
  .team-flow/design-system/
54
- ├── base.md # 品牌共享层
55
- ├── b-end.md # B 端变体(可选)
87
+ ├── base.md # 品牌共享层(含组件契约表/principles/governance)
88
+ ├── b-end.md # B 端变体(可选,components 段只写端特有差异)
56
89
  ├── c-end.md # C 端变体(可选)
57
- └── preview.html # token 预览画廊
90
+ ├── primer.md # AI 约束入口(scripts/gen-primer.mjs 生成,含 digest)
91
+ ├── preview.html # token 预览画廊
92
+ ├── showcase/ # 展示板(v0.54.0,可选:b-end.html / c-end-*.html)
93
+ ├── variants/ # 主题变体(如 dark.md)
94
+ └── pending.md # 增量待办(v0.54.0,单写者 = 本 skill)
58
95
  ```
59
96
 
97
+ ## pending.md(设计系统待办,v0.54.0)
98
+
99
+ 原型阶段用户"暂不处理"的增量暂存(`ds_increment` 的落点)。
100
+ - **单写者 = design-system skill**——本 skill 的 iterate 流程负责增删;主代理与 release-archivist **只读**
101
+ - 条目格式:`{描述} | 来源: <change-id 或 s2-prototype> | 时间: <ISO>`
102
+ - **清理规则**:用户确认 → iterate 落盘时移除该条;用户拒绝 → 直接删除(重复发现就重复问);用户自行解决 → 下次 iterate 按同义条目清理
103
+ - iterate 时检查 pending.md:有待办则提示用户"是否一并处理"
104
+
60
105
  ## 迭代模式(iterate)
61
106
  已有 design-system → 读取 → 合并增量 → 变更履历 → 预览 → 确认 → 写入。
107
+ 增量来源:① 原型阶段确认的 `ds_increment`(⑥ 路由)② **pending.md 累积待办** ③ change closing 二级确认项 ④ **页面规范增量**(v0.55.0:用户提供项目页面规范,或更新既有 `页面范式来源` 声明与页面类型表;用户不提供时**默认补写** `页面范式来源:引用内置`)。
108
+ 落盘后重新生成 primer(`${CLAUDE_PLUGIN_ROOT}/scripts/gen-primer.mjs`)+ 跑 guard;涉及 token/契约变更时**可选**重新生成 showcase(用户可跳过)。
109
+
110
+ ## 逆向建库模式(create-from-code)
111
+
112
+ **场景**:企业已有符合规范的原型代码 → 基于它建立设计系统。**确定性提取 + 人工策展**(不做自动语义推断——避免"垃圾设计系统 + 满分审计")。入口(**单目录**)与完整 5 步流程见 `references/creation-modes.md` §6。
113
+
114
+ > **三种新模式(v0.55.0)**——移植(clone)/ 文档导入(create-from-docs)/ 通用起点(`--profile antd`)的完整命令与流程,同样见 `references/creation-modes.md`。Step 0 表格已列出全部 7 条起点及各自适用场景。
62
115
 
63
116
  ## 迁移兼容
64
117
  检测旧 `prototype/design-system.md` → 提示迁移到 `.team-flow/design-system/base.md`(一次性)。
118
+ 迁移属**导入类创建** → 须补 `来源与裁决记录` 段、`contract` 默认 `legacy`。
119
+
120
+ ## 异常处理(v0.55.0)
121
+
122
+ - **源系统不可读 / `base.md` 缺失**(clone):脚本 exit 1,提示 `--from` 的两种语义(项目根自动定位 `.team-flow/design-system`,或直接给设计系统目录)
123
+ - **目标已有设计系统**(clone):exit 1 并引导走 **iterate**(MERGE 不 OVERWRITE)——不覆盖
124
+ - **primer 缺失或过期**:`gen-primer --check` **exit 2** → prototype Step 0 对 `contract: v1` 系统 **blocked**、对 `legacy` 系统 WARN 放行 → 提示用户重跑生成或 iterate
125
+ - **guard 硬校验失败**:exit 1 阻断落盘 → 按输出逐项修复(9 段 / palette / A1 / A2 公式 / B-slot)
126
+ - **用户中途放弃**:草案阶段产物在 scratch(`/tmp/ds-draft-<slug>/`),放弃确认时**整体丢弃**,不写任何正式路径(两阶段落盘 `confirmed:false → confirmed:true` 保证)
127
+ - **配置漂移**:`check-project-config.mjs` 输出 ⚠️ 但 **exit 0**(提示不阻断)
65
128
 
66
129
  ## 执行引擎
67
130
  内部子代理 `references/agents/design-system-architect.md`(唯一写者,两阶段落盘:confirmed:false 草案 → 人工确认 → confirmed:true 写入)。
131
+ **写者范围(v0.55.0)**:含 `base.md` 与**端变体的 `layout` 段**(页面范式声明 + 容器骨架块)。
68
132
 
69
133
  ## 配置
70
134
  ```json
@@ -73,3 +137,8 @@ Do NOT invoke for:
73
137
  "prototype.designSystemBase": ".team-flow/design-system/base.md"
74
138
  }
75
139
  ```
140
+
141
+ > **配置漂移检查(v0.55.0)**:`node ${CLAUDE_PLUGIN_ROOT}/scripts/check-project-config.mjs [项目根]` ——
142
+ > 检查配置指向的路径是否存在、`version` 是否与插件一致。现场实测三类漂移(version 落后 /
143
+ > 指向不存在的文件 / 零消费者键)**全部静默**,只会在用到时失效。检查面按"实际被消费"分级,
144
+ > 零消费者键只提示不报错。