@su-record/vibe 2.9.35 → 2.9.37

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/CLAUDE.md +1 -1
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/agents/compounder.md +6 -6
  5. package/agents/diagrammer.md +2 -2
  6. package/agents/e2e-tester.md +5 -5
  7. package/agents/qa/acceptance-tester.md +2 -2
  8. package/agents/refactor-cleaner.md +2 -2
  9. package/agents/tester.md +1 -1
  10. package/agents/ui/ui-antipattern-detector.md +1 -1
  11. package/agents/ui/ui-design-system-gen.md +3 -3
  12. package/agents/ui/ui-industry-analyzer.md +1 -1
  13. package/agents/ui/ui-layout-architect.md +1 -1
  14. package/agents/ui/ui-stack-implementer.md +1 -1
  15. package/commands/vibe.analyze.md +1 -1
  16. package/commands/vibe.contract.md +2 -2
  17. package/commands/vibe.figma.md +3 -3
  18. package/commands/vibe.harness.md +3 -3
  19. package/commands/vibe.regress.md +2 -2
  20. package/commands/vibe.review.md +4 -4
  21. package/commands/vibe.run.md +27 -27
  22. package/commands/vibe.scaffold.md +1 -1
  23. package/commands/vibe.spec.md +38 -38
  24. package/commands/vibe.trace.md +14 -14
  25. package/commands/vibe.utils.md +3 -3
  26. package/commands/vibe.verify.md +18 -18
  27. package/dist/infra/lib/figma/extract.d.ts.map +1 -1
  28. package/dist/infra/lib/figma/extract.js +55 -0
  29. package/dist/infra/lib/figma/extract.js.map +1 -1
  30. package/dist/infra/lib/figma/types.d.ts +8 -0
  31. package/dist/infra/lib/figma/types.d.ts.map +1 -1
  32. package/hooks/scripts/__tests__/figma-extract.test.js +338 -0
  33. package/hooks/scripts/figma-extract.js +144 -31
  34. package/hooks/scripts/prompt-dispatcher.js +15 -0
  35. package/hooks/scripts/utils.js +4 -0
  36. package/package.json +1 -1
  37. package/skills/arch-guard/SKILL.md +2 -2
  38. package/skills/arch-guard/agents/rule-generator.md +3 -3
  39. package/skills/arch-guard/scripts/check-boundaries.js +1 -1
  40. package/skills/arch-guard/templates/arch-rules.json +1 -1
  41. package/skills/capability-loop/SKILL.md +2 -2
  42. package/skills/capability-loop/templates/capability-spec.md +1 -1
  43. package/skills/claude-md-guide/SKILL.md +2 -2
  44. package/skills/design-audit/SKILL.md +3 -3
  45. package/skills/design-critique/SKILL.md +2 -2
  46. package/skills/design-distill/SKILL.md +1 -1
  47. package/skills/design-normalize/SKILL.md +4 -4
  48. package/skills/design-polish/SKILL.md +2 -2
  49. package/skills/design-teach/SKILL.md +6 -6
  50. package/skills/design-teach/templates/design-context.json +1 -1
  51. package/skills/devlog/SKILL.md +1 -1
  52. package/skills/event-planning/SKILL.md +1 -1
  53. package/skills/exec-plan/SKILL.md +5 -5
  54. package/skills/exec-plan/agents/decomposer.md +1 -1
  55. package/skills/exec-plan/templates/plan.md +2 -2
  56. package/skills/parallel-research/SKILL.md +3 -3
  57. package/skills/parallel-research/orchestrator.md +1 -1
  58. package/skills/parallel-research/templates/paper.md +1 -1
  59. package/skills/priority-todos/SKILL.md +1 -1
  60. package/skills/vibe-contract/SKILL.md +6 -6
  61. package/skills/vibe-interview/SKILL.md +6 -6
  62. package/skills/vibe-interview/checklists/feature.md +1 -1
  63. package/skills/vibe-plan/SKILL.md +10 -10
  64. package/skills/vibe-regress/SKILL.md +5 -5
  65. package/skills/vibe-regress/templates/bug.md +1 -1
  66. package/skills/vibe-regress/templates/test-jest.md +1 -1
  67. package/skills/vibe-regress/templates/test-vitest.md +1 -1
  68. package/skills/vibe-spec/SKILL.md +45 -45
  69. package/skills/vibe-spec-review/SKILL.md +21 -21
  70. package/skills/vibe-test/SKILL.md +1 -1
  71. package/vibe/constitution.md +2 -2
  72. package/vibe/templates/claudemd-template.md +4 -4
  73. package/vibe/templates/constitution-template.md +2 -2
  74. package/vibe/templates/feature-template.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/infra/lib/figma/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;2CAE2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;mEACmE;AACnE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,2FAA2F;IAC3F,GAAG,EAAE,aAAa,CAAC;IACnB,qEAAqE;IACrE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;CAChC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/infra/lib/figma/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;2CAE2C;AAC3C,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;mEACmE;AACnE,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,2FAA2F;IAC3F,GAAG,EAAE,aAAa,CAAC;IACnB,qEAAqE;IACrE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;CAChC"}
