@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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/cli.mjs +9 -0
- package/package.json +39 -0
- package/skills/artifact/README.md +23 -0
- package/skills/execution/devFlow/SKILL.md +341 -0
- package/skills/execution/devFlow/agents/openai.yaml +7 -0
- package/skills/execution/devFlow/domains/backend/references/api-tech.md +173 -0
- package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.mjs +170 -0
- package/skills/execution/devFlow/domains/backend/scripts/check_api_tech_doc.test.mjs +89 -0
- package/skills/execution/devFlow/domains/backend/templates/api-tech.md +164 -0
- package/skills/execution/devFlow/domains/frontend/references/contract-check.md +270 -0
- package/skills/execution/devFlow/domains/frontend/references/foundation-freeze.md +92 -0
- package/skills/execution/devFlow/domains/frontend/references/lightweight-flow.md +55 -0
- package/skills/execution/devFlow/domains/frontend/references/page-build.md +268 -0
- package/skills/execution/devFlow/domains/frontend/references/page-tech.md +414 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/field-common.md +69 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-complex.md +105 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-id-code.md +44 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-number-date.md +106 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-relation.md +144 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-selectors.md +102 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/fields-text.md +130 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/index.md +42 -0
- package/skills/execution/devFlow/domains/frontend/references/product-design-specs/interaction.md +61 -0
- package/skills/execution/devFlow/domains/frontend/scripts/check_page_tech_doc.mjs +82 -0
- package/skills/execution/devFlow/domains/frontend/scripts/contract_check_static.mjs +145 -0
- package/skills/execution/devFlow/domains/frontend/scripts/generate_foundation_summary.mjs +224 -0
- package/skills/execution/devFlow/domains/frontend/templates/contract-report.md +66 -0
- package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l0.md +7 -0
- package/skills/execution/devFlow/domains/frontend/templates/mini-plan-l1.md +9 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/component.tsx.tpl +8 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/constants.ts.tpl +5 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/index.ts.tpl +1 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/route.tsx.tpl +11 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/service.ts.tpl +17 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-build/types.ts.tpl +11 -0
- package/skills/execution/devFlow/domains/frontend/templates/page-tech.md +103 -0
- package/skills/execution/devFlow/domains/requirement/references/prd-review.md +129 -0
- package/skills/execution/devFlow/domains/requirement/templates/prd-review.md +57 -0
- package/skills/execution/figmaSync/SKILL.md +349 -0
- package/skills/execution/figmaSync/chapters/apply.md +177 -0
- package/skills/execution/figmaSync/chapters/plan.md +359 -0
- package/skills/execution/figmaSync/chapters/prepare.md +47 -0
- package/skills/execution/figmaSync/examples/native-css.md +129 -0
- package/skills/execution/figmaSync/examples/plan-doc-sample.md +39 -0
- package/skills/execution/figmaSync/scripts/_shared/session-log.mjs +82 -0
- package/skills/execution/figmaSync/scripts/_shared/token-resolver.mjs +132 -0
- package/skills/execution/figmaSync/scripts/_shared/token-source.mjs +145 -0
- package/skills/execution/figmaSync/scripts/figma-sync-report.mjs +448 -0
- package/skills/execution/figmaSync/scripts/icon-inventory.mjs +165 -0
- package/skills/execution/figmaSync/scripts/lookup-var.mjs +184 -0
- package/skills/execution/figmaSync/scripts/match-token.mjs +210 -0
- package/skills/execution/figmaSync/scripts/prepare-check.mjs +290 -0
- package/skills/execution/figmaSync/scripts/verify-plan.mjs +458 -0
- package/skills/execution/figmaSync/snapshots/README.md +13 -0
- package/skills/execution/figmaSync/snapshots/last-sync.md +1538 -0
- package/skills/execution/figmaSync/templates/PLAN.md.tpl +227 -0
- package/skills/review/consistency-checker/SKILL.md +112 -0
- package/skills/review/consistency-checker/rules/README.md +16 -0
- package/src/cli/agents.mjs +16 -0
- package/src/cli/copy.mjs +95 -0
- package/src/cli/install.mjs +131 -0
- package/src/cli/prompts.mjs +66 -0
- package/tools/lark/README.md +78 -0
- package/tools/lark/references/lark-doc.md +156 -0
- package/tools/lark/references/lark-read.md +235 -0
- package/tools/lark/references/prepare.md +147 -0
- package/tools/lark/scripts/lark_api.mjs +404 -0
- package/tools/lark/scripts/lark_api.test.mjs +63 -0
- package/tools/lark/scripts/lark_check_permissions.mjs +52 -0
- package/tools/lark/scripts/lark_publish_doc.mjs +299 -0
- package/tools/lark/scripts/lark_read_docx.mjs +328 -0
- 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()
|