@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,39 @@
1
+ # PLAN 示例片段
2
+
3
+ ## 2. 组件与资产映射决策
4
+
5
+ | Figma 节点 | nodeId | 目标实现 | 复用类型 | 是否需要 CSS | 备注 |
6
+ | ---------- | ------ | ----------------------------------------- | -------- | ------------ | ---------------- |
7
+ | CTA | 1:3 | `<Button type="primary" size="medium" />` | Apex UI | 否 | props 可覆盖视觉 |
8
+ | Header | 1:4 | 自定义 header | 无 | 是 | 页面私有布局 |
9
+
10
+ ## 3. CSS 变量映射表
11
+
12
+ | 来源(Figma) | 类型 | CSS variable | /theme 路径 | 原始值 | 命中方式 | nodeId | designContextStatus |
13
+ | -------------------------------------- | ----- | ------------------------ | --------------------------------- | ----------- | ----------------------- | ------ | ------------------- |
14
+ | `var(--fill-neutral-primary, #090A0B)` | color | `--fill-neutral-primary` | `foundation.fill.neutral.primary` | `{grey.14}` | Fast Path 0 (variables) | 1:2 | OK |
15
+
16
+ ## 4. CSS 草案审核
17
+
18
+ CSS 草案文件:`./figma-plan.css`
19
+
20
+ ```css
21
+ .welcome-page {
22
+ min-height: 100%;
23
+ background: var(--fill-neutral-primary);
24
+ }
25
+ ```
26
+
27
+ ## 5. 样式落地清单
28
+
29
+ ### Level 1(组件局部 CSS)
30
+
31
+ - `.welcome-page`:页面根容器。
32
+
33
+ ### Level 2(扩展 common component)
34
+
35
+ - (无)
36
+
37
+ ### Level 3(建议新增 /theme 变量)
38
+
39
+ - (无)
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Session Log
3
+ *
4
+ * figmaSync 任务期间,每次调用 lookup-var / match-token 都追加一行 JSONL,
5
+ * 供 figma-sync-report.mjs 汇总。session 边界:AI 在任务开始时跑
6
+ * `pnpm figma:report --reset` 清空。
7
+ *
8
+ * 路径:.agent/skills/figmaSync/.session-log.jsonl(git 忽略)
9
+ *
10
+ * 去重契约:
11
+ * - 把 (script, canonicalInput) 视为同一查询
12
+ * - hit 优先于 miss:先 miss 后 hit 时,老的 miss 标 superseded;之后再来 miss 直接丢弃
13
+ * - readSessionLog() 默认过滤掉 superseded 项
14
+ *
15
+ * canonicalInput 规则:
16
+ * - 大小写折叠、空白剥除
17
+ * - `Npx` 与 `N` 视为同义(match-token 现接受两种)
18
+ * - `var(--xxx, fallback)` 取变量名
19
+ */
20
+ import fs from 'node:fs'
21
+
22
+ const LOG_PATH = '.agent/skills/figmaSync/.session-log.jsonl'
23
+
24
+ function canonicalize(raw) {
25
+ if (raw === undefined || raw === null) return ''
26
+ let s = String(raw).toLowerCase().trim()
27
+ const varMatch = /var\(\s*(--[a-z0-9_-]+)/.exec(s)
28
+ if (varMatch) s = varMatch[1]
29
+ s = s.replace(/(\d+(?:\.\d+)?)px\b/, '$1')
30
+ return s
31
+ }
32
+
33
+ function readAllRaw() {
34
+ if (!fs.existsSync(LOG_PATH)) return []
35
+ return fs
36
+ .readFileSync(LOG_PATH, 'utf-8')
37
+ .split('\n')
38
+ .filter(Boolean)
39
+ .map((line) => {
40
+ try {
41
+ return JSON.parse(line)
42
+ } catch {
43
+ return null
44
+ }
45
+ })
46
+ .filter(Boolean)
47
+ }
48
+
49
+ export function appendSessionLog(entry) {
50
+ try {
51
+ const key = `${entry.script}|${canonicalize(entry.input)}`
52
+ const existing = readAllRaw()
53
+ const prev = [...existing]
54
+ .reverse()
55
+ .find((e) => `${e.script}|${canonicalize(e.input)}` === key && !e.superseded)
56
+ // 已有 hit 时,无论新 entry 是 hit 还是 miss 都不再追加
57
+ if (prev?.hit) return
58
+ if (entry.hit && prev && !prev.hit) {
59
+ const rewritten = existing.map((e) => {
60
+ if (`${e.script}|${canonicalize(e.input)}` !== key) return e
61
+ if (e.hit) return e
62
+ return { ...e, superseded: true }
63
+ })
64
+ const lines = rewritten.map((e) => JSON.stringify(e)).join('\n') + '\n'
65
+ fs.writeFileSync(LOG_PATH, lines)
66
+ }
67
+ const enriched = { ts: new Date().toISOString(), ...entry }
68
+ fs.appendFileSync(LOG_PATH, JSON.stringify(enriched) + '\n')
69
+ } catch {
70
+ // 日志写入失败不影响调用方
71
+ }
72
+ }
73
+
74
+ export function readSessionLog() {
75
+ return readAllRaw().filter((e) => !e.superseded)
76
+ }
77
+
78
+ export function resetSessionLog() {
79
+ if (fs.existsSync(LOG_PATH)) fs.unlinkSync(LOG_PATH)
80
+ }
81
+
82
+ export const SESSION_LOG_PATH = LOG_PATH
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Token Resolver
3
+ *
4
+ * 把 DTCG 别名引用(如 `{grey.1}`、`{fill.neutral.primary}`)解析成最终原始值。
5
+ * 支持跨 set 引用(component 引用 foundation,foundation 引用 palette)。
6
+ *
7
+ * 使用:
8
+ * const loaded = loadTokens('./tokens')
9
+ * const records = flattenTokens(loaded)
10
+ * const resolver = createResolver(records)
11
+ * resolver.resolve('foundation', ['fill', 'neutral', 'primary']) // -> '#090a0b'
12
+ * resolver.resolveAliasString('{grey.1}') // -> '#090a0b'
13
+ *
14
+ * 同名 token 跨 set 解析顺序(按 tokenSetOrder 反序优先):
15
+ * componentUsage > component > baseValue > foundation > palette
16
+ * 即下游 set 内若定义了同名 token,优先用下游的;用于 component 覆盖 palette 的能力。
17
+ */
18
+
19
+ const ALIAS_RE = /^\{([^}]+)\}$/
20
+
21
+ export function createResolver(records, order = []) {
22
+ // 索引:'setName.a.b.c' -> record
23
+ const bySetPath = new Map()
24
+ // 跨 set 索引:'a.b.c' -> record[](按 set 顺序排)
25
+ const byPathAcrossSets = new Map()
26
+
27
+ for (const rec of records) {
28
+ bySetPath.set(`${rec.setName}.${rec.pathStr}`, rec)
29
+ const list = byPathAcrossSets.get(rec.pathStr) ?? []
30
+ list.push(rec)
31
+ byPathAcrossSets.set(rec.pathStr, list)
32
+ }
33
+
34
+ // 跨 set 命中时的优先级:tokenSetOrder 越靠后越优先
35
+ const setPriority = new Map()
36
+ order.forEach((setName, i) => setPriority.set(setName, i))
37
+
38
+ for (const list of byPathAcrossSets.values()) {
39
+ list.sort((a, b) => (setPriority.get(b.setName) ?? -1) - (setPriority.get(a.setName) ?? -1))
40
+ }
41
+
42
+ function findRecord(setName, pathArr) {
43
+ const key = `${setName}.${pathArr.join('.')}`
44
+ return bySetPath.get(key) ?? null
45
+ }
46
+
47
+ function findRecordByPath(pathStr) {
48
+ const list = byPathAcrossSets.get(pathStr)
49
+ return list && list.length > 0 ? list[0] : null
50
+ }
51
+
52
+ function resolveAliasString(value, seen = new Set()) {
53
+ if (typeof value !== 'string') return value
54
+ const match = value.match(ALIAS_RE)
55
+ if (!match) return value
56
+
57
+ const innerPath = match[1]
58
+ if (seen.has(innerPath)) {
59
+ throw new Error(`circular alias detected at {${innerPath}}`)
60
+ }
61
+ seen.add(innerPath)
62
+
63
+ const target = findRecordByPath(innerPath)
64
+ if (!target) {
65
+ // 引用悬空:返回 null 标记,让上层报告影响
66
+ return { __unresolved: true, alias: innerPath }
67
+ }
68
+ return resolveAliasString(target.$value, seen)
69
+ }
70
+
71
+ function resolve(setName, pathArr) {
72
+ const rec = findRecord(setName, pathArr)
73
+ if (!rec) return null
74
+ return resolveAliasString(rec.$value)
75
+ }
76
+
77
+ return {
78
+ resolve,
79
+ resolveAliasString,
80
+ findRecord,
81
+ findRecordByPath,
82
+ allRecords: records,
83
+ order
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 反向查询:给定一个原始值(如 '#202226'),列出所有最终解析到该值的 token 路径。
89
+ * 返回数组按层级优先级排序:componentUsage > component > foundation > palette > baseValue。
90
+ */
91
+ export function findTokensByValue(resolver, targetValue) {
92
+ const cleanTarget = normalizeValue(targetValue)
93
+ const matches = []
94
+
95
+ for (const rec of resolver.allRecords) {
96
+ const resolved = resolver.resolveAliasString(rec.$value)
97
+ if (resolved && typeof resolved === 'object' && resolved.__unresolved) continue
98
+ if (normalizeValue(resolved) === cleanTarget) {
99
+ matches.push({
100
+ setName: rec.setName,
101
+ path: rec.pathStr,
102
+ fullPath: `${rec.setName}.${rec.pathStr}`,
103
+ $type: rec.$type,
104
+ $value: rec.$value,
105
+ resolvedValue: resolved
106
+ })
107
+ }
108
+ }
109
+
110
+ return matches.sort((a, b) => layerPriority(a.setName) - layerPriority(b.setName))
111
+ }
112
+
113
+ // 数字越小越优先推荐
114
+ const LAYER_PRIORITY = {
115
+ componentUsage: 0,
116
+ component: 1,
117
+ foundation: 2,
118
+ baseValue: 3,
119
+ palette: 4
120
+ }
121
+
122
+ function layerPriority(setName) {
123
+ return LAYER_PRIORITY[setName] ?? 99
124
+ }
125
+
126
+ function normalizeValue(v) {
127
+ if (v === null || v === undefined) return ''
128
+ if (typeof v === 'number') return String(v)
129
+ const str = String(v).toLowerCase().trim()
130
+ const pxMatch = /^(-?\d+(?:\.\d+)?)px$/.exec(str)
131
+ return pxMatch ? pxMatch[1] : str
132
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Token source helpers.
3
+ *
4
+ * 默认 token 源为 theme/dark/*.css。
5
+ */
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+
9
+ const DEFAULT_SOURCE_FILES = ['primitives.css', 'aliases.css']
10
+ const PALETTE_PREFIXES = new Set(['green', 'grey', 'purple', 'red', 'yellow'])
11
+ const FOUNDATION_PREFIXES = new Set(['fill', 'icon', 'stroke', 'text'])
12
+ const BASE_VALUE_PREFIXES = new Set([
13
+ 'border',
14
+ 'fontheight',
15
+ 'fontsize',
16
+ 'fontspacing',
17
+ 'fontweight',
18
+ 'icon',
19
+ 'radius',
20
+ 'size',
21
+ 'spacing'
22
+ ])
23
+
24
+ export function loadCurrentTokenRecords({
25
+ themeDir = path.resolve(process.cwd(), 'theme/dark')
26
+ } = {}) {
27
+ const loaded = loadCssTokens(themeDir)
28
+ return { source: 'css', order: loaded.order, records: flattenCssTokenRecords(loaded) }
29
+ }
30
+
31
+ export function buildTokenPathSets(records) {
32
+ return {
33
+ fullPaths: new Set(records.map((r) => `${r.setName}.${r.pathStr}`)),
34
+ shortPaths: new Set(records.map((r) => r.pathStr))
35
+ }
36
+ }
37
+
38
+ function loadCssTokens(tokensSrcDir, sourceFiles = DEFAULT_SOURCE_FILES) {
39
+ const dir = tokensSrcDir ?? path.resolve(process.cwd(), 'theme/dark')
40
+ if (!fs.existsSync(dir)) {
41
+ throw new Error(`theme directory not found: ${dir}`)
42
+ }
43
+
44
+ const variables = []
45
+ for (const fileName of sourceFiles) {
46
+ const filePath = path.join(dir, fileName)
47
+ if (!fs.existsSync(filePath)) {
48
+ throw new Error(`token css source missing: ${filePath}`)
49
+ }
50
+ variables.push(...parseCssVariables(fs.readFileSync(filePath, 'utf-8'), fileName))
51
+ }
52
+
53
+ return {
54
+ order: ['palette', 'foundation', 'baseValue'],
55
+ variables,
56
+ raw: { dir, sourceFiles }
57
+ }
58
+ }
59
+
60
+ function flattenCssTokenRecords(loaded) {
61
+ const records = []
62
+ for (const variable of loaded.variables) {
63
+ const record = variableToRecord(variable)
64
+ if (record) records.push(record)
65
+ }
66
+ return records
67
+ }
68
+
69
+ function parseCssVariables(cssText, sourceFile) {
70
+ const variables = []
71
+ const declRe = /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g
72
+ let match
73
+ while ((match = declRe.exec(stripComments(cssText)))) {
74
+ variables.push({
75
+ name: `--${match[1]}`,
76
+ value: match[2].trim(),
77
+ sourceFile
78
+ })
79
+ }
80
+ return variables
81
+ }
82
+
83
+ function stripComments(cssText) {
84
+ return cssText.replace(/\/\*[\s\S]*?\*\//g, '')
85
+ }
86
+
87
+ function variableToRecord(variable) {
88
+ const segments = variable.name.slice(2).split('-')
89
+ const [first] = segments
90
+
91
+ let setName = null
92
+ let type = inferType(variable.value)
93
+
94
+ if (PALETTE_PREFIXES.has(first)) {
95
+ setName = 'palette'
96
+ } else if (FOUNDATION_PREFIXES.has(first)) {
97
+ setName = 'foundation'
98
+ type = 'color'
99
+ } else if (BASE_VALUE_PREFIXES.has(first)) {
100
+ setName = 'baseValue'
101
+ type = 'number'
102
+ }
103
+
104
+ if (!setName) return null
105
+
106
+ return {
107
+ setName,
108
+ path: segments,
109
+ pathStr: segments.join('.'),
110
+ $type: type,
111
+ $value: normalizeRecordValue(variable.value),
112
+ cssValue: normalizeCssValue(variable.value),
113
+ cssVar: variable.name,
114
+ sourceFile: variable.sourceFile
115
+ }
116
+ }
117
+
118
+ function inferType(value) {
119
+ if (
120
+ /^#|^rgb\(|^hsl\(|^var\(--(?:green|grey|purple|red|yellow|fill|icon|stroke|text)-/i.test(value)
121
+ ) {
122
+ return 'color'
123
+ }
124
+ return 'number'
125
+ }
126
+
127
+ function normalizeCssValue(value) {
128
+ return value.trim().replace(/\s+/g, ' ')
129
+ }
130
+
131
+ function normalizeRecordValue(value) {
132
+ const normalized = normalizeCssValue(value)
133
+ const varRef = parseVarRef(normalized)
134
+ if (!varRef) return normalized
135
+ return `{${cssVarToPath(varRef)}}`
136
+ }
137
+
138
+ function cssVarToPath(cssVar) {
139
+ return cssVar.slice(2).split('-').join('.')
140
+ }
141
+
142
+ function parseVarRef(value) {
143
+ const match = value.match(/^var\(\s*(--[a-zA-Z0-9_-]+)(?:\s*,[^)]*)?\)$/)
144
+ return match ? match[1] : null
145
+ }