@@ -0,0 +1,338 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ walk,
4
+ isDesignTextNode,
5
+ hasTextDescendantRaw,
6
+ hasRepeatingInstancesRaw,
7
+ vectorChildCountRaw,
8
+ shouldRenderImageAsBg,
9
+ resolveImageFills,
10
+ toHTML,
11
+ } from '../figma-extract.js';
12
+
13
+ // ─── Fixture helpers ────────────────────────────────────────────────
14
+
15
+ const bbox = (w = 100, h = 100, x = 0, y = 0) => ({ x, y, width: w, height: h });
16
+
17
+ function textNode(overrides = {}) {
18
+ return {
19
+ id: 'text-1', name: 'heading', type: 'TEXT', characters: 'Hello',
20
+ absoluteBoundingBox: bbox(200, 40),
21
+ style: { fontFamily: 'Inter', fontSize: 24, fontWeight: 600 },
22
+ fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 } }],
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ function frame(name, children = [], overrides = {}) {
28
+ return {
29
+ id: `frame-${name}`, name, type: 'FRAME',
30
+ absoluteBoundingBox: bbox(800, 400),
31
+ children,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function imageFillNode(name, overrides = {}) {
37
+ return {
38
+ id: `img-${name}`, name, type: 'RECTANGLE',
39
+ absoluteBoundingBox: bbox(400, 300),
40
+ fills: [{ type: 'IMAGE', imageRef: 'ref-' + name, scaleMode: 'FILL' }],
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ // ─── isDesignTextNode (D1-D3) ───────────────────────────────────────
46
+
47
+ describe('isDesignTextNode', () => {
48
+ it('returns false for plain SOLID-filled text', () => {
49
+ expect(isDesignTextNode(textNode())).toBe(false);
50
+ });
51
+
52
+ it('D1: detects 2+ visible fills', () => {
53
+ const n = textNode({
54
+ fills: [
55
+ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 } },
56
+ { type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 0.5 } },
57
+ ],
58
+ });
59
+ expect(isDesignTextNode(n)).toBe(true);
60
+ });
61
+
62
+ it('D3: detects GRADIENT_LINEAR fill', () => {
63
+ const n = textNode({
64
+ fills: [{
65
+ type: 'GRADIENT_LINEAR',
66
+ gradientStops: [
67
+ { position: 0, color: { r: 1, g: 0, b: 0, a: 1 } },
68
+ { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } },
69
+ ],
70
+ gradientHandlePositions: [{ x: 0, y: 0 }, { x: 1, y: 0 }],
71
+ }],
72
+ });
73
+ expect(isDesignTextNode(n)).toBe(true);
74
+ });
75
+
76
+ it('D2: detects DROP_SHADOW effect', () => {
77
+ const n = textNode({ effects: [{ type: 'DROP_SHADOW', visible: true, radius: 4 }] });
78
+ expect(isDesignTextNode(n)).toBe(true);
79
+ });
80
+
81
+ it('D2: detects visible stroke', () => {
82
+ const n = textNode({ strokes: [{ type: 'SOLID', visible: true, color: { r: 1, g: 1, b: 1, a: 1 } }] });
83
+ expect(isDesignTextNode(n)).toBe(true);
84
+ });
85
+
86
+ it('ignores invisible fills', () => {
87
+ const n = textNode({
88
+ fills: [
89
+ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 } },
90
+ { type: 'GRADIENT_LINEAR', visible: false },
91
+ ],
92
+ });
93
+ expect(isDesignTextNode(n)).toBe(false);
94
+ });
95
+
96
+ it('returns false for non-TEXT nodes', () => {
97
+ expect(isDesignTextNode({ type: 'FRAME', fills: [] })).toBe(false);
98
+ });
99
+ });
100
+
101
+ // ─── hasTextDescendantRaw (Q1) ──────────────────────────────────────
102
+
103
+ describe('hasTextDescendantRaw', () => {
104
+ it('true when node itself is non-empty TEXT', () => {
105
+ expect(hasTextDescendantRaw(textNode())).toBe(true);
106
+ });
107
+
108
+ it('false when TEXT node has only whitespace', () => {
109
+ expect(hasTextDescendantRaw(textNode({ characters: ' ' }))).toBe(false);
110
+ });
111
+
112
+ it('true when any descendant is TEXT', () => {
113
+ const tree = frame('BG', [
114
+ { id: 'g', name: 'group', type: 'GROUP', children: [textNode()] },
115
+ ]);
116
+ expect(hasTextDescendantRaw(tree)).toBe(true);
117
+ });
118
+
119
+ it('false for purely decorative VECTOR/RECTANGLE subtree', () => {
120
+ const tree = frame('BG', [
121
+ { id: 'v1', name: 'sparkle', type: 'VECTOR', children: [] },
122
+ { id: 'v2', name: 'glow', type: 'RECTANGLE', children: [] },
123
+ ]);
124
+ expect(hasTextDescendantRaw(tree)).toBe(false);
125
+ });
126
+ });
127
+
128
+ // ─── hasRepeatingInstancesRaw (Q2) ──────────────────────────────────
129
+
130
+ describe('hasRepeatingInstancesRaw', () => {
131
+ it('true for 2+ INSTANCE children with same componentId', () => {
132
+ const tree = frame('cards', [
133
+ { id: 'i1', name: 'Card 1', type: 'INSTANCE', componentId: 'C:1', children: [] },
134
+ { id: 'i2', name: 'Card 2', type: 'INSTANCE', componentId: 'C:1', children: [] },
135
+ ]);
136
+ expect(hasRepeatingInstancesRaw(tree)).toBe(true);
137
+ });
138
+
139
+ it('true for 2+ INSTANCE children with same name stem (trailing digits stripped)', () => {
140
+ const tree = frame('list', [
141
+ { id: 'i1', name: 'item 1', type: 'INSTANCE', children: [] },
142
+ { id: 'i2', name: 'item 2', type: 'INSTANCE', children: [] },
143
+ ]);
144
+ expect(hasRepeatingInstancesRaw(tree)).toBe(true);
145
+ });
146
+
147
+ it('false for single INSTANCE', () => {
148
+ const tree = frame('solo', [
149
+ { id: 'i1', name: 'hero', type: 'INSTANCE', children: [] },
150
+ ]);
151
+ expect(hasRepeatingInstancesRaw(tree)).toBe(false);
152
+ });
153
+
154
+ it('false for mixed non-repeating INSTANCEs', () => {
155
+ const tree = frame('mix', [
156
+ { id: 'i1', name: 'hero', type: 'INSTANCE', componentId: 'C:A', children: [] },
157
+ { id: 'i2', name: 'footer', type: 'INSTANCE', componentId: 'C:B', children: [] },
158
+ ]);
159
+ expect(hasRepeatingInstancesRaw(tree)).toBe(false);
160
+ });
161
+ });
162
+
163
+ // ─── vectorChildCountRaw (D4 helper) ────────────────────────────────
164
+
165
+ describe('vectorChildCountRaw', () => {
166
+ it('counts direct VECTOR-family children', () => {
167
+ const tree = frame('parent', [
168
+ { id: 'v1', type: 'VECTOR', children: [] },
169
+ { id: 'v2', type: 'LINE', children: [] },
170
+ { id: 'v3', type: 'BOOLEAN_OPERATION', children: [] },
171
+ { id: 'r1', type: 'RECTANGLE', children: [] },
172
+ ]);
173
+ expect(vectorChildCountRaw(tree)).toBe(3);
174
+ });
175
+
176
+ it('returns 0 for leaf', () => {
177
+ expect(vectorChildCountRaw({ id: 'x', type: 'FRAME' })).toBe(0);
178
+ });
179
+ });
180
+
181
+ // ─── walk() integration: metadata propagates into tree ──────────────
182
+
183
+ describe('walk() metadata', () => {
184
+ it('emits hasTextChildren on ancestors of TEXT nodes', () => {
185
+ const raw = frame('section', [
186
+ frame('BG', [textNode({ name: 'title' })]),
187
+ ]);
188
+ const tree = walk(raw);
189
+ expect(tree.hasTextChildren).toBe(true);
190
+ expect(tree.children[0].hasTextChildren).toBe(true); // BG frame too
191
+ });
192
+
193
+ it('omits hasTextChildren for pure decoration', () => {
194
+ const raw = frame('section', [
195
+ frame('BG', [
196
+ { id: 'v1', name: 'glow', type: 'VECTOR', children: [] },
197
+ ]),
198
+ ]);
199
+ const tree = walk(raw);
200
+ expect(tree.hasTextChildren).toBeUndefined();
201
+ });
202
+
203
+ it('emits hasInstanceRepeat on parents of repeating INSTANCEs', () => {
204
+ const raw = frame('list', [
205
+ { id: 'c1', name: 'card 1', type: 'INSTANCE', componentId: 'C:1', children: [] },
206
+ { id: 'c2', name: 'card 2', type: 'INSTANCE', componentId: 'C:1', children: [] },
207
+ ]);
208
+ const tree = walk(raw);
209
+ expect(tree.hasInstanceRepeat).toBe(true);
210
+ });
211
+
212
+ it('emits isDesignText on gradient-filled TEXT', () => {
213
+ const raw = textNode({
214
+ fills: [{
215
+ type: 'GRADIENT_LINEAR',
216
+ gradientStops: [
217
+ { position: 0, color: { r: 1, g: 0, b: 0, a: 1 } },
218
+ { position: 1, color: { r: 0, g: 0, b: 1, a: 1 } },
219
+ ],
220
+ gradientHandlePositions: [{ x: 0, y: 0 }, { x: 1, y: 0 }],
221
+ }],
222
+ });
223
+ const tree = walk(raw);
224
+ expect(tree.isDesignText).toBe(true);
225
+ });
226
+ });
227
+
228
+ // ─── shouldRenderImageAsBg ──────────────────────────────────────────
229
+
230
+ describe('shouldRenderImageAsBg', () => {
231
+ it('container with TEXT descendants → CSS bg', () => {
232
+ const node = { name: 'hero', imageRef: 'r1', hasTextChildren: true, children: [{}] };
233
+ expect(shouldRenderImageAsBg(node)).toBe(true);
234
+ });
235
+
236
+ it('container with repeating instances → CSS bg', () => {
237
+ const node = { name: 'grid', imageRef: 'r1', hasInstanceRepeat: true, children: [{}, {}] };
238
+ expect(shouldRenderImageAsBg(node)).toBe(true);
239
+ });
240
+
241
+ it('node named BG → CSS bg', () => {
242
+ const node = { name: 'BG', imageRef: 'r1', children: [] };
243
+ expect(shouldRenderImageAsBg(node)).toBe(true);
244
+ });
245
+
246
+ it('node named 배경 → CSS bg', () => {
247
+ const node = { name: '배경', imageRef: 'r1', children: [] };
248
+ expect(shouldRenderImageAsBg(node)).toBe(true);
249
+ });
250
+
251
+ it('container with non-vector children → CSS bg', () => {
252
+ const node = {
253
+ name: 'card', imageRef: 'r1',
254
+ children: [{ type: 'FRAME' }],
255
+ };
256
+ expect(shouldRenderImageAsBg(node)).toBe(true);
257
+ });
258
+
259
+ it('leaf with image-like name → <img>', () => {
260
+ const node = { name: 'photo-1', imageRef: 'r1', children: [] };
261
+ expect(shouldRenderImageAsBg(node)).toBe(false);
262
+ });
263
+
264
+ it('leaf with generic name → <img> (default)', () => {
265
+ const node = { name: 'Rectangle 5', imageRef: 'r1', children: [] };
266
+ expect(shouldRenderImageAsBg(node)).toBe(false);
267
+ });
268
+
269
+ it('container with only VECTOR children still → CSS bg (parent is not a leaf image)', () => {
270
+ const node = {
271
+ name: 'hero',
272
+ imageRef: 'r1',
273
+ children: [{ type: 'VECTOR' }, { type: 'VECTOR' }],
274
+ };
275
+ expect(shouldRenderImageAsBg(node)).toBe(true);
276
+ });
277
+ });
278
+
279
+ // ─── resolveImageFills + toHTML integration ─────────────────────────
280
+
281
+ describe('resolveImageFills', () => {
282
+ it('converts BG-named leaf with IMAGE fill to CSS bg (no <img>)', () => {
283
+ const tree = walk(frame('section', [imageFillNode('BG')]));
284
+ const imageMap = { 'ref-BG': 'images/bg.webp' };
285
+ resolveImageFills(tree, imageMap);
286
+ const bgChild = tree.children[0];
287
+ expect(bgChild.imageRef).toBeUndefined();
288
+ expect(bgChild.renderAsBg).toBe(true);
289
+ expect(bgChild.css.backgroundImage).toBe("url('images/bg.webp')");
290
+ expect(bgChild.css.backgroundSize).toBe('cover');
291
+ });
292
+
293
+ it('keeps <img> for leaf named like a photo', () => {
294
+ const tree = walk(frame('section', [imageFillNode('photo-hero')]));
295
+ const imageMap = { 'ref-photo-hero': 'images/photo.webp' };
296
+ resolveImageFills(tree, imageMap);
297
+ const leaf = tree.children[0];
298
+ expect(leaf.imageRef).toBe('ref-photo-hero');
299
+ expect(leaf.renderAsBg).toBeUndefined();
300
+ });
301
+
302
+ it('converts IMAGE-fill container with TEXT descendant to CSS bg', () => {
303
+ const container = imageFillNode('card', { type: 'FRAME' });
304
+ container.children = [textNode({ name: 'title' })];
305
+ const tree = walk(frame('section', [container]));
306
+ const imageMap = { 'ref-card': 'images/card.webp' };
307
+ resolveImageFills(tree, imageMap);
308
+ const card = tree.children[0];
309
+ expect(card.imageRef).toBeUndefined();
310
+ expect(card.renderAsBg).toBe(true);
311
+ });
312
+
313
+ it('uses contain for FIT scale mode', () => {
314
+ const tree = walk(frame('s', [imageFillNode('BG', { fills: [{ type: 'IMAGE', imageRef: 'ref-BG', scaleMode: 'FIT' }] })]));
315
+ const imageMap = { 'ref-BG': 'images/bg.webp' };
316
+ resolveImageFills(tree, imageMap);
317
+ expect(tree.children[0].css.backgroundSize).toBe('contain');
318
+ });
319
+ });
320
+
321
+ describe('toHTML after resolveImageFills', () => {
322
+ it('BG-named image node renders as <div>, not <img>', () => {
323
+ const tree = walk(frame('section', [imageFillNode('BG')]));
324
+ const imageMap = { 'ref-BG': 'images/bg.webp' };
325
+ resolveImageFills(tree, imageMap);
326
+ const html = toHTML(tree, '', imageMap);
327
+ expect(html).not.toMatch(/<img[^>]+images\/bg\.webp/);
328
+ expect(html).toMatch(/<div class="section-bg"/);
329
+ });
330
+
331
+ it('standalone photo leaf renders as <img>', () => {
332
+ const tree = walk(frame('section', [imageFillNode('photo-hero')]));
333
+ const imageMap = { 'ref-photo-hero': 'images/photo.webp' };
334
+ resolveImageFills(tree, imageMap);
335
+ const html = toHTML(tree, '', imageMap);
336
+ expect(html).toMatch(/<img[^>]+images\/photo\.webp/);
337
+ });
338
+ });
@@ -414,6 +414,50 @@ function extractCSS(n) {
414
414
  return result;
415
415
  }
