@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.
- package/CLAUDE.md +1 -1
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/agents/compounder.md +6 -6
- package/agents/diagrammer.md +2 -2
- package/agents/e2e-tester.md +5 -5
- package/agents/qa/acceptance-tester.md +2 -2
- package/agents/refactor-cleaner.md +2 -2
- package/agents/tester.md +1 -1
- package/agents/ui/ui-antipattern-detector.md +1 -1
- package/agents/ui/ui-design-system-gen.md +3 -3
- package/agents/ui/ui-industry-analyzer.md +1 -1
- package/agents/ui/ui-layout-architect.md +1 -1
- package/agents/ui/ui-stack-implementer.md +1 -1
- package/commands/vibe.analyze.md +1 -1
- package/commands/vibe.contract.md +2 -2
- package/commands/vibe.figma.md +3 -3
- package/commands/vibe.harness.md +3 -3
- package/commands/vibe.regress.md +2 -2
- package/commands/vibe.review.md +4 -4
- package/commands/vibe.run.md +27 -27
- package/commands/vibe.scaffold.md +1 -1
- package/commands/vibe.spec.md +38 -38
- package/commands/vibe.trace.md +14 -14
- package/commands/vibe.utils.md +3 -3
- package/commands/vibe.verify.md +18 -18
- package/dist/infra/lib/figma/extract.d.ts.map +1 -1
- package/dist/infra/lib/figma/extract.js +55 -0
- package/dist/infra/lib/figma/extract.js.map +1 -1
- package/dist/infra/lib/figma/types.d.ts +8 -0
- package/dist/infra/lib/figma/types.d.ts.map +1 -1
- package/hooks/scripts/__tests__/figma-extract.test.js +338 -0
- package/hooks/scripts/figma-extract.js +144 -31
- package/hooks/scripts/prompt-dispatcher.js +15 -0
- package/hooks/scripts/utils.js +4 -0
- package/package.json +1 -1
- package/skills/arch-guard/SKILL.md +2 -2
- package/skills/arch-guard/agents/rule-generator.md +3 -3
- package/skills/arch-guard/scripts/check-boundaries.js +1 -1
- package/skills/arch-guard/templates/arch-rules.json +1 -1
- package/skills/capability-loop/SKILL.md +2 -2
- package/skills/capability-loop/templates/capability-spec.md +1 -1
- package/skills/claude-md-guide/SKILL.md +2 -2
- package/skills/design-audit/SKILL.md +3 -3
- package/skills/design-critique/SKILL.md +2 -2
- package/skills/design-distill/SKILL.md +1 -1
- package/skills/design-normalize/SKILL.md +4 -4
- package/skills/design-polish/SKILL.md +2 -2
- package/skills/design-teach/SKILL.md +6 -6
- package/skills/design-teach/templates/design-context.json +1 -1
- package/skills/devlog/SKILL.md +1 -1
- package/skills/event-planning/SKILL.md +1 -1
- package/skills/exec-plan/SKILL.md +5 -5
- package/skills/exec-plan/agents/decomposer.md +1 -1
- package/skills/exec-plan/templates/plan.md +2 -2
- package/skills/parallel-research/SKILL.md +3 -3
- package/skills/parallel-research/orchestrator.md +1 -1
- package/skills/parallel-research/templates/paper.md +1 -1
- package/skills/priority-todos/SKILL.md +1 -1
- package/skills/vibe-contract/SKILL.md +6 -6
- package/skills/vibe-interview/SKILL.md +6 -6
- package/skills/vibe-interview/checklists/feature.md +1 -1
- package/skills/vibe-plan/SKILL.md +10 -10
- package/skills/vibe-regress/SKILL.md +5 -5
- package/skills/vibe-regress/templates/bug.md +1 -1
- package/skills/vibe-regress/templates/test-jest.md +1 -1
- package/skills/vibe-regress/templates/test-vitest.md +1 -1
- package/skills/vibe-spec/SKILL.md +45 -45
- package/skills/vibe-spec-review/SKILL.md +21 -21
- package/skills/vibe-test/SKILL.md +1 -1
- package/vibe/constitution.md +2 -2
- package/vibe/templates/claudemd-template.md +4 -4
- package/vibe/templates/constitution-template.md +2 -2
- 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 (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
700
|
-
|
|
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
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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 = [
|
package/hooks/scripts/utils.js
CHANGED
|
@@ -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
|
@@ -152,11 +152,11 @@ describe('Architecture Boundaries', () => {
|
|
|
152
152
|
| File | Purpose |
|
|
153
153
|
|------|---------|
|
|
154
154
|
| `tests/arch-guard.test.ts` | Executable boundary tests |
|
|
155
|
-
| `.
|
|
155
|
+
| `.vibe/arch-rules.json` | Machine-readable rules (for CI) |
|
|
156
156
|
|
|
157
157
|
## Customization
|
|
158
158
|
|
|
159
|
-
Users can add custom rules to `.
|
|
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 `.
|
|
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 `.
|
|
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 `.
|
|
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] || '.
|
|
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
|
|