@dev-workflow/skill 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/cli.mjs +9 -0
  4. package/package.json +39 -0
  5. package/skills/artifact/README.md +23 -0
  6. package/skills/execution/devFlow/SKILL.md +341 -0
  7. package/skills/execution/devFlow/agents/openai.yaml +7 -0
  8. package/skills/execution/devFlow/domains/backend/references/api-tech.md +173 -0
  9. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.mjs +170 -0
  10. package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.test.mjs +89 -0
  11. package/skills/execution/devFlow/domains/backend/templates/api-tech.md +164 -0
  12. package/skills/execution/devFlow/domains/frontend/references/contract-check.md +270 -0
  13. package/skills/execution/devFlow/domains/frontend/references/foundation-freeze.md +92 -0
  14. package/skills/execution/devFlow/domains/frontend/references/lightweight-flow.md +55 -0
  15. package/skills/execution/devFlow/domains/frontend/references/page-build.md +268 -0
  16. package/skills/execution/devFlow/domains/frontend/references/page-tech.md +414 -0
  17. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/field-common.md +69 -0
  18. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-complex.md +105 -0
  19. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-id-code.md +44 -0
  20. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-number-date.md +106 -0
  21. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-relation.md +144 -0
  22. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-selectors.md +102 -0
  23. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-text.md +130 -0
  24. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/index.md +42 -0
  25. package/skills/execution/devFlow/domains/frontend/references/product-design-specs/interaction.md +61 -0
  26. package/skills/execution/devFlow/domains/frontend/scripts/check_page_tech_doc.mjs +82 -0
  27. package/skills/execution/devFlow/domains/frontend/scripts/contract_check_static.mjs +145 -0
  28. package/skills/execution/devFlow/domains/frontend/scripts/generate_foundation_summary.mjs +224 -0
  29. package/skills/execution/devFlow/domains/frontend/templates/contract-report.md +66 -0
  30. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l0.md +7 -0
  31. package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l1.md +9 -0
  32. package/skills/execution/devFlow/domains/frontend/templates/page-build/component.tsx.tpl +8 -0
  33. package/skills/execution/devFlow/domains/frontend/templates/page-build/constants.ts.tpl +5 -0
  34. package/skills/execution/devFlow/domains/frontend/templates/page-build/index.ts.tpl +1 -0
  35. package/skills/execution/devFlow/domains/frontend/templates/page-build/route.tsx.tpl +11 -0
  36. package/skills/execution/devFlow/domains/frontend/templates/page-build/service.ts.tpl +17 -0
  37. package/skills/execution/devFlow/domains/frontend/templates/page-build/types.ts.tpl +11 -0
  38. package/skills/execution/devFlow/domains/frontend/templates/page-tech.md +103 -0
  39. package/skills/execution/devFlow/domains/requirement/references/prd-review.md +129 -0
  40. package/skills/execution/devFlow/domains/requirement/templates/prd-review.md +57 -0
  41. package/skills/execution/figmaSync/SKILL.md +349 -0
  42. package/skills/execution/figmaSync/chapters/apply.md +177 -0
  43. package/skills/execution/figmaSync/chapters/plan.md +359 -0
  44. package/skills/execution/figmaSync/chapters/prepare.md +47 -0
  45. package/skills/execution/figmaSync/examples/native-css.md +129 -0
  46. package/skills/execution/figmaSync/examples/plan-doc-sample.md +39 -0
  47. package/skills/execution/figmaSync/scripts/_shared/session-log.mjs +82 -0
  48. package/skills/execution/figmaSync/scripts/_shared/token-resolver.mjs +132 -0
  49. package/skills/execution/figmaSync/scripts/_shared/token-source.mjs +145 -0
  50. package/skills/execution/figmaSync/scripts/figma-sync-report.mjs +448 -0
  51. package/skills/execution/figmaSync/scripts/icon-inventory.mjs +165 -0
  52. package/skills/execution/figmaSync/scripts/lookup-var.mjs +184 -0
  53. package/skills/execution/figmaSync/scripts/match-token.mjs +210 -0
  54. package/skills/execution/figmaSync/scripts/prepare-check.mjs +290 -0
  55. package/skills/execution/figmaSync/scripts/verify-plan.mjs +458 -0
  56. package/skills/execution/figmaSync/snapshots/README.md +13 -0
  57. package/skills/execution/figmaSync/snapshots/last-sync.md +1538 -0
  58. package/skills/execution/figmaSync/templates/PLAN.md.tpl +227 -0
  59. package/skills/review/consistency-checker/SKILL.md +112 -0
  60. package/skills/review/consistency-checker/rules/README.md +16 -0
  61. package/src/cli/agents.mjs +16 -0
  62. package/src/cli/copy.mjs +95 -0
  63. package/src/cli/install.mjs +131 -0
  64. package/src/cli/prompts.mjs +66 -0
  65. package/tools/lark/README.md +78 -0
  66. package/tools/lark/references/lark-doc.md +156 -0
  67. package/tools/lark/references/lark-read.md +235 -0
  68. package/tools/lark/references/prepare.md +147 -0
  69. package/tools/lark/scripts/lark_api.mjs +404 -0
  70. package/tools/lark/scripts/lark_api.test.mjs +63 -0
  71. package/tools/lark/scripts/lark_check_permissions.mjs +52 -0
  72. package/tools/lark/scripts/lark_publish_doc.mjs +299 -0
  73. package/tools/lark/scripts/lark_read_docx.mjs +328 -0
  74. package/tools/lark/scripts/markdown_to_lark_blocks.mjs +397 -0
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Verify Plan
3
+ *
4
+ * 对 PLAN.md 做反向校验,在 plan 阶段结束前确保契约可验证:
5
+ * 1. §3 CSS 变量映射表的 CSS variable 与 /theme 路径是否存在
6
+ * - CSS variable 必须存在于 theme/dark CSS 源
7
+ * - /theme 路径必须能映射到当前变量记录
8
+ * - 未命中的 → fail
9
+ *
10
+ * 2. §2 组件映射决策的 apex-ui 组件 + props
11
+ * - 解析形如 `<Button type="primary" size="md" />` 的代码块
12
+ * - 读 node_modules/@frontend/apex-ui--react/dist/llm/<component>.txt 的 API 表
13
+ * - prop 不存在 → fail;prop 是枚举型但值不在枚举 → fail
14
+ *
15
+ * 3. §2.5 Icon 资产匹配的缺失项是否已确认
16
+ * - 缺失 icon 但用户确认仍为待确认 / 空 → fail
17
+ * - 缺失 icon 已说明导入方案、忽略策略或用户不关心 → pass
18
+ *
19
+ * 4. figma-plan.css 是否存在,且不包含 Panda CSS 相关内容
20
+ *
21
+ * 用法:
22
+ * pnpm figma:verify-plan path/to/PLAN.md
23
+ *
24
+ * 退出码:
25
+ * 0 = 全通过 / 仅 warning
26
+ * 1 = 有 fail
27
+ */
28
+ import fs from 'node:fs'
29
+ import path from 'node:path'
30
+
31
+ import { loadCurrentTokenRecords } from './_shared/token-source.mjs'
32
+
33
+ const APEX_UI_DIST = 'node_modules/@frontend/apex-ui--react/dist/llm'
34
+
35
+ function main() {
36
+ const planPath = process.argv[2]
37
+ if (!planPath || !fs.existsSync(planPath)) {
38
+ console.error('[verify-plan] usage: pnpm figma:verify-plan <path/to/PLAN.md>')
39
+ process.exit(1)
40
+ }
41
+
42
+ const text = fs.readFileSync(planPath, 'utf-8')
43
+ const failures = []
44
+ const warnings = []
45
+ const infos = []
46
+
47
+ try {
48
+ verifyCssVariables(text, failures, infos)
49
+ } catch (err) {
50
+ failures.push({ kind: 'css-variable-engine', message: err.message })
51
+ }
52
+
53
+ try {
54
+ verifyApexUi(text, failures, warnings, infos)
55
+ } catch (err) {
56
+ failures.push({ kind: 'apex-ui-engine', message: err.message })
57
+ }
58
+
59
+ try {
60
+ verifyIcons(text, failures, warnings, infos)
61
+ } catch (err) {
62
+ failures.push({ kind: 'icon-engine', message: err.message })
63
+ }
64
+
65
+ try {
66
+ verifyCssDraft(text, planPath, failures, warnings, infos)
67
+ } catch (err) {
68
+ failures.push({ kind: 'css-draft-engine', message: err.message })
69
+ }
70
+
71
+ printReport({ failures, warnings, infos })
72
+ process.exit(failures.length > 0 ? 1 : 0)
73
+ }
74
+
75
+ // ---------- §3 CSS variable 校验 ----------
76
+
77
+ function verifyCssVariables(text, failures, infos) {
78
+ const section = extractSection(text, /^##\s+4\.\s+CSS\s+变量映射表/m)
79
+ if (!section) {
80
+ infos.push({ kind: 'css-variable', message: '§4 CSS 变量映射表未找到,跳过变量校验' })
81
+ return
82
+ }
83
+
84
+ const records = loadTokenRecords()
85
+ const fullPaths = new Set(records.map((r) => `${r.setName}.${r.pathStr}`))
86
+ const cssVariables = new Set(records.map((r) => r.cssVar).filter(Boolean))
87
+
88
+ const rows = parseTableRows(section, 8)
89
+ let checked = 0
90
+ for (const row of rows) {
91
+ if (!row) continue
92
+ const cssVariable = stripCodeMarkers(row.cells[2])
93
+ const themePath = stripCodeMarkers(row.cells[3])
94
+ const hitWay = stripCodeMarkers(row.cells[5])
95
+ if (hitWay === '未命中') continue
96
+ if (
97
+ (!cssVariable || cssVariable === '-' || cssVariable === '—') &&
98
+ (!themePath || themePath === '-')
99
+ ) {
100
+ continue
101
+ }
102
+ checked++
103
+ if (!cssVariables.has(cssVariable)) {
104
+ failures.push({
105
+ kind: 'css-variable',
106
+ ref: cssVariable,
107
+ message: `§4 CSS variable "${cssVariable}" 不在 theme/dark CSS 源中`
108
+ })
109
+ }
110
+ if (themePath && themePath !== '-' && themePath !== '—' && !fullPaths.has(themePath)) {
111
+ failures.push({
112
+ kind: 'css-variable',
113
+ ref: themePath,
114
+ message: `§4 /theme 路径 "${themePath}" 不在当前 theme 记录中`
115
+ })
116
+ }
117
+ }
118
+ infos.push({ kind: 'css-variable', message: `§4 CSS variable 校验完成,${checked} 行被检查` })
119
+ }
120
+
121
+ function loadTokenRecords() {
122
+ return loadCurrentTokenRecords().records
123
+ }
124
+
125
+ // ---------- CSS 草案校验 ----------
126
+
127
+ function verifyCssDraft(text, planPath, failures, warnings, infos) {
128
+ const cssDraftPath = readFrontmatterValue(text, 'cssDraftPath') ?? './figma-plan.css'
129
+ const absolutePath = path.resolve(path.dirname(planPath), cssDraftPath)
130
+ if (!fs.existsSync(absolutePath)) {
131
+ failures.push({
132
+ kind: 'css-draft',
133
+ message: `CSS 草案不存在:${cssDraftPath}`
134
+ })
135
+ return
136
+ }
137
+
138
+ const source = fs.readFileSync(absolutePath, 'utf-8')
139
+ const forbidden = source.match(/@pandacss|styled-system|css\(|cva\(|sva\(/g) ?? []
140
+ if (forbidden.length > 0) {
141
+ failures.push({
142
+ kind: 'css-draft',
143
+ message: `CSS 草案包含 Panda 相关内容:${[...new Set(forbidden)].join(', ')}`
144
+ })
145
+ }
146
+
147
+ if (!/var\(--[a-zA-Z0-9_-]+/.test(source)) {
148
+ warnings.push({
149
+ kind: 'css-draft',
150
+ message: 'CSS 草案未发现 CSS variable 引用;请确认设计稿是否确实没有可复用 /theme 变量'
151
+ })
152
+ }
153
+
154
+ infos.push({ kind: 'css-draft', message: `CSS 草案已检查:${cssDraftPath}` })
155
+ }
156
+
157
+ // ---------- §2 apex-ui 校验 ----------
158
+
159
+ function verifyApexUi(text, failures, warnings, infos) {
160
+ const section = extractSection(text, /^##\s+2\.\s+组件映射决策/m)
161
+ if (!section) {
162
+ infos.push({ kind: 'apex-ui', message: '§2 组件映射决策未找到,跳过 apex-ui 校验' })
163
+ return
164
+ }
165
+ if (!fs.existsSync(APEX_UI_DIST)) {
166
+ warnings.push({
167
+ kind: 'apex-ui',
168
+ message: `apex-ui llm 文档目录不存在:${APEX_UI_DIST},跳过校验`
169
+ })
170
+ return
171
+ }
172
+
173
+ // 抓代码块中的 JSX 自闭合 / 普通标签,例如 `<Button type="primary" size="md" />` 或 `<Card extra={<X />}>`
174
+ const tagRe = /<([A-Z][A-Za-z0-9]*)((?:\s+[a-zA-Z][\w-]*=(?:"[^"]*"|\{[^}]*\}))*)\s*\/?>/g
175
+ let m
176
+ const seen = new Set()
177
+ while ((m = tagRe.exec(section)) !== null) {
178
+ const tag = m[1]
179
+ const attrs = parseAttrs(m[2])
180
+ const key = `${tag}|${Object.keys(attrs).sort().join(',')}`
181
+ if (seen.has(key)) continue
182
+ seen.add(key)
183
+ checkApexComponent(tag, attrs, { failures, warnings, infos })
184
+ }
185
+ }
186
+
187
+ function parseAttrs(raw) {
188
+ const out = {}
189
+ const re = /([a-zA-Z][\w-]*)=(?:"([^"]*)"|\{([^}]*)\})/g
190
+ let m
191
+ while ((m = re.exec(raw)) !== null) {
192
+ out[m[1]] = m[2] !== undefined ? { kind: 'string', value: m[2] } : { kind: 'expr', value: m[3] }
193
+ }
194
+ return out
195
+ }
196
+
197
+ function checkApexComponent(tag, attrs, { failures, warnings, infos }) {
198
+ const docPath = findApexDoc(tag)
199
+ if (!docPath) {
200
+ warnings.push({
201
+ kind: 'apex-ui',
202
+ message: `§2 提到 <${tag} /> 但 apex-ui llm 文档里找不到,可能不是组件库组件`
203
+ })
204
+ return
205
+ }
206
+ const propTable = parseApiTable(fs.readFileSync(docPath, 'utf-8'))
207
+ if (!propTable) {
208
+ warnings.push({
209
+ kind: 'apex-ui',
210
+ message: `${docPath} 未找到 ## API 表,无法验证 <${tag} /> 的 props`
211
+ })
212
+ return
213
+ }
214
+ for (const [propName, propValue] of Object.entries(attrs)) {
215
+ const meta = propTable[propName]
216
+ if (!meta) {
217
+ failures.push({
218
+ kind: 'apex-ui',
219
+ ref: `<${tag} ${propName}>`,
220
+ message: `${tag} 不存在 prop "${propName}"(参见 ${docPath})`
221
+ })
222
+ continue
223
+ }
224
+ if (propValue.kind === 'string' && meta.enumValues) {
225
+ if (!meta.enumValues.includes(propValue.value)) {
226
+ failures.push({
227
+ kind: 'apex-ui',
228
+ ref: `<${tag} ${propName}="${propValue.value}">`,
229
+ message: `${tag}.${propName} 不接受 "${propValue.value}";有效值:${meta.enumValues.join(' | ')}`
230
+ })
231
+ }
232
+ }
233
+ }
234
+ infos.push({ kind: 'apex-ui', message: `<${tag} /> ${Object.keys(attrs).length} props 已校验` })
235
+ }
236
+
237
+ // ---------- §2.5 Icon 校验 ----------
238
+
239
+ function verifyIcons(text, failures, warnings, infos) {
240
+ const section = extractSection(text, /^##\s+2\.5\s+Icon\s+资产匹配/m)
241
+ if (!section) {
242
+ failures.push({
243
+ kind: 'icon',
244
+ message: '§2.5 Icon 资产匹配未找到;plan 阶段必须输出 icon 清单和确认状态'
245
+ })
246
+ return
247
+ }
248
+
249
+ const rows = parseIconRows(section)
250
+ if (rows.length === 0) {
251
+ warnings.push({
252
+ kind: 'icon',
253
+ message: '§2.5 未解析到 icon 表格行;若设计稿无 icon,请明确写一行状态为 `忽略` 的说明'
254
+ })
255
+ return
256
+ }
257
+
258
+ let missing = 0
259
+ let available = 0
260
+ let ignored = 0
261
+ for (const row of rows) {
262
+ const figmaNode = stripCodeMarkers(row.cells[0])
263
+ const semantic = stripCodeMarkers(row.cells[2])
264
+ const matchedIcon = stripCodeMarkers(row.cells[3])
265
+ const status = stripCodeMarkers(row.cells[5])
266
+ const confirmation = stripCodeMarkers(row.cells[6])
267
+
268
+ if (status === '可用') {
269
+ available++
270
+ if (!matchedIcon || matchedIcon === '-' || matchedIcon === '—') {
271
+ failures.push({
272
+ kind: 'icon',
273
+ message: `§2.5 ${figmaNode || semantic || '未知 icon'} 标记为可用,但未填写匹配仓库 icon`
274
+ })
275
+ }
276
+ continue
277
+ }
278
+
279
+ if (status === '缺失') {
280
+ missing++
281
+ if (!confirmation || confirmation === '-' || confirmation === '待确认') {
282
+ failures.push({
283
+ kind: 'icon',
284
+ message: `§2.5 ${figmaNode || semantic || '未知 icon'} 缺失且未获得用户确认`
285
+ })
286
+ }
287
+ continue
288
+ }
289
+
290
+ if (status === '忽略') {
291
+ ignored++
292
+ if (!confirmation || confirmation === '-' || confirmation === '待确认') {
293
+ failures.push({
294
+ kind: 'icon',
295
+ message: `§2.5 ${figmaNode || semantic || '未知 icon'} 标记为忽略,但未写明用户确认`
296
+ })
297
+ }
298
+ continue
299
+ }
300
+
301
+ warnings.push({
302
+ kind: 'icon',
303
+ message: `§2.5 ${figmaNode || semantic || '未知 icon'} 使用了未知状态 "${status}"`
304
+ })
305
+ }
306
+
307
+ infos.push({
308
+ kind: 'icon',
309
+ message: `§2.5 icon 校验完成:可用 ${available} · 缺失 ${missing} · 忽略 ${ignored}`
310
+ })
311
+ }
312
+
313
+ function parseIconRows(section) {
314
+ return parseTableRows(section, 7).filter((row) => {
315
+ const first = stripCodeMarkers(row.cells[0])
316
+ if (!first) return false
317
+ if (first === 'Figma 节点') return false
318
+ if (first.startsWith('<')) return false
319
+ return true
320
+ })
321
+ }
322
+
323
+ function findApexDoc(tagName) {
324
+ const candidates = [
325
+ tagName,
326
+ tagName
327
+ .replace(/([A-Z])/g, '-$1')
328
+ .toLowerCase()
329
+ .replace(/^-/, '')
330
+ ]
331
+ for (const c of candidates) {
332
+ const p = path.join(APEX_UI_DIST, `${c}.txt`)
333
+ if (fs.existsSync(p)) return p
334
+ }
335
+ return null
336
+ }
337
+
338
+ function splitMarkdownRow(row) {
339
+ // 表格列的分隔是真 `|`;说明里 `\|` / `<br>` 等不应当切分。
340
+ // 朴素做法:取 `|` 前后非 `\` 的位置作为分隔点。
341
+ const cells = []
342
+ let cur = ''
343
+ for (let i = 0; i < row.length; i++) {
344
+ const ch = row[i]
345
+ if (ch === '\\' && row[i + 1] === '|') {
346
+ cur += '|'
347
+ i++
348
+ continue
349
+ }
350
+ if (ch === '|') {
351
+ cells.push(cur)
352
+ cur = ''
353
+ continue
354
+ }
355
+ cur += ch
356
+ }
357
+ cells.push(cur)
358
+ return cells.slice(1, -1).map((c) => c.trim())
359
+ }
360
+
361
+ function parseApiTable(docText) {
362
+ const section = extractSection(docText, /^##\s+API\s*$/m)
363
+ if (!section) return null
364
+ const lineRe = /^\|[^\n]+\|$/gm
365
+ const rows = section.match(lineRe) ?? []
366
+ const out = {}
367
+ for (const row of rows) {
368
+ if (/^\|\s*-+/.test(row)) continue
369
+ if (/^\|\s*属性\s*\|/.test(row)) continue
370
+ const cells = splitMarkdownRow(row)
371
+ if (cells.length < 3) continue
372
+ const name = cells[0].replace(/`/g, '').trim()
373
+ if (!name) continue
374
+ const typeCell = cells[2] ?? ''
375
+ const enumValues = extractEnumValues(typeCell)
376
+ out[name] = { type: typeCell, enumValues }
377
+ }
378
+ return out
379
+ }
380
+
381
+ function extractEnumValues(typeCell) {
382
+ // 形如 `'default' \| 'primary' \| 'danger'`
383
+ const strLits = [...typeCell.matchAll(/'([^']+)'/g)].map((m) => m[1])
384
+ if (strLits.length >= 2 && typeCell.includes('|')) return strLits
385
+ return null
386
+ }
387
+
388
+ // ---------- 公共工具 ----------
389
+
390
+ function extractSection(text, headRe) {
391
+ const headMatch = headRe.exec(text)
392
+ if (!headMatch) return null
393
+ const start = headMatch.index + headMatch[0].length
394
+ const tail = text.slice(start)
395
+ const nextRe = /^##\s/m
396
+ const nextMatch = nextRe.exec(tail)
397
+ return nextMatch ? tail.slice(0, nextMatch.index) : tail
398
+ }
399
+
400
+ function parseTableRows(section, minCells) {
401
+ const lineRe = /^\|[^\n]+\|$/gm
402
+ const rows = section.match(lineRe) ?? []
403
+ return rows
404
+ .filter((row) => !/^\|\s*-+/.test(row))
405
+ .filter((row) => !/^\|\s*来源|^\|\s*属性|^\|\s*#/.test(row))
406
+ .filter((row) => !/^\|\s*<|^\|\s*\(占位/.test(row))
407
+ .map((row) => {
408
+ const cells = row
409
+ .split('|')
410
+ .slice(1, -1)
411
+ .map((c) => c.trim())
412
+ if (cells.length < minCells) return null
413
+ return { cells }
414
+ })
415
+ .filter(Boolean)
416
+ }
417
+
418
+ function stripCodeMarkers(s) {
419
+ return (s ?? '').replace(/`/g, '').trim()
420
+ }
421
+
422
+ function readFrontmatterValue(text, key) {
423
+ if (!text.startsWith('---')) return null
424
+ const end = text.indexOf('\n---', 3)
425
+ if (end === -1) return null
426
+ const frontmatter = text.slice(3, end)
427
+ const line = frontmatter.split('\n').find((item) => item.trim().startsWith(`${key}:`))
428
+ if (!line) return null
429
+ return (
430
+ line
431
+ .slice(line.indexOf(':') + 1)
432
+ .trim()
433
+ .replace(/^['"]|['"]$/g, '') || null
434
+ )
435
+ }
436
+
437
+ function printReport({ failures, warnings, infos }) {
438
+ const groups = [
439
+ ['❌ FAIL', failures],
440
+ ['⚠️ WARN', warnings],
441
+ ['ℹ️ INFO', infos]
442
+ ]
443
+ for (const [label, items] of groups) {
444
+ if (items.length === 0) continue
445
+ console.log(`\n${label}`)
446
+ for (const it of items) {
447
+ console.log(` · [${it.kind}] ${it.message}`)
448
+ }
449
+ }
450
+ console.log(
451
+ `\n[figma:verify-plan] ${failures.length} fail · ${warnings.length} warn · ${infos.length} info`
452
+ )
453
+ if (failures.length === 0) {
454
+ console.log('✅ PLAN.md passes reverse verification.')
455
+ }
456
+ }
457
+
458
+ main()
@@ -0,0 +1,13 @@
1
+ # Figma Sync Snapshots
2
+
3
+ 此目录用于存放 figmaSync 相关的本地报告。
4
+
5
+ ## 文件说明
6
+
7
+ - `last-sync.md`:最近一次 figmaSync 同步摘要。
8
+
9
+ ## 注意
10
+
11
+ - 当前 token 变更以 `theme/dark/*.css` diff 为准。
12
+ - 不要手动修改报告;需要恢复旧 token 迁移能力时再单独设计迁移方案。
13
+ - 修改后必须走 PR review,禁止直接 push 到 main。