416
416
 
417
+ // ─── Determination (image-vs-HTML) Helpers ──────────────────────────
418
+
419
+ const BG_NAME_RE = /^(BG|bg|배경|background)$/i;
420
+ const VECTOR_TYPE_RE = /^(VECTOR|LINE|BOOLEAN_OPERATION|STAR|REGULAR_POLYGON)$/;
421
+ const FOREGROUND_IMG_NAME_RE = /(^|[-_\s])(image|photo|thumb|thumbnail|avatar|icon|logo|img|picture|banner|poster)([-_\s]|\d|$)/i;
422
+
423
+ /** D1-D3: TEXT whose visual fidelity cannot be preserved by HTML text.
424
+ * D4/D5 (vector siblings, non-web font) need project context and are left to the converter. */
425
+ function isDesignTextNode(n) {
426
+ if (n.type !== 'TEXT') return false;
427
+ const fills = (n.fills || []).filter(f => f.visible !== false);
428
+ if (fills.length >= 2) return true; // D1
429
+ if (fills.some(f => typeof f.type === 'string' && f.type.startsWith('GRADIENT_'))) return true; // D3
430
+ if ((n.effects || []).some(e => e.visible !== false && (e.type === 'DROP_SHADOW' || e.type === 'INNER_SHADOW'))) return true; // D2
431
+ if ((n.strokes || []).some(s => s.visible !== false)) return true; // D2 (text stroke)
432
+ return false;
433
+ }
434
+
435
+ /** Q1: any descendant carries meaningful text content. */
436
+ function hasTextDescendantRaw(n) {
437
+ if (n.type === 'TEXT' && typeof n.characters === 'string' && n.characters.trim().length) return true;
438
+ return (n.children || []).some(hasTextDescendantRaw);
439
+ }
440
+
441
+ /** Q2: 2+ direct children sharing the same componentId (or normalized name stem). */
442
+ function hasRepeatingInstancesRaw(n) {
443
+ const kids = n.children || [];
444
+ if (kids.length < 2) return false;
445
+ const keyCount = {};
446
+ for (const c of kids) {
447
+ if (c.type !== 'INSTANCE' && c.type !== 'COMPONENT') continue;
448
+ const key = c.componentId || (c.name || '').replace(/\s*\d+\s*$/, '').trim();
449
+ if (!key) continue;
450
+ keyCount[key] = (keyCount[key] || 0) + 1;
451
+ if (keyCount[key] >= 2) return true;
452
+ }
453
+ return false;
454
+ }
455
+
456
+ /** D4 helper: direct VECTOR-family children count. */
457
+ function vectorChildCountRaw(n) {
458
+ return (n.children || []).filter(c => VECTOR_TYPE_RE.test(c.type || '')).length;
459
+ }
460
+
417
461
  // ─── Tree ───────────────────────────────────────────────────────────
