@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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma Sync Report
|
|
3
|
+
*
|
|
4
|
+
* 把本次 figmaSync 任务的「AI 做了什么」与「产物长什么样」交叉量化输出。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* pnpm figma:report # 默认 plan 命令、写到 .agent/skills/figmaSync/
|
|
8
|
+
* pnpm figma:report --command=plan|apply # 报告文件名前缀:session-report-<command>.md
|
|
9
|
+
* pnpm figma:report --target-dir=src/foo # 同时把报告复制一份到指定目录
|
|
10
|
+
* pnpm figma:report --plan=src/foo/PLAN.md # 解析该 PLAN.md §4 表格,统计 variables 来源
|
|
11
|
+
* pnpm figma:report --reset # 清空 session log(任务开始时调)
|
|
12
|
+
*
|
|
13
|
+
* 主指标(新版排序):
|
|
14
|
+
* 1. 硬编码计数(产物侧)—— 需要解释
|
|
15
|
+
* 2. CSS variables 命中数(解析 PLAN §4 表格)
|
|
16
|
+
* 3. 脚本调用率 lookupVar / matchToken —— 辅助参考
|
|
17
|
+
*
|
|
18
|
+
* 告警条件:
|
|
19
|
+
* - 硬编码 > 0 → ⚠️
|
|
20
|
+
* - Fast Path 覆盖率 < 50% 且 PLAN.md §4 无 variables 来源 → ⚠️
|
|
21
|
+
*/
|
|
22
|
+
import { execSync } from 'node:child_process'
|
|
23
|
+
import fs from 'node:fs'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
|
|
26
|
+
import { readSessionLog, resetSessionLog } from './_shared/session-log.mjs'
|
|
27
|
+
|
|
28
|
+
const DEFAULT_REPORT_DIR = '.agent/skills/figmaSync'
|
|
29
|
+
const FAST_PATH_LOW_THRESHOLD = 0.5
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const out = { reset: false, command: 'plan', targetDir: null, plan: null }
|
|
33
|
+
for (const arg of argv.slice(2)) {
|
|
34
|
+
if (arg === '--reset') out.reset = true
|
|
35
|
+
else if (arg.startsWith('--command=')) out.command = arg.slice('--command='.length)
|
|
36
|
+
else if (arg.startsWith('--target-dir=')) out.targetDir = arg.slice('--target-dir='.length)
|
|
37
|
+
else if (arg.startsWith('--plan=')) out.plan = arg.slice('--plan='.length)
|
|
38
|
+
}
|
|
39
|
+
if (!['plan', 'apply'].includes(out.command)) out.command = 'plan'
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getChangedFiles() {
|
|
44
|
+
try {
|
|
45
|
+
const out = execSync('git status --porcelain', { encoding: 'utf-8' })
|
|
46
|
+
return out
|
|
47
|
+
.split('\n')
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((line) => ({
|
|
50
|
+
status: line.slice(0, 2).trim(),
|
|
51
|
+
file: line.slice(3).replace(/^"|"$/g, '')
|
|
52
|
+
}))
|
|
53
|
+
.filter((c) => /\.(ts|tsx|css|md)$/.test(c.file))
|
|
54
|
+
.filter((c) => fs.existsSync(c.file))
|
|
55
|
+
} catch {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function auditFile(filePath) {
|
|
61
|
+
const source = fs.readFileSync(filePath, 'utf-8')
|
|
62
|
+
const apexImports = []
|
|
63
|
+
const apexImportRe = /import\s+\{([^}]+)\}\s+from\s+['"]@frontend\/apex-ui--react['"]/g
|
|
64
|
+
let m
|
|
65
|
+
while ((m = apexImportRe.exec(source)) !== null) {
|
|
66
|
+
const names = m[1]
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((n) => n.replace(/\bas\s+\w+/, '').trim())
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
apexImports.push(...names)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cssVariables = {}
|
|
74
|
+
const cssVarRe = /var\(\s*(--[a-zA-Z0-9_-]+)/g
|
|
75
|
+
while ((m = cssVarRe.exec(source)) !== null) {
|
|
76
|
+
cssVariables[m[1]] = (cssVariables[m[1]] ?? 0) + 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hardcodedRe = /(['"`])(#[0-9a-fA-F]{3,8})\1|\b(\d{2,4})px\b/g
|
|
80
|
+
const hardcoded = []
|
|
81
|
+
while ((m = hardcodedRe.exec(source)) !== null) {
|
|
82
|
+
const raw = m[2] ?? m[3] + 'px'
|
|
83
|
+
const before = source.lastIndexOf('\n', m.index)
|
|
84
|
+
const lineIdx = source.slice(0, m.index).split('\n').length
|
|
85
|
+
const snippet = source.slice(before + 1, source.indexOf('\n', m.index)).trim()
|
|
86
|
+
hardcoded.push({ value: raw, line: lineIdx, snippet })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const commonRefs = []
|
|
90
|
+
const commonImportRe = /from\s+['"]([^'"]*src\/common[^'"]*|[^'"]*\/common\/[^'"]*)['"]/g
|
|
91
|
+
while ((m = commonImportRe.exec(source)) !== null) commonRefs.push(m[1])
|
|
92
|
+
|
|
93
|
+
const pandaForbidden = []
|
|
94
|
+
const forbiddenRe = /@pandacss|styled-system|css\(|cva\(|sva\(/g
|
|
95
|
+
while ((m = forbiddenRe.exec(source)) !== null) {
|
|
96
|
+
const lineIdx = source.slice(0, m.index).split('\n').length
|
|
97
|
+
pandaForbidden.push({ value: m[0], line: lineIdx })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { apexImports, cssVariables, hardcoded, commonRefs, pandaForbidden }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function aggregate(files) {
|
|
104
|
+
const acc = {
|
|
105
|
+
apexImports: {},
|
|
106
|
+
cssVariables: {},
|
|
107
|
+
hardcoded: [],
|
|
108
|
+
commonRefs: {},
|
|
109
|
+
pandaForbidden: [],
|
|
110
|
+
files: []
|
|
111
|
+
}
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const r = auditFile(file)
|
|
114
|
+
if (
|
|
115
|
+
r.apexImports.length === 0 &&
|
|
116
|
+
Object.keys(r.cssVariables).length === 0 &&
|
|
117
|
+
r.hardcoded.length === 0 &&
|
|
118
|
+
r.commonRefs.length === 0 &&
|
|
119
|
+
r.pandaForbidden.length === 0
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
acc.files.push(file)
|
|
123
|
+
for (const name of r.apexImports) acc.apexImports[name] = (acc.apexImports[name] ?? 0) + 1
|
|
124
|
+
for (const [k, v] of Object.entries(r.cssVariables))
|
|
125
|
+
acc.cssVariables[k] = (acc.cssVariables[k] ?? 0) + v
|
|
126
|
+
for (const h of r.hardcoded) acc.hardcoded.push({ ...h, file })
|
|
127
|
+
for (const ref of r.commonRefs) acc.commonRefs[ref] = (acc.commonRefs[ref] ?? 0) + 1
|
|
128
|
+
for (const hit of r.pandaForbidden) acc.pandaForbidden.push({ ...hit, file })
|
|
129
|
+
}
|
|
130
|
+
return acc
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function auditForbiddenFiles(changedFiles) {
|
|
134
|
+
const forbidden = [
|
|
135
|
+
'src/routeTree.gen.ts',
|
|
136
|
+
'src/services/request.ts',
|
|
137
|
+
'src/shared/wujie-bridge.ts',
|
|
138
|
+
'src/stores/shell-store.ts'
|
|
139
|
+
]
|
|
140
|
+
return changedFiles
|
|
141
|
+
.map((c) => c.file)
|
|
142
|
+
.filter((file) => forbidden.includes(file) || file.startsWith('src/common/'))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function tallyLog(log) {
|
|
146
|
+
const summary = {
|
|
147
|
+
lookupVar: { hit: 0, miss: 0, byLayer: {} },
|
|
148
|
+
matchToken: { hit: 0, miss: 0, byLayer: {} },
|
|
149
|
+
misses: []
|
|
150
|
+
}
|
|
151
|
+
for (const entry of log) {
|
|
152
|
+
const target =
|
|
153
|
+
entry.script === 'lookup-var'
|
|
154
|
+
? summary.lookupVar
|
|
155
|
+
: entry.script === 'match-token'
|
|
156
|
+
? summary.matchToken
|
|
157
|
+
: null
|
|
158
|
+
if (!target) continue
|
|
159
|
+
if (entry.hit) {
|
|
160
|
+
target.hit++
|
|
161
|
+
const layer = entry.layer ?? 'unknown'
|
|
162
|
+
target.byLayer[layer] = (target.byLayer[layer] ?? 0) + 1
|
|
163
|
+
} else {
|
|
164
|
+
target.miss++
|
|
165
|
+
summary.misses.push({ script: entry.script, input: entry.input })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return summary
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 解析 PLAN.md §4 「CSS 变量映射表」,统计「命中方式」列里 variables 来源出现次数。
|
|
173
|
+
* 表格契约(PLAN.md.tpl 强制):列顺序为 来源 | 类型 | CSS variable | /theme 路径 | 原始值 | 命中方式 | nodeId | designContextStatus。
|
|
174
|
+
* 命中方式取值约定:
|
|
175
|
+
* - "Fast Path 0 (variables)" → MCP get_variable_defs 直接命中
|
|
176
|
+
* - "Fast Path 1" → lookup-var 命中
|
|
177
|
+
* - "Slow Path" → match-token 命中
|
|
178
|
+
* - "未命中" → 降级
|
|
179
|
+
*/
|
|
180
|
+
function parsePlanTokenTable(planPath) {
|
|
181
|
+
if (!planPath || !fs.existsSync(planPath)) {
|
|
182
|
+
return { source: 'none', stats: null }
|
|
183
|
+
}
|
|
184
|
+
const text = fs.readFileSync(planPath, 'utf-8')
|
|
185
|
+
// 切 §4 整段:从 "## 4. CSS 变量映射表" 到下一个 "## " 之间(不含下一个标题)
|
|
186
|
+
const headRe = /^##\s+4\.\s+CSS\s+变量映射表\s*$/m
|
|
187
|
+
const headMatch = headRe.exec(text)
|
|
188
|
+
if (!headMatch) return { source: planPath, stats: null }
|
|
189
|
+
const start = headMatch.index + headMatch[0].length
|
|
190
|
+
const tail = text.slice(start)
|
|
191
|
+
const nextRe = /^##\s/m
|
|
192
|
+
const nextMatch = nextRe.exec(tail)
|
|
193
|
+
const section = nextMatch ? tail.slice(0, nextMatch.index) : tail
|
|
194
|
+
|
|
195
|
+
const lineRe = /^\|[^\n]+\|$/gm
|
|
196
|
+
const rows = section.match(lineRe) ?? []
|
|
197
|
+
const stats = {
|
|
198
|
+
'Fast Path 0 (variables)': 0,
|
|
199
|
+
'Fast Path 1 (lookup-var)': 0,
|
|
200
|
+
'Slow Path (value-match)': 0,
|
|
201
|
+
未命中: 0
|
|
202
|
+
}
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
if (/^\|\s*-+/.test(row)) continue
|
|
205
|
+
if (/来源(Figma|(Figma))|命中方式|CSS variable/.test(row)) continue
|
|
206
|
+
if (/^\|\s*<|^\|\s*\(占位/.test(row)) continue
|
|
207
|
+
const cells = row
|
|
208
|
+
.split('|')
|
|
209
|
+
.slice(1, -1)
|
|
210
|
+
.map((c) => c.trim())
|
|
211
|
+
const hitWay = cells[5] ?? ''
|
|
212
|
+
for (const key of Object.keys(stats)) {
|
|
213
|
+
if (hitWay.includes(key)) stats[key]++
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { source: planPath, stats }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderReport({
|
|
220
|
+
log,
|
|
221
|
+
audit,
|
|
222
|
+
planTokens,
|
|
223
|
+
generatedAt,
|
|
224
|
+
command,
|
|
225
|
+
changedFiles,
|
|
226
|
+
forbiddenFiles
|
|
227
|
+
}) {
|
|
228
|
+
const lines = []
|
|
229
|
+
lines.push(`# figmaSync Session Report (${command}) — ${generatedAt}`)
|
|
230
|
+
lines.push('')
|
|
231
|
+
|
|
232
|
+
lines.push('## Review 摘要')
|
|
233
|
+
lines.push('')
|
|
234
|
+
lines.push(`- 改动文件:${changedFiles.length} 个`)
|
|
235
|
+
lines.push(
|
|
236
|
+
`- 禁止 / 高风险文件命中:${forbiddenFiles.length} 个${forbiddenFiles.length === 0 ? ' ✅' : ' ⚠️'}`
|
|
237
|
+
)
|
|
238
|
+
lines.push(
|
|
239
|
+
`- Panda / styled-system 禁用内容:${audit.pandaForbidden.length} 处${audit.pandaForbidden.length === 0 ? ' ✅' : ' ⚠️'}`
|
|
240
|
+
)
|
|
241
|
+
lines.push(
|
|
242
|
+
`- 硬编码嫌疑:${audit.hardcoded.length} 处${audit.hardcoded.length === 0 ? ' ✅' : ' ⚠️'}`
|
|
243
|
+
)
|
|
244
|
+
lines.push('')
|
|
245
|
+
|
|
246
|
+
lines.push('## 改动文件')
|
|
247
|
+
lines.push('')
|
|
248
|
+
if (changedFiles.length === 0) {
|
|
249
|
+
lines.push('- 未检测到工作树改动')
|
|
250
|
+
} else {
|
|
251
|
+
for (const file of changedFiles) lines.push(`- ${file.status || '??'} ${file.file}`)
|
|
252
|
+
}
|
|
253
|
+
lines.push('')
|
|
254
|
+
|
|
255
|
+
lines.push('## 成功项 / 跳过项 / 风险项')
|
|
256
|
+
lines.push('')
|
|
257
|
+
const success = []
|
|
258
|
+
const skipped = []
|
|
259
|
+
const risks = []
|
|
260
|
+
if (Object.keys(audit.cssVariables).length > 0) success.push('使用 CSS variables')
|
|
261
|
+
else skipped.push('未检测到 CSS variable 引用')
|
|
262
|
+
if (Object.keys(audit.apexImports).length > 0) success.push('复用 Apex UI 组件')
|
|
263
|
+
else skipped.push('未检测到 Apex UI import')
|
|
264
|
+
if (Object.keys(audit.commonRefs).length > 0) success.push('复用 common module')
|
|
265
|
+
if (audit.hardcoded.length > 0) risks.push('存在硬编码嫌疑,需要解释或确认')
|
|
266
|
+
if (audit.pandaForbidden.length > 0) risks.push('存在 Panda / styled-system 禁用内容')
|
|
267
|
+
if (forbiddenFiles.length > 0) risks.push('命中禁止或高风险文件,需要 Human 再确认')
|
|
268
|
+
lines.push(`- 成功项:${success.length > 0 ? success.join(';') : '无自动识别项'}`)
|
|
269
|
+
lines.push(`- 跳过项:${skipped.length > 0 ? skipped.join(';') : '无'}`)
|
|
270
|
+
lines.push(`- 风险项:${risks.length > 0 ? risks.join(';') : '无'}`)
|
|
271
|
+
lines.push('')
|
|
272
|
+
|
|
273
|
+
if (forbiddenFiles.length > 0) {
|
|
274
|
+
lines.push('## 禁止 / 高风险文件')
|
|
275
|
+
lines.push('')
|
|
276
|
+
for (const file of forbiddenFiles) lines.push(`- ${file}`)
|
|
277
|
+
lines.push('')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------- 主指标 ----------
|
|
281
|
+
lines.push('## 🎯 主指标')
|
|
282
|
+
lines.push('')
|
|
283
|
+
const hardcodedCount = audit.hardcoded.length
|
|
284
|
+
lines.push(`- 硬编码嫌疑:${hardcodedCount} ${hardcodedCount === 0 ? '✅' : '⚠️ 需解释'}`)
|
|
285
|
+
|
|
286
|
+
let variablesHits = 0
|
|
287
|
+
if (planTokens.stats) {
|
|
288
|
+
variablesHits = planTokens.stats['Fast Path 0 (variables)'] ?? 0
|
|
289
|
+
const total = Object.values(planTokens.stats).reduce((a, b) => a + b, 0)
|
|
290
|
+
lines.push(`- PLAN §4 CSS variable 命中分布(共 ${total} 行):`)
|
|
291
|
+
for (const [key, n] of Object.entries(planTokens.stats)) {
|
|
292
|
+
lines.push(` - ${key}: ${n}`)
|
|
293
|
+
}
|
|
294
|
+
} else if (planTokens.source !== 'none') {
|
|
295
|
+
lines.push(`- PLAN §4 CSS variable 表格解析失败(${planTokens.source})`)
|
|
296
|
+
} else {
|
|
297
|
+
lines.push('- PLAN §4 CSS variable 表格:未指定 --plan 路径,跳过解析')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const fastTotal = log.lookupVar.hit + log.lookupVar.miss
|
|
301
|
+
const allLookupTotal = fastTotal + log.matchToken.hit + log.matchToken.miss
|
|
302
|
+
const fastCoverage = allLookupTotal > 0 ? log.lookupVar.hit / allLookupTotal : null
|
|
303
|
+
if (fastCoverage !== null) {
|
|
304
|
+
const pct = Math.round(fastCoverage * 100)
|
|
305
|
+
lines.push(`- 脚本调用率(辅助参考):lookup-var ${pct}% / 总查询 ${allLookupTotal}`)
|
|
306
|
+
if (fastCoverage < FAST_PATH_LOW_THRESHOLD && variablesHits === 0) {
|
|
307
|
+
lines.push(
|
|
308
|
+
' - ⚠️ 调用率 < 50% 且 PLAN §4 无 variables 来源;AI 既没查脚本也没标 variables 命中'
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
lines.push('- 脚本调用率(辅助参考):本 session 未触发匹配脚本')
|
|
313
|
+
}
|
|
314
|
+
lines.push('')
|
|
315
|
+
|
|
316
|
+
// ---------- AI 行为 ----------
|
|
317
|
+
lines.push('## 🔎 AI 调用轨迹')
|
|
318
|
+
lines.push('')
|
|
319
|
+
const total = log.lookupVar.hit + log.lookupVar.miss + log.matchToken.hit + log.matchToken.miss
|
|
320
|
+
if (total === 0) {
|
|
321
|
+
lines.push('- 本 session 未调用 lookup-var / match-token')
|
|
322
|
+
} else {
|
|
323
|
+
lines.push(`- lookup-var 命中:${log.lookupVar.hit} / ${fastTotal}`)
|
|
324
|
+
for (const [layer, n] of Object.entries(log.lookupVar.byLayer)) {
|
|
325
|
+
lines.push(` - ${layer}.* × ${n}`)
|
|
326
|
+
}
|
|
327
|
+
const slowTotal = log.matchToken.hit + log.matchToken.miss
|
|
328
|
+
if (slowTotal > 0) {
|
|
329
|
+
lines.push(`- match-token 命中:${log.matchToken.hit} / ${slowTotal}`)
|
|
330
|
+
for (const [layer, n] of Object.entries(log.matchToken.byLayer)) {
|
|
331
|
+
lines.push(` - 推荐 ${layer} 层 × ${n}`)
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
lines.push('- match-token 本次未触发')
|
|
335
|
+
}
|
|
336
|
+
if (log.misses.length > 0) {
|
|
337
|
+
lines.push('- 未命中输入:')
|
|
338
|
+
for (const miss of log.misses) {
|
|
339
|
+
lines.push(` - \`${miss.script}\` ← \`${miss.input}\``)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
lines.push('')
|
|
344
|
+
|
|
345
|
+
// ---------- 产物审计 ----------
|
|
346
|
+
lines.push('## 📦 产物审计')
|
|
347
|
+
lines.push('')
|
|
348
|
+
if (audit.files.length === 0) {
|
|
349
|
+
lines.push('- 未在工作树中检测到相关 ts/tsx 改动')
|
|
350
|
+
} else {
|
|
351
|
+
lines.push(`改动文件:${audit.files.length} 个`)
|
|
352
|
+
for (const f of audit.files) lines.push(` - ${f}`)
|
|
353
|
+
lines.push('')
|
|
354
|
+
|
|
355
|
+
const apexEntries = Object.entries(audit.apexImports)
|
|
356
|
+
if (apexEntries.length > 0) {
|
|
357
|
+
lines.push(`apex-ui 组件复用:${apexEntries.length} 种`)
|
|
358
|
+
for (const [name, n] of apexEntries) lines.push(` - ${name} × ${n}`)
|
|
359
|
+
lines.push('')
|
|
360
|
+
} else {
|
|
361
|
+
lines.push('apex-ui 组件复用:0 ⚠️ 检查是否漏用组件库')
|
|
362
|
+
lines.push('')
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const variableEntries = Object.entries(audit.cssVariables)
|
|
366
|
+
if (variableEntries.length > 0) {
|
|
367
|
+
lines.push('CSS variable 引用:')
|
|
368
|
+
for (const [name, n] of variableEntries) lines.push(` - ${name} × ${n}`)
|
|
369
|
+
lines.push('')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const commonEntries = Object.entries(audit.commonRefs)
|
|
373
|
+
if (commonEntries.length > 0) {
|
|
374
|
+
lines.push('common component / common module 复用:')
|
|
375
|
+
for (const [ref, n] of commonEntries) lines.push(` - ${ref} × ${n}`)
|
|
376
|
+
lines.push('')
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (audit.hardcoded.length > 0) {
|
|
380
|
+
lines.push(`硬编码嫌疑:${audit.hardcoded.length} 处`)
|
|
381
|
+
for (const h of audit.hardcoded) {
|
|
382
|
+
lines.push(` - ${h.file}:${h.line} \`${h.value}\` — ${h.snippet}`)
|
|
383
|
+
}
|
|
384
|
+
lines.push('')
|
|
385
|
+
} else {
|
|
386
|
+
lines.push('硬编码嫌疑:0 ✅')
|
|
387
|
+
lines.push('')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (audit.pandaForbidden.length > 0) {
|
|
391
|
+
lines.push(`Panda 禁用内容:${audit.pandaForbidden.length} 处 ⚠️`)
|
|
392
|
+
for (const hit of audit.pandaForbidden) {
|
|
393
|
+
lines.push(` - ${hit.file}:${hit.line} \`${hit.value}\``)
|
|
394
|
+
}
|
|
395
|
+
lines.push('')
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return lines.join('\n') + '\n'
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function writeReport({ report, command, targetDir }) {
|
|
403
|
+
const fileName = `session-report-${command}.md`
|
|
404
|
+
const primary = path.join(DEFAULT_REPORT_DIR, fileName)
|
|
405
|
+
fs.mkdirSync(DEFAULT_REPORT_DIR, { recursive: true })
|
|
406
|
+
fs.writeFileSync(primary, report)
|
|
407
|
+
const written = [primary]
|
|
408
|
+
|
|
409
|
+
if (targetDir) {
|
|
410
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
411
|
+
const copy = path.join(targetDir, fileName)
|
|
412
|
+
fs.writeFileSync(copy, report)
|
|
413
|
+
written.push(copy)
|
|
414
|
+
}
|
|
415
|
+
return written
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function main() {
|
|
419
|
+
const args = parseArgs(process.argv)
|
|
420
|
+
if (args.reset) {
|
|
421
|
+
resetSessionLog()
|
|
422
|
+
console.log('[figma:report] session log reset.')
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const log = tallyLog(readSessionLog())
|
|
427
|
+
const changedFiles = getChangedFiles()
|
|
428
|
+
const audit = aggregate(
|
|
429
|
+
changedFiles.filter((c) => /\.(ts|tsx|css)$/.test(c.file)).map((c) => c.file)
|
|
430
|
+
)
|
|
431
|
+
const planTokens = parsePlanTokenTable(args.plan)
|
|
432
|
+
const forbiddenFiles = auditForbiddenFiles(changedFiles)
|
|
433
|
+
const report = renderReport({
|
|
434
|
+
log,
|
|
435
|
+
audit,
|
|
436
|
+
planTokens,
|
|
437
|
+
command: args.command,
|
|
438
|
+
changedFiles,
|
|
439
|
+
forbiddenFiles,
|
|
440
|
+
generatedAt: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const written = writeReport({ report, command: args.command, targetDir: args.targetDir })
|
|
444
|
+
console.log(report)
|
|
445
|
+
for (const w of written) console.log(`[figma:report] saved -> ${w}`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon Inventory
|
|
3
|
+
*
|
|
4
|
+
* 扫描仓库中真实存在的 icon 资产源,供 figmaSync plan 做语义匹配。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* node .agent/skills/figmaSync/scripts/icon-inventory.mjs
|
|
8
|
+
* node .agent/skills/figmaSync/scripts/icon-inventory.mjs --pretty
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
|
|
13
|
+
const ROOT = process.cwd()
|
|
14
|
+
const ICON_EXTENSIONS = new Set(['.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif', '.tsx', '.ts'])
|
|
15
|
+
const STATIC_EXTENSIONS = new Set(['.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif'])
|
|
16
|
+
const SEARCH_ROOTS = ['assets', 'icons', 'public', 'src']
|
|
17
|
+
const IGNORE_DIRS = new Set([
|
|
18
|
+
'.git',
|
|
19
|
+
'.agent',
|
|
20
|
+
'dist',
|
|
21
|
+
'node_modules',
|
|
22
|
+
'styled-system',
|
|
23
|
+
'coverage',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'.vite'
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
const pretty = process.argv.includes('--pretty')
|
|
30
|
+
const roots = SEARCH_ROOTS.filter((dir) => fs.existsSync(path.join(ROOT, dir)))
|
|
31
|
+
const files = []
|
|
32
|
+
|
|
33
|
+
for (const root of roots) {
|
|
34
|
+
walk(path.join(ROOT, root), files)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const icons = files
|
|
38
|
+
.map((file) => toIconRecord(file))
|
|
39
|
+
.flat()
|
|
40
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path))
|
|
41
|
+
|
|
42
|
+
const out = {
|
|
43
|
+
roots,
|
|
44
|
+
count: icons.length,
|
|
45
|
+
icons,
|
|
46
|
+
note:
|
|
47
|
+
icons.length === 0
|
|
48
|
+
? '仓库未发现可用 icon 源;请用户提供其他 icon 导入方案,或确认不关心 icon。'
|
|
49
|
+
: null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(JSON.stringify(out, null, pretty ? 2 : 0))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function walk(dir, files) {
|
|
56
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.name.startsWith('.')) continue
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (IGNORE_DIRS.has(entry.name)) continue
|
|
61
|
+
const next = path.join(dir, entry.name)
|
|
62
|
+
if (isIconSearchDir(next)) walk(next, files)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (!entry.isFile()) continue
|
|
66
|
+
const file = path.join(dir, entry.name)
|
|
67
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
68
|
+
if (!ICON_EXTENSIONS.has(ext)) continue
|
|
69
|
+
if (isInAllowedIconPath(file) || looksLikeIconFile(file)) {
|
|
70
|
+
files.push(file)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isIconSearchDir(absDir) {
|
|
76
|
+
const rel = toPosix(path.relative(ROOT, absDir))
|
|
77
|
+
if (!rel) return true
|
|
78
|
+
const parts = rel.split('/')
|
|
79
|
+
if (parts.some((p) => IGNORE_DIRS.has(p))) return false
|
|
80
|
+
if (parts[0] === 'assets' || parts[0] === 'icons' || parts[0] === 'public') return true
|
|
81
|
+
if (parts[0] === 'src') return true
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isInAllowedIconPath(absFile) {
|
|
86
|
+
const rel = toPosix(path.relative(ROOT, absFile))
|
|
87
|
+
const parts = rel.split('/')
|
|
88
|
+
if (parts[0] === 'assets' || parts[0] === 'icons' || parts[0] === 'public') return true
|
|
89
|
+
return parts[0] === 'src' && (parts.includes('assets') || parts.includes('icons'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function looksLikeIconFile(absFile) {
|
|
93
|
+
const basename = path.basename(absFile, path.extname(absFile))
|
|
94
|
+
return /\bicon\b/i.test(basename) || /(^|[-_])ic([-_]|$)/i.test(basename)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toIconRecord(absFile) {
|
|
98
|
+
const ext = path.extname(absFile).toLowerCase()
|
|
99
|
+
const rel = toPosix(path.relative(ROOT, absFile))
|
|
100
|
+
if (STATIC_EXTENSIONS.has(ext)) {
|
|
101
|
+
const baseName = path.basename(absFile, ext)
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
name: baseName,
|
|
105
|
+
normalizedName: normalizeName(baseName),
|
|
106
|
+
type: ext.slice(1),
|
|
107
|
+
path: rel,
|
|
108
|
+
exportName: null
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const source = fs.readFileSync(absFile, 'utf-8')
|
|
114
|
+
const exportNames = extractIconExports(source)
|
|
115
|
+
if (exportNames.length === 0) return []
|
|
116
|
+
return exportNames.map((exportName) => ({
|
|
117
|
+
name: exportName,
|
|
118
|
+
normalizedName: normalizeName(exportName),
|
|
119
|
+
type: 'component',
|
|
120
|
+
path: rel,
|
|
121
|
+
exportName
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractIconExports(source) {
|
|
126
|
+
const names = new Set()
|
|
127
|
+
const patterns = [
|
|
128
|
+
/export\s+(?:const|function)\s+([A-Z][A-Za-z0-9]*Icon|Icon[A-Z][A-Za-z0-9]*)\b/g,
|
|
129
|
+
/export\s*\{\s*([^}]+)\s*\}/g
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
let m
|
|
133
|
+
while ((m = patterns[0].exec(source)) !== null) {
|
|
134
|
+
names.add(m[1])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
while ((m = patterns[1].exec(source)) !== null) {
|
|
138
|
+
for (const raw of m[1].split(',')) {
|
|
139
|
+
const name = raw
|
|
140
|
+
.trim()
|
|
141
|
+
.replace(/\s+as\s+.+$/, '')
|
|
142
|
+
.trim()
|
|
143
|
+
if (/([A-Z][A-Za-z0-9]*Icon|Icon[A-Z][A-Za-z0-9]*)$/.test(name)) names.add(name)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return [...names]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeName(name) {
|
|
151
|
+
return name
|
|
152
|
+
.replace(/Icon$/i, '')
|
|
153
|
+
.replace(/^Icon/i, '')
|
|
154
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
155
|
+
.replace(/[_\s]+/g, '-')
|
|
156
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
157
|
+
.toLowerCase()
|
|
158
|
+
.replace(/^-+|-+$/g, '')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toPosix(p) {
|
|
162
|
+
return p.split(path.sep).join('/')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main()
|