@su-record/vibe 2.9.38 → 2.10.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/CLAUDE.md +19 -6
- package/README.md +31 -24
- package/agents/{teams/figma → figma}/figma-analyst.md +2 -2
- package/agents/research/{best-practices-agent.md → best-practices.md} +1 -1
- package/agents/research/{codebase-patterns-agent.md → codebase-patterns.md} +1 -1
- package/agents/research/{framework-docs-agent.md → framework-docs.md} +1 -1
- package/agents/research/{security-advisory-agent.md → security-advisory.md} +1 -1
- package/agents/teams/research-team.md +4 -4
- package/agents/teams/review-debate-team.md +2 -2
- package/agents/teams/security-team.md +4 -4
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts +3 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +47 -9
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +5 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +165 -23
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/cursor-skills.js +2 -2
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +19 -10
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/infra/lib/OrchestrateWorkflow.js +1 -1
- package/dist/infra/lib/OrchestrateWorkflow.js.map +1 -1
- package/dist/infra/lib/telemetry/SkillTelemetry.test.js +4 -4
- package/dist/infra/lib/telemetry/SkillTelemetry.test.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +4 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/hooks/scripts/__tests__/curation-index.test.js +157 -0
- package/hooks/scripts/__tests__/recipe-extractor.test.js +244 -0
- package/hooks/scripts/__tests__/step-counter.test.js +358 -0
- package/hooks/scripts/clone-extract.js +712 -0
- package/hooks/scripts/clone-refine.js +510 -0
- package/hooks/scripts/clone-to-scss.js +275 -0
- package/hooks/scripts/clone-validate.js +280 -0
- package/hooks/scripts/lib/curation-index.js +101 -0
- package/hooks/scripts/recipe-extractor.js +249 -0
- package/hooks/scripts/session-start.js +19 -0
- package/hooks/scripts/step-counter.js +230 -21
- package/package.json +2 -1
- package/skills/agents-md/SKILL.md +2 -0
- package/skills/arch-guard/SKILL.md +2 -0
- package/skills/brand-assets/SKILL.md +1 -0
- package/skills/capability-loop/SKILL.md +2 -0
- package/skills/characterization-test/SKILL.md +2 -0
- package/skills/chub-usage/SKILL.md +1 -0
- package/skills/claude-md-guide/SKILL.md +2 -0
- package/skills/clone/SKILL.md +361 -0
- package/skills/commerce-patterns/SKILL.md +1 -0
- package/skills/commit-push-pr/SKILL.md +1 -0
- package/skills/context7-usage/SKILL.md +1 -0
- package/skills/{vibe-contract → contract}/SKILL.md +7 -8
- package/skills/create-prd/SKILL.md +1 -0
- package/skills/design-audit/SKILL.md +1 -0
- package/skills/design-critique/SKILL.md +1 -0
- package/skills/design-distill/SKILL.md +1 -0
- package/skills/design-normalize/SKILL.md +1 -0
- package/skills/design-polish/SKILL.md +1 -0
- package/skills/design-teach/SKILL.md +2 -0
- package/skills/devlog/SKILL.md +1 -0
- package/skills/{vibe-docs → docs}/SKILL.md +5 -5
- package/skills/e2e-commerce/SKILL.md +1 -0
- package/skills/event-comms/SKILL.md +1 -0
- package/skills/event-ops/SKILL.md +1 -0
- package/skills/event-planning/SKILL.md +1 -0
- package/skills/exec-plan/SKILL.md +2 -0
- package/skills/{vibe-figma → figma}/SKILL.md +4 -3
- package/skills/{vibe-figma-convert → figma-convert}/SKILL.md +4 -3
- package/skills/{vibe-figma-extract → figma-extract}/SKILL.md +4 -3
- package/skills/git-worktree/SKILL.md +1 -0
- package/skills/handoff/SKILL.md +2 -0
- package/skills/{vibe-interview → interview}/SKILL.md +16 -16
- package/skills/parallel-research/SKILL.md +2 -0
- package/skills/{vibe-plan → plan}/SKILL.md +9 -9
- package/skills/prioritization-frameworks/SKILL.md +1 -0
- package/skills/priority-todos/SKILL.md +2 -0
- package/skills/{vibe-regress → regress}/SKILL.md +5 -6
- package/skills/rob-pike/SKILL.md +2 -0
- package/skills/seo-checklist/SKILL.md +1 -0
- package/skills/{vibe-spec → spec}/SKILL.md +14 -14
- package/skills/{vibe-spec-review → spec-review}/SKILL.md +8 -9
- package/skills/systematic-debugging/SKILL.md +2 -0
- package/skills/techdebt/SKILL.md +2 -0
- package/skills/{vibe-test → test}/SKILL.md +12 -12
- package/skills/tool-fallback/SKILL.md +1 -0
- package/skills/typescript-advanced-types/SKILL.md +1 -0
- package/skills/ui-ux-pro-max/SKILL.md +1 -0
- package/skills/user-personas/SKILL.md +1 -0
- package/skills/vercel-react-best-practices/SKILL.md +1 -0
- package/skills/vibe/SKILL.md +266 -0
- package/{commands/vibe.analyze.md → skills/vibe.analyze/SKILL.md} +2 -0
- package/skills/vibe.clone/SKILL.md +117 -0
- package/{commands/vibe.contract.md → skills/vibe.contract/SKILL.md} +3 -1
- package/{commands/vibe.docs.md → skills/vibe.docs/SKILL.md} +3 -1
- package/{commands/vibe.event.md → skills/vibe.event/SKILL.md} +2 -0
- package/{commands/vibe.figma.md → skills/vibe.figma/SKILL.md} +25 -23
- package/{commands/vibe.harness.md → skills/vibe.harness/SKILL.md} +2 -0
- package/{commands/vibe.reason.md → skills/vibe.reason/SKILL.md} +2 -0
- package/{commands/vibe.regress.md → skills/vibe.regress/SKILL.md} +5 -3
- package/{commands/vibe.review.md → skills/vibe.review/SKILL.md} +2 -0
- package/{commands/vibe.run.md → skills/vibe.run/SKILL.md} +3 -1
- package/{commands/vibe.scaffold.md → skills/vibe.scaffold/SKILL.md} +2 -0
- package/{commands/vibe.spec.md → skills/vibe.spec/SKILL.md} +36 -34
- package/{commands/vibe.test.md → skills/vibe.test/SKILL.md} +4 -2
- package/{commands/vibe.trace.md → skills/vibe.trace/SKILL.md} +7 -0
- package/{commands/vibe.utils.md → skills/vibe.utils/SKILL.md} +2 -0
- package/{commands/vibe.verify.md → skills/vibe.verify/SKILL.md} +10 -2
- package/skills/video-production/SKILL.md +1 -0
- /package/agents/{teams/figma → figma}/figma-architect.md +0 -0
- /package/agents/{teams/figma → figma}/figma-auditor.md +0 -0
- /package/agents/{teams/figma → figma}/figma-builder.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/architecture.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/behavioral-principles.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/readme.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/release-notes.md +0 -0
- /package/skills/{vibe-figma → figma}/rubrics/extraction-checklist.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/component-index.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/component-spec.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/figma-handoff.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/remapped-tree.md +0 -0
- /package/skills/{vibe-figma-convert → figma-convert}/rubrics/conversion-rules.md +0 -0
- /package/skills/{vibe-figma-convert → figma-convert}/templates/component.md +0 -0
- /package/skills/{vibe-figma-extract → figma-extract}/rubrics/image-rules.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/api.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/feature.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/library.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/mobile.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/webapp.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/website.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/bug.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/test-jest.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/test-vitest.md +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* clone-to-scss.js — sections.json → SCSS partials + class plan
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node clone-to-scss.js <sections.json> --out=<styles-dir> [--feature=<name>] [--asset-root=<public-prefix>]
|
|
8
|
+
*
|
|
9
|
+
* Output:
|
|
10
|
+
* <styles-dir>/
|
|
11
|
+
* index.scss (master orchestrator)
|
|
12
|
+
* _tokens.scss (CSS variables from tokens)
|
|
13
|
+
* _base.scss (@font-face + body defaults from stylesheets.json hints)
|
|
14
|
+
* _shared.scss (placeholder for cross-section utilities)
|
|
15
|
+
* sections/_<name>.scss (per-section partial)
|
|
16
|
+
* <styles-dir>/class-plan.json (node.id → BEM class name; HTML scaffolder applies these)
|
|
17
|
+
*
|
|
18
|
+
* Rules:
|
|
19
|
+
* - CSS values copied verbatim from sections.json — no eyeballing
|
|
20
|
+
* - Token-referenced values stay as var(--xxx)
|
|
21
|
+
* - Selectors are class-based, BEM-flavored: .{feature}__{section}__{role}
|
|
22
|
+
* - One class per node; nested rules use SCSS &__ syntax
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
|
|
28
|
+
// ─── CLI ────────────────────────────────────────────────────────────
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const [, , inputPath, ...rest] = argv;
|
|
31
|
+
const opts = {};
|
|
32
|
+
for (const a of rest) {
|
|
33
|
+
if (a.startsWith('--out=')) opts.out = a.slice(6);
|
|
34
|
+
else if (a.startsWith('--feature=')) opts.feature = a.slice(10);
|
|
35
|
+
else if (a.startsWith('--asset-root=')) opts.assetRoot = a.slice(13);
|
|
36
|
+
}
|
|
37
|
+
return { inputPath, opts };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { inputPath, opts } = parseArgs(process.argv);
|
|
41
|
+
if (!inputPath || !opts.out) {
|
|
42
|
+
console.error('Usage: node clone-to-scss.js <sections.json> --out=<styles-dir> [--feature=<name>] [--asset-root=<public-prefix>]');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
47
|
+
function kebab(s) {
|
|
48
|
+
return String(s)
|
|
49
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
50
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '')
|
|
52
|
+
.toLowerCase() || 'x';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shorten(id) {
|
|
56
|
+
// node id like "0.1.2.3" → "n-0-1-2-3"
|
|
57
|
+
return 'n-' + id.replace(/[^0-9]/g, '-').replace(/^-+|-+$/g, '').slice(0, 20);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Detect a semantic role for a node based on tag/class/text/attrs
|
|
61
|
+
function semanticRole(node) {
|
|
62
|
+
const cls = (node.classes || '').toLowerCase();
|
|
63
|
+
const tag = node.tag;
|
|
64
|
+
if (tag === 'h1' || /\bheadline\b|\bhero-title\b/.test(cls)) return 'title';
|
|
65
|
+
if (/^h[2-6]$/.test(tag)) return `heading-${tag.slice(1)}`;
|
|
66
|
+
if (tag === 'p' || /\bsubtitle\b|\bdescription\b/.test(cls)) return 'body';
|
|
67
|
+
if (tag === 'a' && (/\bbutton\b|\bbtn\b|\bcta\b/.test(cls) || (node.attrs && /button/.test(node.attrs.role || '')))) return 'button';
|
|
68
|
+
if (tag === 'button') return 'button';
|
|
69
|
+
if (tag === 'img' || (node.css && node.css['background-image'] && node.css['background-image'] !== 'none')) return 'media';
|
|
70
|
+
if (tag === 'ul' || tag === 'ol') return 'list';
|
|
71
|
+
if (tag === 'li') return 'item';
|
|
72
|
+
if (tag === 'nav' || /\bnav\b/.test(cls)) return 'nav';
|
|
73
|
+
if (tag === 'form') return 'form';
|
|
74
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') return 'field';
|
|
75
|
+
if (tag === 'header') return 'header';
|
|
76
|
+
if (tag === 'footer') return 'footer';
|
|
77
|
+
if (tag === 'section' || tag === 'article') return 'content';
|
|
78
|
+
if (tag === 'div' && node.children && node.children.length > 1) return 'group';
|
|
79
|
+
return 'el';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Class plan: assign one BEM class per visible node ──────────────
|
|
83
|
+
function buildClassPlan(sections, feature) {
|
|
84
|
+
const plan = {}; // id → class name
|
|
85
|
+
const usedNames = new Map(); // base name → count for disambiguation
|
|
86
|
+
const sectionUsedRoles = new Map();
|
|
87
|
+
|
|
88
|
+
const assign = (sectionName, node, parentRole) => {
|
|
89
|
+
if (!node || node.pseudo) return;
|
|
90
|
+
const sectionKey = kebab(sectionName);
|
|
91
|
+
let role = semanticRole(node);
|
|
92
|
+
if (parentRole === 'list' && role !== 'item') role = 'item';
|
|
93
|
+
// disambiguate within section by role
|
|
94
|
+
const k = `${sectionKey}::${role}`;
|
|
95
|
+
const idx = (sectionUsedRoles.get(k) || 0) + 1;
|
|
96
|
+
sectionUsedRoles.set(k, idx);
|
|
97
|
+
const suffix = idx === 1 ? role : `${role}-${idx}`;
|
|
98
|
+
const cls = `${feature}__${sectionKey}__${suffix}`;
|
|
99
|
+
plan[node.id] = cls;
|
|
100
|
+
for (const child of (node.children || [])) assign(sectionName, child, role);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const sec of sections) {
|
|
104
|
+
// Root section class
|
|
105
|
+
const sectionKey = kebab(sec.name);
|
|
106
|
+
plan[sec.nodeRef] = `${feature}__${sectionKey}`;
|
|
107
|
+
for (const child of (sec.children || [])) assign(sec.name, child, 'section');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return plan;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── CSS emission ───────────────────────────────────────────────────
|
|
114
|
+
const SKIP_DEFAULT_VALUES = {
|
|
115
|
+
'pointer-events': 'auto',
|
|
116
|
+
'visibility': 'visible',
|
|
117
|
+
'opacity': '1',
|
|
118
|
+
'transform': 'none',
|
|
119
|
+
'background-repeat': 'repeat',
|
|
120
|
+
'background-attachment': 'scroll',
|
|
121
|
+
'background-blend-mode': 'normal',
|
|
122
|
+
'mix-blend-mode': 'normal',
|
|
123
|
+
'filter': 'none',
|
|
124
|
+
'backdrop-filter': 'none',
|
|
125
|
+
'overflow': 'visible',
|
|
126
|
+
'overflow-x': 'visible',
|
|
127
|
+
'overflow-y': 'visible',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function emitDeclarations(css, indent) {
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const [prop, val] of Object.entries(css)) {
|
|
133
|
+
if (!val || prop.startsWith('--')) continue;
|
|
134
|
+
if (SKIP_DEFAULT_VALUES[prop] === val) continue;
|
|
135
|
+
out.push(`${indent}${prop}: ${val};`);
|
|
136
|
+
}
|
|
137
|
+
return out.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function emitNodeRule(node, plan, indent) {
|
|
141
|
+
const cls = plan[node.id];
|
|
142
|
+
if (!cls) return '';
|
|
143
|
+
const lines = [];
|
|
144
|
+
lines.push(`${indent}.${cls} {`);
|
|
145
|
+
const decl = emitDeclarations(node.css || {}, indent + ' ');
|
|
146
|
+
if (decl) lines.push(decl);
|
|
147
|
+
// pseudo-elements
|
|
148
|
+
for (const child of (node.children || [])) {
|
|
149
|
+
if (child.pseudo) {
|
|
150
|
+
const kind = child.tag.replace('::', '');
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(`${indent} &::${kind} {`);
|
|
153
|
+
lines.push(emitDeclarations(child.css || {}, indent + ' '));
|
|
154
|
+
lines.push(`${indent} }`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
lines.push(`${indent}}`);
|
|
158
|
+
|
|
159
|
+
// Non-pseudo children — emit at same level (BEM flat)
|
|
160
|
+
const childRules = (node.children || [])
|
|
161
|
+
.filter((c) => !c.pseudo)
|
|
162
|
+
.map((c) => emitNodeRule(c, plan, indent))
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
|
|
165
|
+
return [lines.join('\n'), ...childRules].join('\n\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── File emission ──────────────────────────────────────────────────
|
|
169
|
+
function emitTokens(tokens) {
|
|
170
|
+
const lines = [':root {'];
|
|
171
|
+
for (const c of tokens.colors) lines.push(` --${c.name}: ${c.value};`);
|
|
172
|
+
for (const t of tokens.typography) {
|
|
173
|
+
lines.push(` --${t.name}-family: ${t.family};`);
|
|
174
|
+
lines.push(` --${t.name}-size: ${t.size};`);
|
|
175
|
+
lines.push(` --${t.name}-weight: ${t.weight};`);
|
|
176
|
+
if (t.lineHeight && t.lineHeight !== 'normal') lines.push(` --${t.name}-line-height: ${t.lineHeight};`);
|
|
177
|
+
if (t.letterSpacing && t.letterSpacing !== 'normal') lines.push(` --${t.name}-letter-spacing: ${t.letterSpacing};`);
|
|
178
|
+
}
|
|
179
|
+
for (const s of tokens.spacing) lines.push(` --${s.name}: ${s.value};`);
|
|
180
|
+
for (const r of tokens.radius) lines.push(` --${r.name}: ${r.value};`);
|
|
181
|
+
for (const s of tokens.shadow) lines.push(` --${s.name}: ${s.value};`);
|
|
182
|
+
lines.push('}');
|
|
183
|
+
return lines.join('\n') + '\n';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function emitBase(feature, stylesheets) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
lines.push('// Auto-generated by clone-to-scss.js — do not edit by hand.');
|
|
189
|
+
lines.push('');
|
|
190
|
+
if (stylesheets && stylesheets.fontFaces) {
|
|
191
|
+
for (const ff of stylesheets.fontFaces) {
|
|
192
|
+
lines.push('@font-face {');
|
|
193
|
+
lines.push(` font-family: '${ff.family}';`);
|
|
194
|
+
if (ff.weight) lines.push(` font-weight: ${ff.weight};`);
|
|
195
|
+
if (ff.style) lines.push(` font-style: ${ff.style};`);
|
|
196
|
+
if (ff.display) lines.push(` font-display: ${ff.display};`);
|
|
197
|
+
const srcs = ff.sources.map((s) => s.format
|
|
198
|
+
? `url('${s.url}') format('${s.format}')`
|
|
199
|
+
: `url('${s.url}')`).join(',\n ');
|
|
200
|
+
lines.push(` src: ${srcs};`);
|
|
201
|
+
lines.push('}');
|
|
202
|
+
lines.push('');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function emitSection(section, plan) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
lines.push(`// Section: ${section.name}`);
|
|
211
|
+
lines.push('');
|
|
212
|
+
// root + descendants
|
|
213
|
+
lines.push(emitNodeRule({ ...section, id: section.nodeRef, children: section.children }, plan, ''));
|
|
214
|
+
return lines.join('\n') + '\n';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function emitIndex(feature, sections) {
|
|
218
|
+
const lines = [];
|
|
219
|
+
lines.push(`// ${feature} — auto-generated by clone-to-scss.js`);
|
|
220
|
+
lines.push('');
|
|
221
|
+
lines.push(`@use './tokens';`);
|
|
222
|
+
lines.push(`@use './base';`);
|
|
223
|
+
lines.push(`@use './shared';`);
|
|
224
|
+
for (const sec of sections) {
|
|
225
|
+
lines.push(`@use './sections/${kebab(sec.name)}';`);
|
|
226
|
+
}
|
|
227
|
+
return lines.join('\n') + '\n';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Main ───────────────────────────────────────────────────────────
|
|
231
|
+
function main() {
|
|
232
|
+
const data = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
233
|
+
const feature = opts.feature || data.meta?.feature || 'clone';
|
|
234
|
+
const featureClass = kebab(feature);
|
|
235
|
+
|
|
236
|
+
// Read sibling stylesheets.json if present (from clone-extract.js output)
|
|
237
|
+
const sectionsDir = path.dirname(inputPath);
|
|
238
|
+
const stylesheetsPath = path.join(sectionsDir, 'stylesheets.json');
|
|
239
|
+
let stylesheets = null;
|
|
240
|
+
if (fs.existsSync(stylesheetsPath)) {
|
|
241
|
+
stylesheets = JSON.parse(fs.readFileSync(stylesheetsPath, 'utf8'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const outDir = opts.out;
|
|
245
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
246
|
+
fs.mkdirSync(path.join(outDir, 'sections'), { recursive: true });
|
|
247
|
+
|
|
248
|
+
const plan = buildClassPlan(data.sections, featureClass);
|
|
249
|
+
|
|
250
|
+
// Write files
|
|
251
|
+
fs.writeFileSync(path.join(outDir, '_tokens.scss'), emitTokens(data.tokens));
|
|
252
|
+
fs.writeFileSync(path.join(outDir, '_base.scss'), emitBase(featureClass, stylesheets));
|
|
253
|
+
fs.writeFileSync(
|
|
254
|
+
path.join(outDir, '_shared.scss'),
|
|
255
|
+
`// Cross-section utilities — extend as needed.\n`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
for (const sec of data.sections) {
|
|
259
|
+
const fname = `_${kebab(sec.name)}.scss`;
|
|
260
|
+
fs.writeFileSync(path.join(outDir, 'sections', fname), emitSection(sec, plan));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fs.writeFileSync(path.join(outDir, 'index.scss'), emitIndex(featureClass, data.sections));
|
|
264
|
+
fs.writeFileSync(path.join(outDir, 'class-plan.json'), JSON.stringify(plan, null, 2));
|
|
265
|
+
|
|
266
|
+
console.log(`[clone-to-scss] done → ${outDir}`);
|
|
267
|
+
console.log(` sections: ${data.sections.length}, classes: ${Object.keys(plan).length}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try { main(); }
|
|
271
|
+
catch (e) {
|
|
272
|
+
console.error(`[clone-to-scss] FAIL: ${e.message}`);
|
|
273
|
+
if (process.env.DEBUG) console.error(e.stack);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* clone-validate.js — written SCSS vs sections.json (source of truth)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node clone-validate.js <styles-dir> <sections.json> [--section=<name>]
|
|
8
|
+
*
|
|
9
|
+
* Reads:
|
|
10
|
+
* <styles-dir>/sections/_<section>.scss
|
|
11
|
+
* <styles-dir>/class-plan.json
|
|
12
|
+
* <sections.json>
|
|
13
|
+
*
|
|
14
|
+
* Compares emitted CSS declarations against expectations:
|
|
15
|
+
* - Missing property → P1
|
|
16
|
+
* - Mismatched value → P1 (if box prop and delta > 4px), else P2
|
|
17
|
+
* - Extra property → P3 (informational)
|
|
18
|
+
*
|
|
19
|
+
* Exit codes:
|
|
20
|
+
* 0 PASS, 1 FAIL (P1 found), 2 usage error
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'fs';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const [, , stylesDir, sectionsPath, ...rest] = argv;
|
|
28
|
+
const opts = {};
|
|
29
|
+
for (const a of rest) {
|
|
30
|
+
if (a.startsWith('--section=')) opts.section = a.slice(10);
|
|
31
|
+
else if (a === '--quiet') opts.quiet = true;
|
|
32
|
+
}
|
|
33
|
+
return { stylesDir, sectionsPath, opts };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { stylesDir, sectionsPath, opts } = parseArgs(process.argv);
|
|
37
|
+
if (!stylesDir || !sectionsPath) {
|
|
38
|
+
console.error('Usage: node clone-validate.js <styles-dir> <sections.json> [--section=<name>]');
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Minimal SCSS parser ────────────────────────────────────────────
|
|
43
|
+
// Strips comments, expands `&::pseudo` (one level), flattens nested rules.
|
|
44
|
+
// NOT a full SCSS engine — assumes clone-to-scss.js output style.
|
|
45
|
+
|
|
46
|
+
function stripComments(src) {
|
|
47
|
+
return src
|
|
48
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
49
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseScss(src) {
|
|
53
|
+
const clean = stripComments(src);
|
|
54
|
+
const rules = new Map(); // selector → { prop: value }
|
|
55
|
+
let i = 0;
|
|
56
|
+
const len = clean.length;
|
|
57
|
+
|
|
58
|
+
const skipWs = () => { while (i < len && /\s/.test(clean[i])) i++; };
|
|
59
|
+
|
|
60
|
+
const readUntil = (chars) => {
|
|
61
|
+
let buf = '';
|
|
62
|
+
while (i < len && !chars.includes(clean[i])) buf += clean[i++];
|
|
63
|
+
return buf;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const stack = []; // selector stack
|
|
67
|
+
|
|
68
|
+
while (i < len) {
|
|
69
|
+
skipWs();
|
|
70
|
+
if (i >= len) break;
|
|
71
|
+
const ch = clean[i];
|
|
72
|
+
if (ch === '}') { stack.pop(); i++; continue; }
|
|
73
|
+
|
|
74
|
+
// Lookahead: is this a rule (has '{' before ';' or end) or declaration?
|
|
75
|
+
let depth = 0;
|
|
76
|
+
let j = i;
|
|
77
|
+
let kind = 'decl';
|
|
78
|
+
while (j < len) {
|
|
79
|
+
const c = clean[j];
|
|
80
|
+
if (c === '"' || c === "'") {
|
|
81
|
+
const q = c; j++;
|
|
82
|
+
while (j < len && clean[j] !== q) { if (clean[j] === '\\') j++; j++; }
|
|
83
|
+
}
|
|
84
|
+
if (c === '(') depth++;
|
|
85
|
+
else if (c === ')') depth--;
|
|
86
|
+
else if (depth === 0 && c === ';') { kind = 'decl'; break; }
|
|
87
|
+
else if (depth === 0 && c === '{') { kind = 'rule'; break; }
|
|
88
|
+
else if (depth === 0 && c === '}') { break; }
|
|
89
|
+
j++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (kind === 'rule') {
|
|
93
|
+
const selector = clean.slice(i, j).trim();
|
|
94
|
+
i = j + 1; // consume '{'
|
|
95
|
+
// Resolve nested selector
|
|
96
|
+
const parent = stack[stack.length - 1] || '';
|
|
97
|
+
let resolved;
|
|
98
|
+
if (selector.startsWith('&')) {
|
|
99
|
+
resolved = parent + selector.slice(1);
|
|
100
|
+
} else if (parent) {
|
|
101
|
+
// descendant
|
|
102
|
+
resolved = selector.split(',').map((s) => parent + ' ' + s.trim()).join(', ');
|
|
103
|
+
} else {
|
|
104
|
+
resolved = selector;
|
|
105
|
+
}
|
|
106
|
+
stack.push(resolved);
|
|
107
|
+
if (!rules.has(resolved)) rules.set(resolved, {});
|
|
108
|
+
} else {
|
|
109
|
+
// declaration
|
|
110
|
+
const text = clean.slice(i, j).trim();
|
|
111
|
+
i = j + 1;
|
|
112
|
+
if (!text) continue;
|
|
113
|
+
const colon = text.indexOf(':');
|
|
114
|
+
if (colon < 0) continue;
|
|
115
|
+
const prop = text.slice(0, colon).trim();
|
|
116
|
+
const val = text.slice(colon + 1).trim();
|
|
117
|
+
const sel = stack[stack.length - 1];
|
|
118
|
+
if (sel) {
|
|
119
|
+
if (!rules.has(sel)) rules.set(sel, {});
|
|
120
|
+
rules.get(sel)[prop] = val;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return rules;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Comparison ─────────────────────────────────────────────────────
|
|
129
|
+
const BOX_PROPS = new Set([
|
|
130
|
+
'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
|
|
131
|
+
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
132
|
+
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
133
|
+
'top', 'right', 'bottom', 'left', 'gap', 'row-gap', 'column-gap',
|
|
134
|
+
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
function pxOf(v) {
|
|
138
|
+
if (typeof v !== 'string') return null;
|
|
139
|
+
const m = /^(-?\d+(?:\.\d+)?)px$/.exec(v.trim());
|
|
140
|
+
return m ? Number(m[1]) : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalize(v) {
|
|
144
|
+
if (typeof v !== 'string') return v;
|
|
145
|
+
return v.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function compareValue(prop, expected, actual) {
|
|
149
|
+
if (normalize(expected) === normalize(actual)) return { ok: true };
|
|
150
|
+
if (BOX_PROPS.has(prop)) {
|
|
151
|
+
const ep = pxOf(expected), ap = pxOf(actual);
|
|
152
|
+
if (ep !== null && ap !== null) {
|
|
153
|
+
const delta = Math.abs(ep - ap);
|
|
154
|
+
return { ok: delta <= 4, severity: delta > 4 ? 'P1' : 'P2', delta };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { ok: false, severity: 'P1' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function walkNodes(section) {
|
|
161
|
+
const flat = [];
|
|
162
|
+
const recur = (n) => {
|
|
163
|
+
flat.push(n);
|
|
164
|
+
for (const c of (n.children || [])) recur(c);
|
|
165
|
+
};
|
|
166
|
+
flat.push({ id: section.nodeRef, css: section.css, tag: section.tag, classes: section.classes });
|
|
167
|
+
for (const c of (section.children || [])) recur(c);
|
|
168
|
+
return flat;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Main ───────────────────────────────────────────────────────────
|
|
172
|
+
function main() {
|
|
173
|
+
const sectionsJson = JSON.parse(fs.readFileSync(sectionsPath, 'utf8'));
|
|
174
|
+
const classPlanPath = path.join(stylesDir, 'class-plan.json');
|
|
175
|
+
if (!fs.existsSync(classPlanPath)) {
|
|
176
|
+
console.error(`class-plan.json not found at ${classPlanPath}. Run clone-to-scss.js first.`);
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
const classPlan = JSON.parse(fs.readFileSync(classPlanPath, 'utf8'));
|
|
180
|
+
|
|
181
|
+
const targets = opts.section
|
|
182
|
+
? sectionsJson.sections.filter((s) => s.name === opts.section)
|
|
183
|
+
: sectionsJson.sections;
|
|
184
|
+
|
|
185
|
+
if (opts.section && targets.length === 0) {
|
|
186
|
+
console.error(`Section not found: ${opts.section}`);
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const issues = [];
|
|
191
|
+
let totalChecked = 0;
|
|
192
|
+
|
|
193
|
+
for (const section of targets) {
|
|
194
|
+
const kebabName = section.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
195
|
+
const scssPath = path.join(stylesDir, 'sections', `_${kebabName}.scss`);
|
|
196
|
+
if (!fs.existsSync(scssPath)) {
|
|
197
|
+
issues.push({ section: section.name, severity: 'P1', kind: 'missing-file', path: scssPath });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const scss = fs.readFileSync(scssPath, 'utf8');
|
|
201
|
+
const rules = parseScss(scss);
|
|
202
|
+
|
|
203
|
+
const flat = walkNodes(section);
|
|
204
|
+
for (const node of flat) {
|
|
205
|
+
const cls = classPlan[node.id];
|
|
206
|
+
if (!cls || !node.css) continue;
|
|
207
|
+
const selector = `.${cls}`;
|
|
208
|
+
const declared = rules.get(selector) || {};
|
|
209
|
+
|
|
210
|
+
for (const [prop, expected] of Object.entries(node.css)) {
|
|
211
|
+
if (prop.startsWith('--')) continue;
|
|
212
|
+
if (!expected || expected === 'normal' || expected === 'auto' || expected === 'none') continue;
|
|
213
|
+
totalChecked++;
|
|
214
|
+
const actual = declared[prop];
|
|
215
|
+
if (actual === undefined) {
|
|
216
|
+
issues.push({
|
|
217
|
+
section: section.name,
|
|
218
|
+
nodeId: node.id,
|
|
219
|
+
class: cls,
|
|
220
|
+
prop,
|
|
221
|
+
severity: 'P1',
|
|
222
|
+
kind: 'missing-prop',
|
|
223
|
+
expected,
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const cmp = compareValue(prop, expected, actual);
|
|
228
|
+
if (!cmp.ok) {
|
|
229
|
+
issues.push({
|
|
230
|
+
section: section.name,
|
|
231
|
+
nodeId: node.id,
|
|
232
|
+
class: cls,
|
|
233
|
+
prop,
|
|
234
|
+
severity: cmp.severity,
|
|
235
|
+
kind: 'value-mismatch',
|
|
236
|
+
expected,
|
|
237
|
+
actual,
|
|
238
|
+
delta: cmp.delta,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const p1 = issues.filter((i) => i.severity === 'P1');
|
|
246
|
+
const p2 = issues.filter((i) => i.severity === 'P2');
|
|
247
|
+
const p3 = issues.filter((i) => i.severity === 'P3');
|
|
248
|
+
|
|
249
|
+
if (!opts.quiet) {
|
|
250
|
+
console.log(`[clone-validate] checked ${totalChecked} declarations across ${targets.length} section(s)`);
|
|
251
|
+
console.log(` P1: ${p1.length}, P2: ${p2.length}, P3: ${p3.length}`);
|
|
252
|
+
const show = (label, list, limit = 20) => {
|
|
253
|
+
if (list.length === 0) return;
|
|
254
|
+
console.log(`\n${label}:`);
|
|
255
|
+
for (const it of list.slice(0, limit)) {
|
|
256
|
+
const loc = it.class ? `.${it.class}` : it.path || '?';
|
|
257
|
+
if (it.kind === 'missing-file') console.log(` ${it.severity} ${it.section}: missing ${it.path}`);
|
|
258
|
+
else if (it.kind === 'missing-prop') console.log(` ${it.severity} ${loc} { ${it.prop}: MISSING (expected ${it.expected}) }`);
|
|
259
|
+
else console.log(` ${it.severity} ${loc} { ${it.prop}: ${it.actual} ≠ ${it.expected}${it.delta != null ? ` (Δ${it.delta}px)` : ''} }`);
|
|
260
|
+
}
|
|
261
|
+
if (list.length > limit) console.log(` …and ${list.length - limit} more`);
|
|
262
|
+
};
|
|
263
|
+
show('P1 issues', p1);
|
|
264
|
+
show('P2 issues', p2);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (p1.length > 0) {
|
|
268
|
+
if (!opts.quiet) console.error(`\n[clone-validate] FAIL: ${p1.length} P1 issue(s)`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
if (!opts.quiet) console.log('\n[clone-validate] PASS');
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try { main(); }
|
|
276
|
+
catch (e) {
|
|
277
|
+
console.error(`[clone-validate] FAIL: ${e.message}`);
|
|
278
|
+
if (process.env.DEBUG) console.error(e.stack);
|
|
279
|
+
process.exit(2);
|
|
280
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 3 — Curation index loader
|
|
3
|
+
*
|
|
4
|
+
* `.vibe/recipes/*.md` 와 `.vibe/anti-patterns/*.md` 의 frontmatter 만 parse 해
|
|
5
|
+
* 1줄 요약 인덱스를 만든다. 본문은 읽지 않음 (세션 컨텍스트 절약).
|
|
6
|
+
*
|
|
7
|
+
* SPEC 결정:
|
|
8
|
+
* - INDEX.jsonl 미사용. 디렉토리 스캔 + frontmatter parse 가 충분히 빠르다 (<100 파일).
|
|
9
|
+
* - 최근 N=5 상한 (created 내림차순).
|
|
10
|
+
*
|
|
11
|
+
* 의도적 제한:
|
|
12
|
+
* - 본격 YAML parser 의존성 추가 거부. 우리가 *직접 작성*한 frontmatter 만
|
|
13
|
+
* 읽으므로 문법이 정해져 있다. 라인별 정규식이면 충분.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { projectVibeRoot } from '../utils.js';
|
|
18
|
+
|
|
19
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
20
|
+
// "key: value" 또는 'key: "quoted value"' (이스케이프 \\" 포함)
|
|
21
|
+
const FIELD_RE = /^([a-z][a-z0-9_-]*):\s*(?:"((?:[^"\\]|\\.)*)"|(.+?))\s*$/;
|
|
22
|
+
|
|
23
|
+
function parseFrontmatter(content) {
|
|
24
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
const fields = {};
|
|
27
|
+
for (const line of m[1].split('\n')) {
|
|
28
|
+
const fm = FIELD_RE.exec(line);
|
|
29
|
+
if (!fm) continue;
|
|
30
|
+
const key = fm[1];
|
|
31
|
+
const value = fm[2] !== undefined ? fm[2].replace(/\\"/g, '"') : fm[3];
|
|
32
|
+
fields[key] = value;
|
|
33
|
+
}
|
|
34
|
+
return fields;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readHead(filePath, bytes = 2048) {
|
|
38
|
+
const fd = fs.openSync(filePath, 'r');
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.alloc(bytes);
|
|
41
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
42
|
+
return buf.toString('utf-8', 0, n);
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function listMd(dir) {
|
|
49
|
+
if (!fs.existsSync(dir)) return [];
|
|
50
|
+
return fs.readdirSync(dir)
|
|
51
|
+
.filter((f) => f.endsWith('.md') && !f.startsWith('_') && f !== 'README.md')
|
|
52
|
+
.map((f) => path.join(dir, f));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeParse(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
const head = readHead(filePath);
|
|
58
|
+
const fields = parseFrontmatter(head);
|
|
59
|
+
if (!fields || !fields.slug) return null;
|
|
60
|
+
return fields;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function compareCreatedDesc(a, b) {
|
|
67
|
+
// created 가 ISO 면 문자열 비교로 시간 정렬 가능, 아니면 mtime fallback
|
|
68
|
+
return (b.created || '').localeCompare(a.created || '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 프로젝트의 recipes + anti-patterns 인덱스를 로드.
|
|
73
|
+
* @returns { recipes: [{slug, summary}], antiPatterns: [{tag, summary}] }
|
|
74
|
+
*/
|
|
75
|
+
export function loadCurationIndex(projectDir, opts = {}) {
|
|
76
|
+
const { recipeLimit = 5, antiPatternLimit = 5 } = opts;
|
|
77
|
+
const root = projectVibeRoot(projectDir);
|
|
78
|
+
|
|
79
|
+
const recipes = listMd(path.join(root, 'recipes'))
|
|
80
|
+
.map(safeParse).filter(Boolean)
|
|
81
|
+
.sort(compareCreatedDesc)
|
|
82
|
+
.slice(0, recipeLimit)
|
|
83
|
+
.map((f) => ({
|
|
84
|
+
slug: f.slug,
|
|
85
|
+
summary: f.recipe || f['symptom-context'] || '(no summary)',
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const antiPatterns = listMd(path.join(root, 'anti-patterns'))
|
|
89
|
+
.map(safeParse).filter(Boolean)
|
|
90
|
+
.sort(compareCreatedDesc)
|
|
91
|
+
.slice(0, antiPatternLimit)
|
|
92
|
+
.map((f) => ({
|
|
93
|
+
tag: f['root-cause-tag'] || 'other',
|
|
94
|
+
summary: f['suggested-stop'] || f['trigger-signature'] || '(no summary)',
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return { recipes, antiPatterns };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Test-only: frontmatter parser 노출 */
|
|
101
|
+
export const _internal = { parseFrontmatter };
|