418
462
 
419
463
  function walk(node, parentAbsBBox) {
@@ -441,6 +485,12 @@ function walk(node, parentAbsBBox) {
441
485
  // Translation-loss warnings (Figma → CSS incompatibilities)
442
486
  const warnings = auditNode(node);
443
487
  if (warnings.length) r.warnings = warnings;
488
+ // Determination metadata (Q1, Q2, D1-D3) — used downstream to decide <img> vs CSS bg vs composite
489
+ if (hasTextDescendantRaw(node)) r.hasTextChildren = true;
490
+ if (hasRepeatingInstancesRaw(node)) r.hasInstanceRepeat = true;
491
+ if (node.type === 'TEXT' && isDesignTextNode(node)) r.isDesignText = true;
492
+ const vcnt = vectorChildCountRaw(node);
493
+ if (vcnt > 0) r.vectorChildCount = vcnt;
444
494
  if (node.children?.length) r.children = node.children.map(c => walk(c, node.absoluteBoundingBox));
445
495
  return r;
446
496
  }
@@ -640,6 +690,45 @@ function buildImageNames(node, prefix, result = {}) {
640
690
  return result;
641
691
  }
642
692
 
693
+ /** Decide how an IMAGE-fill node should render: CSS background vs <img> tag.
694
+ * Rules (first match wins):
695
+ * 1. Has TEXT descendants → CSS bg (text would be baked into <img>)
696
+ * 2. Has repeating instance children → CSS bg (repeats need HTML structure)
697
+ * 3. Named BG/background → CSS bg
698
+ * 4. Has non-decorative children (type not in VECTOR-family) → CSS bg
699
+ * 5. Leaf named image/photo/thumb/etc → <img>
700
+ * 6. Leaf otherwise → <img> (standalone asset)
701
+ */
702
+ function shouldRenderImageAsBg(node) {
703
+ if (node.hasTextChildren) return true;
704
+ if (node.hasInstanceRepeat) return true;
705
+ if (BG_NAME_RE.test(node.name || '')) return true;
706
+ const kids = node.children || [];
707
+ const contentKids = kids.filter(c => !VECTOR_TYPE_RE.test(c.type || '') && c.type !== 'RECTANGLE');
708
+ if (contentKids.length > 0) return true;
709
+ if (kids.length > 0) return true; // any non-leaf with fills is a container → bg
710
+ if (FOREGROUND_IMG_NAME_RE.test(node.name || '')) return false;
711
+ return false; // leaf fallback: <img>
712
+ }
713
+
714
+ /** Walk tree and convert IMAGE-fill nodes that are actually backgrounds into CSS background-image.
715
+ * Mutates node.css and removes node.imageRef so downstream toHTML emits a container <div>. */
716
+ function resolveImageFills(node, imgMap) {
717
+ if (node.imageRef && imgMap[node.imageRef] && shouldRenderImageAsBg(node)) {
718
+ const url = imgMap[node.imageRef];
719
+ const scaleMode = node.imageScaleMode;
720
+ const bgSize = scaleMode === 'FIT' ? 'contain' : scaleMode === 'TILE' ? 'auto' : 'cover';
721
+ const bgRepeat = scaleMode === 'TILE' ? 'repeat' : 'no-repeat';
722
+ node.css.backgroundImage = `url('${url}')`;
723
+ node.css.backgroundSize = bgSize;
724
+ node.css.backgroundRepeat = bgRepeat;
725
+ node.css.backgroundPosition = 'center';
726
+ node.renderAsBg = true;
727
+ delete node.imageRef;
728
+ }
729
+ for (const c of node.children || []) resolveImageFills(c, imgMap);
730
+ }
731
+
643
732
  async function cmdRender(token, fk, nid, outDir, depth, scale) {
644
733
  if (!outDir) fail('--out=<dir> required');
645
734
  if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
@@ -684,22 +773,26 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
684
773
  }
685
774
 
686
775
  // 4. 복합 BG 노드 → 순차 큐 (unsafe: Figma render API rate limit)
776
+ // 텍스트/반복 인스턴스가 섞인 BG는 composite 캡처 금지 — 이미지에 TEXT가 굽혀
777
+ // HTML 레이어와 중복되거나, 리스트/카드를 한 장으로 뭉개버리는 회귀가 있음
778
+ // (exchange-section1, daily-step2-list, prize-section1 케이스)
687
779
  const bgTasks = [];
688
780
  for (const child of tree.children || []) {
689
- if (/^(BG|bg|배경)$/i.test(child.name) && child.children?.length > 3) {
690
- const bgName = `${sectionPrefix}-bg-composite.webp`;
691
- const bgPath = path.join(imgDir, bgName);
692
- bgTasks.push(async () => {
693
- const result = await screenshotWithRecovery(token, fk, child.nodeId, bgPath);
694
- if (result) {
695
- const actualName = path.basename(result.path);
696
- imageMap[`__bg_${child.nodeId}`] = `images/${actualName}`;
697
- child.imageRef = `__bg_${child.nodeId}`;
698
- child.children = [];
699
- }
700
- return result;
701
- });
702
- }
781
+ if (!BG_NAME_RE.test(child.name) || !(child.children?.length > 3)) continue;
782
+ if (child.hasTextChildren) continue; // TEXT 포함 → HTML로 분리 처리
783
+ if (child.hasInstanceRepeat) continue; // 반복 인스턴스 → v-for로 처리
784
+ const bgName = `${sectionPrefix}-bg-composite.webp`;
785
+ const bgPath = path.join(imgDir, bgName);
786
+ bgTasks.push(async () => {
787
+ const result = await screenshotWithRecovery(token, fk, child.nodeId, bgPath);
788
+ if (result) {
789
+ const actualName = path.basename(result.path);
790
+ imageMap[`__bg_${child.nodeId}`] = `images/${actualName}`;
791
+ child.imageRef = `__bg_${child.nodeId}`;
792
+ child.children = [];
793
+ }
794
+ return result;
795
+ });
703
796
  }
704
797
 
705
798
  // 5. 섹션 스크린샷도 순차 큐에 추가
@@ -717,6 +810,9 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
717
810
  sequentialQueue(bgTasks),
718
811
  ]);
719
812
 
813
+ // 6b. IMAGE-fill 노드를 CSS background 로 변환 (배경성이면 <img> 금지)
814
+ resolveImageFills(tree, imageMap);
815
+
720
816
  // 7. HTML 생성
721
817
  const html = toHTML(tree, '', imageMap);
722
818
  fs.writeFileSync(path.join(outDir, `${sectionPrefix}.html`), html);
@@ -743,26 +839,43 @@ async function cmdRender(token, fk, nid, outDir, depth, scale) {
743
839
  }, null, 2));
744
840
  }
745
841
 
842
+ // ─── Exports (for tests) ────────────────────────────────────────────
843
+
844
+ export {
845
+ walk,
846
+ extractCSS,
847
+ isDesignTextNode,
848
+ hasTextDescendantRaw,
849
+ hasRepeatingInstancesRaw,
850
+ vectorChildCountRaw,
851
+ shouldRenderImageAsBg,
852
+ resolveImageFills,
853
+ toHTML,
854
+ };
855
+
746
856
  // ─── CLI ────────────────────────────────────────────────────────────
747
857
 
748
- const args = process.argv.slice(2);
749
- const flags = {};
750
- const pos = [];
751
- for (const a of args) {
752
- if (a.startsWith('--')) { const [k,v] = a.slice(2).split('='); flags[k] = v ?? ''; }
753
- else pos.push(a);
754
- }
858
+ const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
859
+ if (invokedDirectly) {
860
+ const args = process.argv.slice(2);
861
+ const flags = {};
862
+ const pos = [];
863
+ for (const a of args) {
864
+ if (a.startsWith('--')) { const [k,v] = a.slice(2).split('='); flags[k] = v ?? ''; }
865
+ else pos.push(a);
866
+ }
755
867
 
756
- const token = loadToken();
757
- if (!token) fail('Figma token not found. Run: vibe figma setup <token>');
868
+ const token = loadToken();
869
+ if (!token) fail('Figma token not found. Run: vibe figma setup <token>');
758
870
 
759
- const [cmd, fk, nidRaw] = pos;
760
- const nid = nidRaw?.replace(/-/g, ':');
871
+ const [cmd, fk, nidRaw] = pos;
872
+ const nid = nidRaw?.replace(/-/g, ':');
761
873
 
762
- switch (cmd) {
763
- case 'tree': await cmdTree(token, fk, nid, flags.depth ? +flags.depth : undefined); break;
764
- case 'images': await cmdImages(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10); break;
765
- case 'screenshot': await cmdScreenshot(token, fk, nid, flags.out); break;
766
- case 'render': await cmdRender(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10, flags.scale ? +flags.scale : 0.667); break;
767
- default: console.log('Usage: node figma-extract.js <tree|images|screenshot|render> <fileKey> <nodeId> [flags]');
874
+ switch (cmd) {
875
+ case 'tree': await cmdTree(token, fk, nid, flags.depth ? +flags.depth : undefined); break;
876
+ case 'images': await cmdImages(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10); break;
877
+ case 'screenshot': await cmdScreenshot(token, fk, nid, flags.out); break;
878
+ case 'render': await cmdRender(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10, flags.scale ? +flags.scale : 0.667); break;
879
+ default: console.log('Usage: node figma-extract.js <tree|images|screenshot|render> <fileKey> <nodeId> [flags]');
880
+ }
768
881
  }
@@ -42,6 +42,21 @@ try {
42
42
 
43
43
  if (!prompt) process.exit(0);
44
44
 
45
+ // 레거시 SSOT 통합 — `/vibe.*` 진입 시 `.claude/vibe/`·`.coco/vibe/` → `.vibe/` 자동 이동.
46
+ // `vibe init`/`update` 와 동일한 `consolidateLegacyVibe` (dist/cli/setup/LegacyMigration.js) 를 직접 재사용. Idempotent.
47
+ if (/^\s*\/vibe\b/i.test(prompt)) {
48
+ try {
49
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.COCO_PROJECT_DIR || process.cwd();
50
+ const utils = await import('./utils.js');
51
+ const CLI_BASE = utils.getCliBaseUrl();
52
+ const { consolidateLegacyVibe } = await import(`${CLI_BASE}setup/LegacyMigration.js`);
53
+ const moved = consolidateLegacyVibe(projectDir);
54
+ if (moved.length > 0) {
55
+ process.stdout.write(`[vibe] Migrated legacy dirs → .vibe/ (${moved.join(', ')})\n`);
56
+ }
57
+ } catch { /* migration is best-effort */ }
58
+ }
59
+
45
60
  // 패턴 → 실행할 스크립트 매핑
46
61
  // 각 항목: { pattern, script, args, label }
47
62
  const DISPATCH_RULES = [
@@ -219,6 +219,10 @@ export function getLibBaseUrl() {
219
219
  return getPackageUrl(path.join('dist', 'infra', 'lib'), 'constants.js');
220
220
  }
221
221
 
222
+ export function getCliBaseUrl() {
223
+ return getPackageUrl(path.join('dist', 'cli'), 'index.js');
224
+ }
225
+
222
226
  // ─── Hook Trace Logging ───
223
227
 
224
228
  const HOOK_TRACE_PATH = path.join(VIBE_HOME_DIR, 'hook-traces.jsonl');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.9.35",
3
+ "version": "2.9.37",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",
@@ -152,11 +152,11 @@ describe('Architecture Boundaries', () => {
152
152
  | File | Purpose |
153
153
  |------|---------|
154
154
  | `tests/arch-guard.test.ts` | Executable boundary tests |
155
- | `.claude/vibe/arch-rules.json` | Machine-readable rules (for CI) |
155
+ | `.vibe/arch-rules.json` | Machine-readable rules (for CI) |
156
156
 
157
157
  ## Customization
158
158
 
159
- Users can add custom rules to `.claude/vibe/arch-rules.json`:
159
+ Users can add custom rules to `.vibe/arch-rules.json`:
160
160
 
161
161
  ```json
162
162
  {
@@ -7,17 +7,17 @@ tools: [Read]
7
7
  # Arch Rule Generator
8
8
 
9
9
  ## Role
10
- Translates a detected architecture map into a precise, machine-checkable set of import boundary rules. Merges default rules for the detected pattern with any custom rules defined in `.claude/vibe/arch-rules.json`. Outputs a normalized rule set ready for the violation checker.
10
+ Translates a detected architecture map into a precise, machine-checkable set of import boundary rules. Merges default rules for the detected pattern with any custom rules defined in `.vibe/arch-rules.json`. Outputs a normalized rule set ready for the violation checker.
11
11
 
12
12
  ## Responsibilities
13
13
  - Select default rule templates for the detected architecture pattern
14
- - Merge with custom rules from `.claude/vibe/arch-rules.json` if present
14
+ - Merge with custom rules from `.vibe/arch-rules.json` if present
15
15
  - Resolve glob patterns to concrete layer names
16
16
  - Deduplicate and normalize rule list
17
17
  - Flag rules with low confidence (detected layer with no matching files)
18
18
 
19
19
  ## Input
20
- Architecture map JSON from `arch-detector`, plus optional `.claude/vibe/arch-rules.json` for user-defined overrides.
20
+ Architecture map JSON from `arch-detector`, plus optional `.vibe/arch-rules.json` for user-defined overrides.
21
21
 
22
22
  ## Output
23
23
  Normalized rule set JSON:
@@ -53,7 +53,7 @@ function resolveImport(fromFile, imp, baseDir) {
53
53
  return path.resolve(path.dirname(fromFile), imp).replace(baseDir + path.sep, '').replace(/\\/g, '/');
54
54
  }
55
55
 
56
- const rulesPath = process.argv[2] || '.claude/vibe/arch-rules.json';
56
+ const rulesPath = process.argv[2] || '.vibe/arch-rules.json';
57
57
  const srcDir = path.resolve(process.argv[3] || 'src');
58
58
  const baseDir = path.resolve('.');
59
59
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Architecture boundary rules for arch-guard. Copy to .claude/vibe/arch-rules.json and customize.",
2
+ "_comment": "Architecture boundary rules for arch-guard. Copy to .vibe/arch-rules.json and customize.",
3
3
  "rules": [
4
4
  {
5
5
  "name": "services-no-ui",