@fragments-sdk/cli 0.10.1 → 0.12.1
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/dist/ai-client-I6MDWNYA.js +21 -0
- package/dist/bin.js +292 -367
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
- package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
- package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
- package/dist/chunk-GVDSFQ4E.js.map +1 -0
- package/dist/chunk-JJ2VRTBU.js +626 -0
- package/dist/chunk-JJ2VRTBU.js.map +1 -0
- package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
- package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
- package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
- package/dist/chunk-OQKMEFOS.js.map +1 -0
- package/dist/chunk-SXTKFDCR.js +104 -0
- package/dist/chunk-SXTKFDCR.js.map +1 -0
- package/dist/chunk-T5OMVL7E.js +443 -0
- package/dist/chunk-T5OMVL7E.js.map +1 -0
- package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
- package/dist/chunk-TPWGL2XS.js.map +1 -0
- package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
- package/dist/chunk-WFS63PCW.js.map +1 -0
- package/dist/core/index.js +9 -1
- package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
- package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/init-ZSX3NRCZ.js +636 -0
- package/dist/init-ZSX3NRCZ.js.map +1 -0
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
- package/dist/scan-generate-SYU4PYZD.js +1115 -0
- package/dist/scan-generate-SYU4PYZD.js.map +1 -0
- package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
- package/dist/snapshot-XOISO2IS.js +139 -0
- package/dist/snapshot-XOISO2IS.js.map +1 -0
- package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
- package/dist/static-viewer-5GXH2MGE.js.map +1 -0
- package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
- package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
- package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
- package/dist/viewer-7ZEAFBVN.js.map +1 -0
- package/package.json +6 -14
- package/src/ai-client.ts +156 -0
- package/src/bin.ts +74 -2
- package/src/build.ts +95 -33
- package/src/commands/__tests__/drift-sync.test.ts +252 -0
- package/src/commands/__tests__/scan-generate.test.ts +497 -45
- package/src/commands/enhance.ts +11 -35
- package/src/commands/init.ts +296 -193
- package/src/commands/scan-generate.ts +740 -139
- package/src/commands/scan.ts +37 -32
- package/src/commands/setup.ts +143 -52
- package/src/commands/snapshot.ts +197 -0
- package/src/commands/sync.ts +357 -0
- package/src/commands/validate.ts +43 -1
- package/src/core/component-extractor.test.ts +282 -0
- package/src/core/component-extractor.ts +1030 -0
- package/src/core/discovery.ts +93 -7
- package/src/service/enhance/props-extractor.ts +235 -13
- package/src/validators.ts +236 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
- package/src/viewer/server.ts +37 -22
- package/src/viewer/vite-plugin.ts +25 -9
- package/dist/chunk-5G3VZH43.js.map +0 -1
- package/dist/chunk-OQO55NKV.js.map +0 -1
- package/dist/chunk-WXSR2II7.js.map +0 -1
- package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
- package/dist/init-NDQXUWDU.js +0 -796
- package/dist/init-NDQXUWDU.js.map +0 -1
- package/dist/scan-generate-SJAN5MVI.js +0 -691
- package/dist/scan-generate-SJAN5MVI.js.map +0 -1
- package/dist/viewer-DNMNC5VS.js.map +0 -1
- package/src/ai.ts +0 -266
- package/src/commands/init-framework.ts +0 -414
- package/src/mcp/bin.ts +0 -36
- package/src/migrate/bin.ts +0 -114
- package/src/theme/index.ts +0 -77
- package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
- package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
- package/src/viewer/__tests__/render-utils.test.ts +0 -232
- package/src/viewer/__tests__/style-utils.test.ts +0 -404
- package/src/viewer/assets/fragments-logo.ts +0 -4
- package/src/viewer/assets/fragments_logo.png +0 -0
- package/src/viewer/bin.ts +0 -86
- package/src/viewer/cli/health.ts +0 -256
- package/src/viewer/cli/index.ts +0 -33
- package/src/viewer/cli/scan.ts +0 -124
- package/src/viewer/cli/utils.ts +0 -174
- package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
- package/src/viewer/components/ActionCapture.tsx +0 -172
- package/src/viewer/components/ActionsPanel.tsx +0 -332
- package/src/viewer/components/AllVariantsPreview.tsx +0 -78
- package/src/viewer/components/App.tsx +0 -582
- package/src/viewer/components/BottomPanel.tsx +0 -288
- package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
- package/src/viewer/components/CodePanel.tsx +0 -118
- package/src/viewer/components/CommandPalette.tsx +0 -392
- package/src/viewer/components/ComponentDocView.tsx +0 -164
- package/src/viewer/components/ComponentGraph.tsx +0 -380
- package/src/viewer/components/ComponentHeader.tsx +0 -88
- package/src/viewer/components/ContractPanel.tsx +0 -241
- package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
- package/src/viewer/components/ErrorBoundary.tsx +0 -97
- package/src/viewer/components/FigmaEmbed.tsx +0 -238
- package/src/viewer/components/FragmentEditor.tsx +0 -525
- package/src/viewer/components/FragmentRenderer.tsx +0 -61
- package/src/viewer/components/HeaderSearch.tsx +0 -24
- package/src/viewer/components/HealthDashboard.tsx +0 -441
- package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
- package/src/viewer/components/Icons.tsx +0 -479
- package/src/viewer/components/InteractionsPanel.tsx +0 -757
- package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
- package/src/viewer/components/IsolatedRender.tsx +0 -113
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
- package/src/viewer/components/LandingPage.tsx +0 -421
- package/src/viewer/components/Layout.tsx +0 -27
- package/src/viewer/components/LeftSidebar.tsx +0 -472
- package/src/viewer/components/LoadErrorMessage.tsx +0 -102
- package/src/viewer/components/MultiViewportPreview.tsx +0 -522
- package/src/viewer/components/NoVariantsMessage.tsx +0 -59
- package/src/viewer/components/PanelShell.tsx +0 -161
- package/src/viewer/components/PerformancePanel.tsx +0 -304
- package/src/viewer/components/PreviewArea.tsx +0 -472
- package/src/viewer/components/PreviewAside.tsx +0 -168
- package/src/viewer/components/PreviewFrameHost.tsx +0 -303
- package/src/viewer/components/PreviewPane.tsx +0 -149
- package/src/viewer/components/PreviewToolbar.tsx +0 -80
- package/src/viewer/components/PropsEditor.tsx +0 -506
- package/src/viewer/components/PropsTable.tsx +0 -111
- package/src/viewer/components/RelationsSection.tsx +0 -88
- package/src/viewer/components/ResizablePanel.tsx +0 -271
- package/src/viewer/components/RightSidebar.tsx +0 -102
- package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
- package/src/viewer/components/ScreenshotButton.tsx +0 -90
- package/src/viewer/components/Sidebar.tsx +0 -169
- package/src/viewer/components/SkeletonLoader.tsx +0 -161
- package/src/viewer/components/ThemeProvider.tsx +0 -42
- package/src/viewer/components/Toast.tsx +0 -3
- package/src/viewer/components/TokenStylePanel.tsx +0 -699
- package/src/viewer/components/TopToolbar.tsx +0 -159
- package/src/viewer/components/UsageSection.tsx +0 -95
- package/src/viewer/components/VariantMatrix.tsx +0 -388
- package/src/viewer/components/VariantRenderer.tsx +0 -131
- package/src/viewer/components/VariantTabs.tsx +0 -40
- package/src/viewer/components/ViewerHeader.tsx +0 -69
- package/src/viewer/components/ViewerStateSync.tsx +0 -52
- package/src/viewer/components/ViewportSelector.tsx +0 -172
- package/src/viewer/components/WebMCPDevTools.tsx +0 -503
- package/src/viewer/components/WebMCPIntegration.tsx +0 -47
- package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
- package/src/viewer/components/_future/CreatePage.tsx +0 -836
- package/src/viewer/components/viewer-utils.ts +0 -16
- package/src/viewer/composition-renderer.ts +0 -381
- package/src/viewer/constants/index.ts +0 -1
- package/src/viewer/constants/ui.ts +0 -166
- package/src/viewer/entry.tsx +0 -335
- package/src/viewer/hooks/index.ts +0 -2
- package/src/viewer/hooks/useA11yCache.ts +0 -383
- package/src/viewer/hooks/useA11yService.ts +0 -364
- package/src/viewer/hooks/useActions.ts +0 -138
- package/src/viewer/hooks/useAppState.ts +0 -147
- package/src/viewer/hooks/useCompiledFragments.ts +0 -42
- package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
- package/src/viewer/hooks/useHmrStatus.ts +0 -109
- package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
- package/src/viewer/hooks/usePreviewBridge.ts +0 -347
- package/src/viewer/hooks/useScrollSpy.ts +0 -78
- package/src/viewer/hooks/useUrlState.ts +0 -318
- package/src/viewer/hooks/useViewSettings.ts +0 -111
- package/src/viewer/index.html +0 -28
- package/src/viewer/intelligence/healthReport.ts +0 -505
- package/src/viewer/intelligence/styleDrift.ts +0 -340
- package/src/viewer/intelligence/usageScanner.ts +0 -309
- package/src/viewer/jsx-parser.ts +0 -486
- package/src/viewer/preview-frame-entry.tsx +0 -25
- package/src/viewer/preview-frame.html +0 -125
- package/src/viewer/public/favicon.ico +0 -0
- package/src/viewer/render-template.html +0 -68
- package/src/viewer/styles/globals.css +0 -278
- package/src/viewer/types/a11y.ts +0 -197
- package/src/viewer/utils/a11y-fixes.ts +0 -509
- package/src/viewer/utils/actionExport.ts +0 -372
- package/src/viewer/utils/colorSchemes.ts +0 -201
- package/src/viewer/utils/detectRelationships.ts +0 -256
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
- package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
- package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
- package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
- package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
- package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
- package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
- package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
- package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
- package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
- package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
- package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
- package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
- package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
- package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
- package/src/viewer/vendor/shared/src/index.ts +0 -34
- package/src/viewer/vendor/shared/src/types.ts +0 -53
- package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
- package/src/viewer/webmcp/analytics.ts +0 -165
- package/src/viewer/webmcp/index.ts +0 -3
- package/src/viewer/webmcp/posthog-bridge.ts +0 -39
- package/src/viewer/webmcp/runtime-tools.ts +0 -152
- package/src/viewer/webmcp/scan-utils.ts +0 -135
- package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
- package/src/viewer/webmcp/viewer-state.ts +0 -45
- /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
- /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
- /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
- /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
- /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
- /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
- /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
- /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
- /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Combines:
|
|
8
8
|
* - Component discovery (core/discovery.ts)
|
|
9
|
-
* -
|
|
10
|
-
* - JSDoc extraction (new: TypeScript AST)
|
|
11
|
-
* - Compound component detection (new: Object.assign pattern)
|
|
9
|
+
* - ComponentExtractor: persistent LanguageService-based prop + compound extraction
|
|
12
10
|
* - Confidence scoring with TODO markers
|
|
11
|
+
* - Composition-aware code generation
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { readFile, writeFile, access, mkdir } from "node:fs/promises";
|
|
@@ -22,11 +21,12 @@ import {
|
|
|
22
21
|
type DiscoveredComponent,
|
|
23
22
|
} from "../core/node.js";
|
|
24
23
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
|
|
24
|
+
createComponentExtractor,
|
|
25
|
+
type ComponentExtractor,
|
|
26
|
+
type ComponentMeta,
|
|
27
|
+
type PropMeta,
|
|
28
|
+
type CompositionMeta,
|
|
29
|
+
} from "../core/component-extractor.js";
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
32
|
// Types
|
|
@@ -45,6 +45,18 @@ export interface ScanGenerateOptions {
|
|
|
45
45
|
skipStorybook?: boolean;
|
|
46
46
|
/** Verbose logging */
|
|
47
47
|
verbose?: boolean;
|
|
48
|
+
/** Path to tsconfig.json for accurate type resolution */
|
|
49
|
+
tsconfig?: string;
|
|
50
|
+
/** Enable LLM enrichment of knowledge fields */
|
|
51
|
+
enrich?: boolean;
|
|
52
|
+
/** Show what would be enriched without calling API */
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
/** AI provider override: anthropic or openai */
|
|
55
|
+
provider?: 'anthropic' | 'openai';
|
|
56
|
+
/** Explicit API key for enrichment */
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
/** Override default AI model for enrichment */
|
|
59
|
+
model?: string;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export interface ScanGenerateResult {
|
|
@@ -54,17 +66,18 @@ export interface ScanGenerateResult {
|
|
|
54
66
|
path: string;
|
|
55
67
|
confidence: number;
|
|
56
68
|
todoCount: number;
|
|
69
|
+
enriched?: boolean;
|
|
57
70
|
}>;
|
|
58
71
|
skipped: Array<{ name: string; reason: string }>;
|
|
59
72
|
errors: Array<{ name: string; error: string }>;
|
|
60
73
|
averageConfidence: number;
|
|
74
|
+
enrichmentCost?: number;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
interface ComponentData {
|
|
64
78
|
component: DiscoveredComponent;
|
|
65
|
-
props
|
|
66
|
-
|
|
67
|
-
compoundChildren: string[];
|
|
79
|
+
/** Full metadata from ComponentExtractor (props, composition, description) */
|
|
80
|
+
meta: ComponentMeta | null;
|
|
68
81
|
storyVariants: StoryVariant[];
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -78,6 +91,15 @@ interface FieldConfidence {
|
|
|
78
91
|
todoFields: string[];
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
export interface EnrichmentResult {
|
|
95
|
+
when: string[];
|
|
96
|
+
whenNot: string[];
|
|
97
|
+
guidelines: string[];
|
|
98
|
+
a11yRules: string[];
|
|
99
|
+
scenarioTags: string[];
|
|
100
|
+
tags: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
81
103
|
// ---------------------------------------------------------------------------
|
|
82
104
|
// Main orchestrator
|
|
83
105
|
// ---------------------------------------------------------------------------
|
|
@@ -133,33 +155,42 @@ export async function scanGenerate(
|
|
|
133
155
|
|
|
134
156
|
console.log(pc.green(` Found ${components.length} components`));
|
|
135
157
|
|
|
136
|
-
// Phase 2: Extract data for each component
|
|
158
|
+
// Phase 2: Extract data for each component (persistent LanguageService)
|
|
137
159
|
console.log(pc.dim("\nPhase 2: Extracting component metadata..."));
|
|
138
160
|
|
|
161
|
+
// Create a single extractor that reuses its LanguageService across all components
|
|
162
|
+
const extractor = createComponentExtractor(options.tsconfig);
|
|
139
163
|
const componentDataList: ComponentData[] = [];
|
|
140
164
|
|
|
141
|
-
|
|
142
|
-
let propsResult: PropsExtractionResult | null = null;
|
|
143
|
-
try {
|
|
144
|
-
propsResult = await extractPropsFromFile(comp.sourcePath, {
|
|
145
|
-
propsTypeName: `${comp.name}Props`,
|
|
146
|
-
});
|
|
147
|
-
} catch {
|
|
148
|
-
// Props extraction can fail for complex types — continue gracefully
|
|
149
|
-
}
|
|
165
|
+
const extractionStart = performance.now();
|
|
150
166
|
|
|
151
|
-
|
|
167
|
+
for (const comp of components) {
|
|
168
|
+
let meta: ComponentMeta | null = null;
|
|
152
169
|
try {
|
|
153
|
-
|
|
170
|
+
meta = extractor.extract(comp.sourcePath, comp.name);
|
|
154
171
|
} catch {
|
|
155
|
-
//
|
|
172
|
+
// Extraction can fail for complex types — continue gracefully
|
|
156
173
|
}
|
|
157
174
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
// Fallback compound detection for prefix-based patterns (e.g., shadcn-style
|
|
176
|
+
// components that export CardHeader, CardFooter etc. as separate named exports
|
|
177
|
+
// rather than using Object.assign)
|
|
178
|
+
if (meta && !meta.composition) {
|
|
179
|
+
try {
|
|
180
|
+
const subComponents = await detectCompoundComponents(comp.sourcePath, comp.name);
|
|
181
|
+
if (subComponents.length > 0) {
|
|
182
|
+
meta = {
|
|
183
|
+
...meta,
|
|
184
|
+
composition: {
|
|
185
|
+
pattern: 'compound',
|
|
186
|
+
parts: subComponents.map(name => ({ name, props: {} })),
|
|
187
|
+
required: [],
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Non-fatal
|
|
193
|
+
}
|
|
163
194
|
}
|
|
164
195
|
|
|
165
196
|
let storyVariants: StoryVariant[] = [];
|
|
@@ -173,20 +204,53 @@ export async function scanGenerate(
|
|
|
173
204
|
|
|
174
205
|
componentDataList.push({
|
|
175
206
|
component: comp,
|
|
176
|
-
|
|
177
|
-
jsDoc,
|
|
178
|
-
compoundChildren,
|
|
207
|
+
meta,
|
|
179
208
|
storyVariants,
|
|
180
209
|
});
|
|
181
210
|
}
|
|
182
211
|
|
|
212
|
+
extractor.dispose();
|
|
213
|
+
|
|
214
|
+
const extractionMs = (performance.now() - extractionStart).toFixed(0);
|
|
183
215
|
const propsExtracted = componentDataList.filter(
|
|
184
|
-
(d) => d.
|
|
216
|
+
(d) => d.meta && Object.keys(d.meta.props).length > 0
|
|
217
|
+
).length;
|
|
218
|
+
const compoundCount = componentDataList.filter(
|
|
219
|
+
(d) => d.meta?.composition !== null
|
|
185
220
|
).length;
|
|
186
|
-
console.log(pc.green(` Extracted props for ${propsExtracted} components`));
|
|
221
|
+
console.log(pc.green(` Extracted props for ${propsExtracted} components (${extractionMs}ms)`));
|
|
222
|
+
if (compoundCount > 0) {
|
|
223
|
+
console.log(pc.green(` Detected ${compoundCount} compound component(s) with sub-component props`));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Phase 3: Enrich with LLM (if --enrich)
|
|
227
|
+
let enrichments = new Map<string, EnrichmentResult>();
|
|
228
|
+
let enrichmentCost: number | undefined;
|
|
229
|
+
|
|
230
|
+
if (options.enrich) {
|
|
231
|
+
console.log(pc.dim("\nPhase 3: Enriching with AI..."));
|
|
232
|
+
|
|
233
|
+
const enrichResult = await enrichComponents(componentDataList, {
|
|
234
|
+
dryRun: options.dryRun,
|
|
235
|
+
provider: options.provider,
|
|
236
|
+
apiKey: options.apiKey,
|
|
237
|
+
model: options.model,
|
|
238
|
+
});
|
|
187
239
|
|
|
188
|
-
|
|
189
|
-
|
|
240
|
+
enrichments = enrichResult.enrichments;
|
|
241
|
+
|
|
242
|
+
if (enrichResult.model && enrichResult.totalInputTokens > 0) {
|
|
243
|
+
const { calculateCost } = await import('../ai-client.js');
|
|
244
|
+
enrichmentCost = calculateCost(
|
|
245
|
+
enrichResult.model,
|
|
246
|
+
enrichResult.totalInputTokens,
|
|
247
|
+
enrichResult.totalOutputTokens
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Phase 4: Generate fragment files
|
|
253
|
+
console.log(pc.dim(`\nPhase ${options.enrich ? '4' : '3'}: Generating fragment files...`));
|
|
190
254
|
|
|
191
255
|
for (const data of componentDataList) {
|
|
192
256
|
const comp = data.component;
|
|
@@ -225,8 +289,11 @@ export async function scanGenerate(
|
|
|
225
289
|
}
|
|
226
290
|
|
|
227
291
|
try {
|
|
228
|
-
//
|
|
229
|
-
const
|
|
292
|
+
// Look up enrichment for this component
|
|
293
|
+
const enrichment = enrichments.get(comp.name);
|
|
294
|
+
|
|
295
|
+
// Calculate confidence (enrichment boosts score)
|
|
296
|
+
const confidence = calculateFieldConfidence(data, enrichment);
|
|
230
297
|
|
|
231
298
|
// Compute import path
|
|
232
299
|
const importPath = computeImportPath(
|
|
@@ -240,7 +307,8 @@ export async function scanGenerate(
|
|
|
240
307
|
comp.name,
|
|
241
308
|
importPath,
|
|
242
309
|
data,
|
|
243
|
-
confidence
|
|
310
|
+
confidence,
|
|
311
|
+
enrichment
|
|
244
312
|
);
|
|
245
313
|
|
|
246
314
|
await writeFile(fragmentPath, content, "utf-8");
|
|
@@ -251,6 +319,7 @@ export async function scanGenerate(
|
|
|
251
319
|
path: relPath,
|
|
252
320
|
confidence: confidence.score,
|
|
253
321
|
todoCount: confidence.todoFields.length,
|
|
322
|
+
enriched: !!enrichment,
|
|
254
323
|
});
|
|
255
324
|
|
|
256
325
|
const confColor =
|
|
@@ -297,6 +366,14 @@ export async function scanGenerate(
|
|
|
297
366
|
|
|
298
367
|
console.log(pc.dim(` Average confidence: ${avgConfidence}/100`));
|
|
299
368
|
|
|
369
|
+
if (options.enrich) {
|
|
370
|
+
const enrichedCount = generated.filter(g => g.enriched).length;
|
|
371
|
+
console.log(pc.dim(` Enriched: ${enrichedCount}/${generated.length} components`));
|
|
372
|
+
if (enrichmentCost !== undefined && enrichmentCost > 0) {
|
|
373
|
+
console.log(pc.dim(` Estimated cost: $${enrichmentCost.toFixed(4)}`));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
300
377
|
const totalTodos = generated.reduce((sum, g) => sum + g.todoCount, 0);
|
|
301
378
|
if (totalTodos > 0) {
|
|
302
379
|
console.log(
|
|
@@ -313,6 +390,7 @@ export async function scanGenerate(
|
|
|
313
390
|
skipped,
|
|
314
391
|
errors,
|
|
315
392
|
averageConfidence: avgConfidence,
|
|
393
|
+
enrichmentCost,
|
|
316
394
|
};
|
|
317
395
|
}
|
|
318
396
|
|
|
@@ -453,15 +531,17 @@ function getLeadingJSDoc(
|
|
|
453
531
|
* Returns the names of sub-components found.
|
|
454
532
|
*/
|
|
455
533
|
export async function detectCompoundComponents(
|
|
456
|
-
filePath: string
|
|
534
|
+
filePath: string,
|
|
535
|
+
primaryComponentName?: string
|
|
457
536
|
): Promise<string[]> {
|
|
458
537
|
const content = await readFile(filePath, "utf-8");
|
|
459
|
-
return detectCompoundComponentsFromSource(content, filePath);
|
|
538
|
+
return detectCompoundComponentsFromSource(content, filePath, primaryComponentName);
|
|
460
539
|
}
|
|
461
540
|
|
|
462
541
|
export function detectCompoundComponentsFromSource(
|
|
463
542
|
source: string,
|
|
464
|
-
filePath: string
|
|
543
|
+
filePath: string,
|
|
544
|
+
primaryComponentName?: string
|
|
465
545
|
): string[] {
|
|
466
546
|
const sourceFile = ts.createSourceFile(
|
|
467
547
|
filePath,
|
|
@@ -502,6 +582,58 @@ export function detectCompoundComponentsFromSource(
|
|
|
502
582
|
}
|
|
503
583
|
|
|
504
584
|
ts.forEachChild(sourceFile, visit);
|
|
585
|
+
|
|
586
|
+
// Fallback: detect shadcn-style compound exports.
|
|
587
|
+
// Libraries like shadcn/ui export compound sub-components as separate named exports
|
|
588
|
+
// that share a prefix: export { Card, CardHeader, CardContent, CardFooter }
|
|
589
|
+
// Detect these by finding PascalCase exports that start with the primary component name.
|
|
590
|
+
if (subComponents.length === 0 && primaryComponentName) {
|
|
591
|
+
const exportedNames: string[] = [];
|
|
592
|
+
|
|
593
|
+
// Find export { Name1, Name2, ... } blocks
|
|
594
|
+
for (const statement of sourceFile.statements) {
|
|
595
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause) {
|
|
596
|
+
if (ts.isNamedExports(statement.exportClause)) {
|
|
597
|
+
for (const element of statement.exportClause.elements) {
|
|
598
|
+
exportedNames.push(element.name.text);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Also catch `export function CardHeader` / `export const CardHeader`
|
|
603
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
604
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
|
605
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
606
|
+
exportedNames.push(statement.name.text);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (ts.isVariableStatement(statement)) {
|
|
610
|
+
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
|
611
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
612
|
+
for (const decl of statement.declarationList.declarations) {
|
|
613
|
+
if (ts.isIdentifier(decl.name)) {
|
|
614
|
+
exportedNames.push(decl.name.text);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Find exports that start with primaryComponentName but aren't the primary itself
|
|
622
|
+
// e.g., "Card" → ["CardHeader", "CardFooter", "CardContent", "CardTitle"]
|
|
623
|
+
const prefix = primaryComponentName;
|
|
624
|
+
for (const name of exportedNames) {
|
|
625
|
+
if (
|
|
626
|
+
name !== prefix &&
|
|
627
|
+
name.startsWith(prefix) &&
|
|
628
|
+
/^[A-Z]/.test(name.slice(prefix.length))
|
|
629
|
+
) {
|
|
630
|
+
// Strip the prefix to get the sub-component name
|
|
631
|
+
const subName = name.slice(prefix.length);
|
|
632
|
+
subComponents.push(subName);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
505
637
|
return subComponents;
|
|
506
638
|
}
|
|
507
639
|
|
|
@@ -587,33 +719,271 @@ function parseArgsBlock(argsBlock: string): Record<string, unknown> {
|
|
|
587
719
|
return args;
|
|
588
720
|
}
|
|
589
721
|
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Enrichment: LLM-powered knowledge field generation
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
export function buildEnrichmentSystemPrompt(): string {
|
|
727
|
+
return `You are a senior frontend architect and design system expert.
|
|
728
|
+
Given a component's metadata (name, description, props, composition), generate knowledge fields that help AI models use the component correctly.
|
|
729
|
+
|
|
730
|
+
Respond ONLY with a JSON object. No explanation, no markdown outside the JSON.
|
|
731
|
+
|
|
732
|
+
The JSON must have exactly these fields:
|
|
733
|
+
{
|
|
734
|
+
"when": ["..."], // 3-5 scenarios when this component should be used
|
|
735
|
+
"whenNot": ["..."], // 2-4 scenarios when NOT to use this component (suggest alternatives)
|
|
736
|
+
"guidelines": ["..."], // 2-3 usage guidelines or best practices
|
|
737
|
+
"a11yRules": ["..."], // 2-4 accessibility rules or requirements
|
|
738
|
+
"scenarioTags": ["..."], // 3-5 dot-notation scenario tags (e.g., "form.input.text", "layout.container.card")
|
|
739
|
+
"tags": ["..."] // 3-5 search keywords for component discovery
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
Rules:
|
|
743
|
+
- "when" items should describe concrete UI scenarios (e.g., "Displaying grouped content with a header, body, and footer")
|
|
744
|
+
- "whenNot" items should name a better alternative (e.g., "Simple text grouping without visual separation — use a div or Stack instead")
|
|
745
|
+
- "guidelines" should be actionable best practices
|
|
746
|
+
- "a11yRules" should reference WCAG or ARIA patterns where relevant
|
|
747
|
+
- "scenarioTags" use dot notation: category.subcategory.detail
|
|
748
|
+
- "tags" should be lowercase single words or short phrases for search
|
|
749
|
+
- Keep each item to 1 sentence. Be specific, not generic.`;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function buildEnrichmentUserPrompt(
|
|
753
|
+
name: string,
|
|
754
|
+
data: ComponentData
|
|
755
|
+
): string {
|
|
756
|
+
const props = data.meta?.props ?? {};
|
|
757
|
+
const localProps = Object.entries(props).filter(([_, p]) => p.source === 'local');
|
|
758
|
+
const composition = data.meta?.composition ?? null;
|
|
759
|
+
const description = data.meta?.description || '';
|
|
760
|
+
const category = inferCategoryFromMeta(
|
|
761
|
+
name,
|
|
762
|
+
Object.fromEntries(localProps)
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const lines: string[] = [
|
|
766
|
+
`Component: ${name}`,
|
|
767
|
+
`Category: ${category}`,
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
if (description) {
|
|
771
|
+
lines.push(`Description: ${description}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (localProps.length > 0) {
|
|
775
|
+
lines.push('');
|
|
776
|
+
lines.push('Props:');
|
|
777
|
+
for (const [propName, prop] of localProps) {
|
|
778
|
+
let propLine = ` - ${propName}: ${prop.typeKind}`;
|
|
779
|
+
if (prop.values && prop.values.length > 0) {
|
|
780
|
+
propLine += ` (${prop.values.join(' | ')})`;
|
|
781
|
+
}
|
|
782
|
+
if (prop.required) propLine += ' [required]';
|
|
783
|
+
if (prop.description) propLine += ` — ${prop.description}`;
|
|
784
|
+
lines.push(propLine);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (composition && composition.parts.length > 0) {
|
|
789
|
+
lines.push('');
|
|
790
|
+
lines.push(`Composition: ${composition.pattern} pattern`);
|
|
791
|
+
lines.push(`Sub-components: ${composition.parts.map(p => p.name).join(', ')}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (data.storyVariants.length > 0) {
|
|
795
|
+
lines.push('');
|
|
796
|
+
lines.push(`Known variants: ${data.storyVariants.map(v => v.name).join(', ')}`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return lines.join('\n');
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function parseEnrichmentResponse(text: string): EnrichmentResult {
|
|
803
|
+
const empty: EnrichmentResult = {
|
|
804
|
+
when: [],
|
|
805
|
+
whenNot: [],
|
|
806
|
+
guidelines: [],
|
|
807
|
+
a11yRules: [],
|
|
808
|
+
scenarioTags: [],
|
|
809
|
+
tags: [],
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
// Parse JSON from response, handling ```json fences
|
|
814
|
+
const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
|
|
815
|
+
const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[0]) : text;
|
|
816
|
+
const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
817
|
+
|
|
818
|
+
const getArray = (key: string, maxLen: number): string[] => {
|
|
819
|
+
const val = parsed[key];
|
|
820
|
+
if (!Array.isArray(val)) return [];
|
|
821
|
+
return val
|
|
822
|
+
.filter((item): item is string => typeof item === 'string')
|
|
823
|
+
.slice(0, maxLen);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
when: getArray('when', 5),
|
|
828
|
+
whenNot: getArray('whenNot', 4),
|
|
829
|
+
guidelines: getArray('guidelines', 3),
|
|
830
|
+
a11yRules: getArray('a11yRules', 4),
|
|
831
|
+
scenarioTags: getArray('scenarioTags', 5),
|
|
832
|
+
tags: getArray('tags', 5),
|
|
833
|
+
};
|
|
834
|
+
} catch {
|
|
835
|
+
return empty;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function enrichComponents(
|
|
840
|
+
componentDataList: ComponentData[],
|
|
841
|
+
options: Pick<ScanGenerateOptions, 'dryRun' | 'provider' | 'apiKey' | 'model'>
|
|
842
|
+
): Promise<{
|
|
843
|
+
enrichments: Map<string, EnrichmentResult>;
|
|
844
|
+
totalInputTokens: number;
|
|
845
|
+
totalOutputTokens: number;
|
|
846
|
+
model: string;
|
|
847
|
+
}> {
|
|
848
|
+
const enrichments = new Map<string, EnrichmentResult>();
|
|
849
|
+
let totalInputTokens = 0;
|
|
850
|
+
let totalOutputTokens = 0;
|
|
851
|
+
|
|
852
|
+
const {
|
|
853
|
+
detectProvider,
|
|
854
|
+
getApiKey,
|
|
855
|
+
createAIClient,
|
|
856
|
+
generateCompletion,
|
|
857
|
+
ENRICHMENT_MODELS,
|
|
858
|
+
} = await import('../ai-client.js');
|
|
859
|
+
|
|
860
|
+
const provider = detectProvider({ provider: options.provider, apiKey: options.apiKey });
|
|
861
|
+
if (provider === 'none') {
|
|
862
|
+
console.log(pc.yellow(' No API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or use --api-key'));
|
|
863
|
+
console.log(pc.yellow(' Skipping enrichment — fragment files will have TODO markers instead.\n'));
|
|
864
|
+
return { enrichments, totalInputTokens: 0, totalOutputTokens: 0, model: '' };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const apiKey = getApiKey(provider, options.apiKey);
|
|
868
|
+
if (!apiKey) {
|
|
869
|
+
console.log(pc.yellow(' API key not found for provider: ' + provider));
|
|
870
|
+
console.log(pc.yellow(' Skipping enrichment.\n'));
|
|
871
|
+
return { enrichments, totalInputTokens: 0, totalOutputTokens: 0, model: '' };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const model = options.model || ENRICHMENT_MODELS[provider];
|
|
875
|
+
|
|
876
|
+
if (options.dryRun) {
|
|
877
|
+
for (const data of componentDataList) {
|
|
878
|
+
console.log(pc.dim(` Would enrich: ${data.component.name}`));
|
|
879
|
+
}
|
|
880
|
+
return { enrichments, totalInputTokens: 0, totalOutputTokens: 0, model };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const client = await createAIClient(provider, apiKey);
|
|
884
|
+
const systemPrompt = buildEnrichmentSystemPrompt();
|
|
885
|
+
|
|
886
|
+
for (const data of componentDataList) {
|
|
887
|
+
const name = data.component.name;
|
|
888
|
+
process.stdout.write(pc.dim(` Enriching ${name}...`));
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const userPrompt = buildEnrichmentUserPrompt(name, data);
|
|
892
|
+
const result = await generateCompletion(client, provider, model, systemPrompt, userPrompt, 512);
|
|
893
|
+
|
|
894
|
+
totalInputTokens += result.inputTokens;
|
|
895
|
+
totalOutputTokens += result.outputTokens;
|
|
896
|
+
|
|
897
|
+
const enrichment = parseEnrichmentResponse(result.text);
|
|
898
|
+
const fieldCount =
|
|
899
|
+
enrichment.when.length +
|
|
900
|
+
enrichment.whenNot.length +
|
|
901
|
+
enrichment.guidelines.length +
|
|
902
|
+
enrichment.a11yRules.length +
|
|
903
|
+
enrichment.scenarioTags.length +
|
|
904
|
+
enrichment.tags.length;
|
|
905
|
+
|
|
906
|
+
enrichments.set(name, enrichment);
|
|
907
|
+
process.stdout.write(`\r ${pc.green('✓')} ${name} (${fieldCount} fields)\n`);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
910
|
+
process.stdout.write(`\r ${pc.yellow('!')} ${name}: ${msg}\n`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return { enrichments, totalInputTokens, totalOutputTokens, model };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export function buildEnrichedUsageBlock(enrichment: EnrichmentResult): string {
|
|
918
|
+
const lines: string[] = [];
|
|
919
|
+
lines.push(' usage: {');
|
|
920
|
+
|
|
921
|
+
if (enrichment.when.length > 0) {
|
|
922
|
+
lines.push(' when: [');
|
|
923
|
+
for (const item of enrichment.when) {
|
|
924
|
+
lines.push(` '${escapeQuotes(item)}',`);
|
|
925
|
+
}
|
|
926
|
+
lines.push(' ],');
|
|
927
|
+
} else {
|
|
928
|
+
lines.push(' when: [],');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (enrichment.whenNot.length > 0) {
|
|
932
|
+
lines.push(' whenNot: [');
|
|
933
|
+
for (const item of enrichment.whenNot) {
|
|
934
|
+
lines.push(` '${escapeQuotes(item)}',`);
|
|
935
|
+
}
|
|
936
|
+
lines.push(' ],');
|
|
937
|
+
} else {
|
|
938
|
+
lines.push(' whenNot: [],');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (enrichment.guidelines.length > 0) {
|
|
942
|
+
lines.push(' guidelines: [');
|
|
943
|
+
for (const item of enrichment.guidelines) {
|
|
944
|
+
lines.push(` '${escapeQuotes(item)}',`);
|
|
945
|
+
}
|
|
946
|
+
lines.push(' ],');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
lines.push(' },');
|
|
950
|
+
return lines.join('\n');
|
|
951
|
+
}
|
|
952
|
+
|
|
590
953
|
// ---------------------------------------------------------------------------
|
|
591
954
|
// Confidence Scoring
|
|
592
955
|
// ---------------------------------------------------------------------------
|
|
593
956
|
|
|
594
|
-
export function calculateFieldConfidence(
|
|
957
|
+
export function calculateFieldConfidence(
|
|
958
|
+
data: ComponentData,
|
|
959
|
+
enrichment?: EnrichmentResult
|
|
960
|
+
): FieldConfidence {
|
|
595
961
|
let score = 0;
|
|
596
962
|
const todoFields: string[] = [];
|
|
597
963
|
|
|
964
|
+
const props = data.meta?.props ?? {};
|
|
965
|
+
// Use only local props — inherited HTML/React props inflate confidence
|
|
966
|
+
// without adding value to the generated fragment output
|
|
967
|
+
const localEntries = Object.values(props).filter(p => p.source === 'local');
|
|
968
|
+
const hasProps = localEntries.length > 0;
|
|
969
|
+
|
|
598
970
|
// Props extracted: +30
|
|
599
|
-
const hasProps =
|
|
600
|
-
data.props?.success && data.props.props.length > 0;
|
|
601
971
|
if (hasProps) {
|
|
602
972
|
score += 30;
|
|
603
973
|
}
|
|
604
974
|
|
|
605
975
|
// JSDoc description found: +15
|
|
606
|
-
if (data.
|
|
976
|
+
if (data.meta?.description) {
|
|
607
977
|
score += 15;
|
|
608
978
|
} else {
|
|
609
979
|
todoFields.push("meta.description");
|
|
610
980
|
}
|
|
611
981
|
|
|
612
982
|
// Category inferred from path/name (not fallback): +10
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
data.props?.success ? data.props.props : []
|
|
983
|
+
const localProps = Object.fromEntries(
|
|
984
|
+
Object.entries(props).filter(([_, p]) => p.source === 'local')
|
|
616
985
|
);
|
|
986
|
+
const category = inferCategoryFromMeta(data.component.name, localProps);
|
|
617
987
|
if (category !== "Components") {
|
|
618
988
|
score += 10;
|
|
619
989
|
} else {
|
|
@@ -627,32 +997,45 @@ export function calculateFieldConfidence(data: ComponentData): FieldConfidence {
|
|
|
627
997
|
|
|
628
998
|
// All prop types resolved (no "custom"): +10
|
|
629
999
|
if (hasProps) {
|
|
630
|
-
const allResolved =
|
|
631
|
-
(p) => p.propType.type !== "custom"
|
|
632
|
-
);
|
|
1000
|
+
const allResolved = localEntries.every((p) => p.typeKind !== "custom");
|
|
633
1001
|
if (allResolved) {
|
|
634
1002
|
score += 10;
|
|
635
1003
|
}
|
|
636
1004
|
}
|
|
637
1005
|
|
|
638
|
-
// Compound component detected: +
|
|
639
|
-
if (data.
|
|
640
|
-
score +=
|
|
1006
|
+
// Compound component detected with sub-component props: +10
|
|
1007
|
+
if (data.meta?.composition && data.meta.composition.parts.length > 0) {
|
|
1008
|
+
score += 10;
|
|
641
1009
|
}
|
|
642
1010
|
|
|
643
1011
|
// Has default values: +5
|
|
644
1012
|
if (hasProps) {
|
|
645
|
-
const hasDefaults =
|
|
646
|
-
(p) => p.defaultValue !== undefined
|
|
647
|
-
);
|
|
1013
|
+
const hasDefaults = localEntries.some((p) => p.default !== undefined);
|
|
648
1014
|
if (hasDefaults) {
|
|
649
1015
|
score += 5;
|
|
650
1016
|
}
|
|
651
1017
|
}
|
|
652
1018
|
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
1019
|
+
// Enrichment bonuses — if LLM filled these, they're no longer TODOs
|
|
1020
|
+
if (enrichment && enrichment.when.length > 0) {
|
|
1021
|
+
score += 10;
|
|
1022
|
+
} else {
|
|
1023
|
+
todoFields.push("usage.when");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (enrichment && enrichment.whenNot.length > 0) {
|
|
1027
|
+
score += 5;
|
|
1028
|
+
} else {
|
|
1029
|
+
todoFields.push("usage.whenNot");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (enrichment && enrichment.guidelines.length > 0) {
|
|
1033
|
+
score += 5;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (enrichment && enrichment.tags.length > 0) {
|
|
1037
|
+
score += 5;
|
|
1038
|
+
}
|
|
656
1039
|
|
|
657
1040
|
return { score: Math.min(score, 100), todoFields };
|
|
658
1041
|
}
|
|
@@ -691,9 +1074,9 @@ const CATEGORY_PATTERNS: Record<string, string[]> = {
|
|
|
691
1074
|
Media: ["image", "video", "icon", "carousel"],
|
|
692
1075
|
};
|
|
693
1076
|
|
|
694
|
-
function
|
|
1077
|
+
function inferCategoryFromMeta(
|
|
695
1078
|
componentName: string,
|
|
696
|
-
props:
|
|
1079
|
+
props: Record<string, PropMeta>
|
|
697
1080
|
): string {
|
|
698
1081
|
const lower = componentName.toLowerCase();
|
|
699
1082
|
|
|
@@ -706,7 +1089,7 @@ function inferCategory(
|
|
|
706
1089
|
}
|
|
707
1090
|
|
|
708
1091
|
// Prop-based fallbacks
|
|
709
|
-
const propNames = new Set(
|
|
1092
|
+
const propNames = new Set(Object.keys(props));
|
|
710
1093
|
if (propNames.has("onClick") || propNames.has("onPress")) return "Actions";
|
|
711
1094
|
if (propNames.has("value") || propNames.has("defaultValue")) return "Forms";
|
|
712
1095
|
if (propNames.has("children")) return "Layout";
|
|
@@ -728,16 +1111,16 @@ function inferStatus(
|
|
|
728
1111
|
return "stable";
|
|
729
1112
|
}
|
|
730
1113
|
|
|
731
|
-
function
|
|
1114
|
+
function inferDescriptionFromMeta(
|
|
732
1115
|
componentName: string,
|
|
733
|
-
props:
|
|
1116
|
+
props: Record<string, PropMeta>
|
|
734
1117
|
): string {
|
|
735
1118
|
const words = componentName
|
|
736
1119
|
.replace(/([A-Z])/g, " $1")
|
|
737
1120
|
.trim()
|
|
738
1121
|
.toLowerCase();
|
|
739
1122
|
|
|
740
|
-
const propNames = new Set(
|
|
1123
|
+
const propNames = new Set(Object.keys(props));
|
|
741
1124
|
const hasOnClick =
|
|
742
1125
|
propNames.has("onClick") || propNames.has("onPress");
|
|
743
1126
|
const hasValue =
|
|
@@ -751,11 +1134,11 @@ function inferDescription(
|
|
|
751
1134
|
return `${words.charAt(0).toUpperCase() + words.slice(1)} component`;
|
|
752
1135
|
}
|
|
753
1136
|
|
|
754
|
-
function
|
|
1137
|
+
function inferAccessibilityFromMeta(props: Record<string, PropMeta>): {
|
|
755
1138
|
role?: string;
|
|
756
1139
|
requirements?: string[];
|
|
757
1140
|
} {
|
|
758
|
-
const propNames = new Set(
|
|
1141
|
+
const propNames = new Set(Object.keys(props));
|
|
759
1142
|
const accessibility: { role?: string; requirements?: string[] } = {};
|
|
760
1143
|
|
|
761
1144
|
const hasOnClick =
|
|
@@ -780,6 +1163,152 @@ function inferAccessibility(props: ExtractedProp[]): {
|
|
|
780
1163
|
return accessibility;
|
|
781
1164
|
}
|
|
782
1165
|
|
|
1166
|
+
// ---------------------------------------------------------------------------
|
|
1167
|
+
// Contract Block Generation
|
|
1168
|
+
// ---------------------------------------------------------------------------
|
|
1169
|
+
|
|
1170
|
+
interface ContractBlock {
|
|
1171
|
+
propsSummary: string[];
|
|
1172
|
+
compoundChildren?: Record<string, {
|
|
1173
|
+
required?: boolean;
|
|
1174
|
+
accepts?: string[];
|
|
1175
|
+
description?: string;
|
|
1176
|
+
}>;
|
|
1177
|
+
canonicalUsage?: string[];
|
|
1178
|
+
a11yRules?: string[];
|
|
1179
|
+
scenarioTags?: string[];
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function buildContractBlock(
|
|
1183
|
+
componentName: string,
|
|
1184
|
+
props: Record<string, PropMeta>,
|
|
1185
|
+
composition: CompositionMeta | null,
|
|
1186
|
+
accessibility: { role?: string; requirements?: string[] }
|
|
1187
|
+
): ContractBlock | null {
|
|
1188
|
+
const localEntries = Object.entries(props).filter(([_, p]) => p.source === 'local');
|
|
1189
|
+
if (localEntries.length === 0 && !composition && !accessibility.requirements?.length) {
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const contract: ContractBlock = { propsSummary: [] };
|
|
1194
|
+
|
|
1195
|
+
// propsSummary: "propName: value1 | value2 | value3" for enums, "propName: type" otherwise
|
|
1196
|
+
for (const [name, prop] of localEntries) {
|
|
1197
|
+
let summary = name + ': ';
|
|
1198
|
+
if (prop.typeKind === 'enum' && prop.values && prop.values.length > 0) {
|
|
1199
|
+
summary += prop.values.join(' | ');
|
|
1200
|
+
} else {
|
|
1201
|
+
summary += prop.typeKind;
|
|
1202
|
+
}
|
|
1203
|
+
if (prop.required) summary += ' (required)';
|
|
1204
|
+
if (prop.default !== undefined) summary += ` (default: ${prop.default})`;
|
|
1205
|
+
contract.propsSummary.push(summary);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// compoundChildren: built from CompositionMeta.parts[]
|
|
1209
|
+
if (composition && composition.parts.length > 0) {
|
|
1210
|
+
const children: Record<string, { required?: boolean; accepts?: string[]; description?: string }> = {};
|
|
1211
|
+
for (const part of composition.parts) {
|
|
1212
|
+
const childEntry: { required?: boolean; accepts?: string[]; description?: string } = {};
|
|
1213
|
+
if (composition.required.includes(part.name)) {
|
|
1214
|
+
childEntry.required = true;
|
|
1215
|
+
}
|
|
1216
|
+
// Infer accepts from ReactNode-type props on the sub-component
|
|
1217
|
+
const nodeProps = Object.entries(part.props)
|
|
1218
|
+
.filter(([_, p]) => p.typeKind === 'node' || p.typeKind === 'element')
|
|
1219
|
+
.map(([n]) => n);
|
|
1220
|
+
if (nodeProps.length > 0) {
|
|
1221
|
+
childEntry.accepts = nodeProps;
|
|
1222
|
+
}
|
|
1223
|
+
children[part.name] = childEntry;
|
|
1224
|
+
}
|
|
1225
|
+
contract.compoundChildren = children;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// canonicalUsage: generate 1 JSX snippet using actual component + sub-component names
|
|
1229
|
+
if (composition && composition.parts.length > 0) {
|
|
1230
|
+
const innerLines = composition.parts
|
|
1231
|
+
.map(part => ` <${componentName}.${part.name}>...</${componentName}.${part.name}>`)
|
|
1232
|
+
.join('\n');
|
|
1233
|
+
contract.canonicalUsage = [
|
|
1234
|
+
`<${componentName}>\n${innerLines}\n</${componentName}>`,
|
|
1235
|
+
];
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// a11yRules: from accessibility inference
|
|
1239
|
+
const rules: string[] = [];
|
|
1240
|
+
if (accessibility.role) {
|
|
1241
|
+
rules.push(`Role: ${accessibility.role}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (accessibility.requirements) {
|
|
1244
|
+
rules.push(...accessibility.requirements);
|
|
1245
|
+
}
|
|
1246
|
+
if (rules.length > 0) {
|
|
1247
|
+
contract.a11yRules = rules;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return contract;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function emitContractBlock(contract: ContractBlock): string {
|
|
1254
|
+
const lines: string[] = [];
|
|
1255
|
+
lines.push('\n contract: {');
|
|
1256
|
+
|
|
1257
|
+
// propsSummary
|
|
1258
|
+
if (contract.propsSummary.length > 0) {
|
|
1259
|
+
lines.push(' propsSummary: [');
|
|
1260
|
+
for (const s of contract.propsSummary) {
|
|
1261
|
+
lines.push(` '${escapeQuotes(s)}',`);
|
|
1262
|
+
}
|
|
1263
|
+
lines.push(' ],');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// compoundChildren
|
|
1267
|
+
if (contract.compoundChildren && Object.keys(contract.compoundChildren).length > 0) {
|
|
1268
|
+
lines.push(' compoundChildren: {');
|
|
1269
|
+
for (const [name, meta] of Object.entries(contract.compoundChildren)) {
|
|
1270
|
+
const fields: string[] = [];
|
|
1271
|
+
if (meta.required) fields.push(`required: true`);
|
|
1272
|
+
if (meta.accepts && meta.accepts.length > 0) {
|
|
1273
|
+
fields.push(`accepts: [${meta.accepts.map(a => `'${a}'`).join(', ')}]`);
|
|
1274
|
+
}
|
|
1275
|
+
if (meta.description) fields.push(`description: '${escapeQuotes(meta.description)}'`);
|
|
1276
|
+
lines.push(` ${name}: { ${fields.join(', ')} },`);
|
|
1277
|
+
}
|
|
1278
|
+
lines.push(' },');
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// canonicalUsage
|
|
1282
|
+
if (contract.canonicalUsage && contract.canonicalUsage.length > 0) {
|
|
1283
|
+
lines.push(' canonicalUsage: [');
|
|
1284
|
+
for (const usage of contract.canonicalUsage) {
|
|
1285
|
+
lines.push(` \`${usage}\`,`);
|
|
1286
|
+
}
|
|
1287
|
+
lines.push(' ],');
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// a11yRules
|
|
1291
|
+
if (contract.a11yRules && contract.a11yRules.length > 0) {
|
|
1292
|
+
lines.push(' a11yRules: [');
|
|
1293
|
+
for (const rule of contract.a11yRules) {
|
|
1294
|
+
lines.push(` '${escapeQuotes(rule)}',`);
|
|
1295
|
+
}
|
|
1296
|
+
lines.push(' ],');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// scenarioTags
|
|
1300
|
+
if (contract.scenarioTags && contract.scenarioTags.length > 0) {
|
|
1301
|
+
lines.push(' scenarioTags: [');
|
|
1302
|
+
for (const tag of contract.scenarioTags) {
|
|
1303
|
+
lines.push(` '${escapeQuotes(tag)}',`);
|
|
1304
|
+
}
|
|
1305
|
+
lines.push(' ],');
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
lines.push(' },');
|
|
1309
|
+
return lines.join('\n');
|
|
1310
|
+
}
|
|
1311
|
+
|
|
783
1312
|
// ---------------------------------------------------------------------------
|
|
784
1313
|
// Import path computation
|
|
785
1314
|
// ---------------------------------------------------------------------------
|
|
@@ -821,77 +1350,111 @@ function generateFragmentWithTodos(
|
|
|
821
1350
|
componentName: string,
|
|
822
1351
|
importPath: string,
|
|
823
1352
|
data: ComponentData,
|
|
824
|
-
confidence: FieldConfidence
|
|
1353
|
+
confidence: FieldConfidence,
|
|
1354
|
+
enrichment?: EnrichmentResult
|
|
825
1355
|
): string {
|
|
826
|
-
const props = data.
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
//
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
1356
|
+
const props = data.meta?.props ?? {};
|
|
1357
|
+
// Use only local props for inference — inherited HTML/React props cause
|
|
1358
|
+
// wrong descriptions (e.g., Card getting "Form card for user input" from
|
|
1359
|
+
// inherited defaultValue) and wrong roles (e.g., Card getting role: 'button'
|
|
1360
|
+
// from inherited onClick)
|
|
1361
|
+
const localProps = Object.fromEntries(
|
|
1362
|
+
Object.entries(props).filter(([_, p]) => p.source === 'local')
|
|
1363
|
+
);
|
|
1364
|
+
const composition = data.meta?.composition ?? null;
|
|
1365
|
+
const description = data.meta?.description || inferDescriptionFromMeta(componentName, localProps);
|
|
1366
|
+
const descriptionTodo = data.meta?.description ? "" : " // TODO: Review description";
|
|
1367
|
+
const category = inferCategoryFromMeta(componentName, localProps);
|
|
834
1368
|
const categoryTodo = category === "Components" ? " // TODO: Set correct category" : "";
|
|
835
1369
|
const status = inferStatus(data.component.sourcePath);
|
|
836
|
-
const accessibility =
|
|
1370
|
+
const accessibility = inferAccessibilityFromMeta(localProps);
|
|
1371
|
+
|
|
1372
|
+
// Build props block from PropMeta
|
|
1373
|
+
const propsBlock = buildPropsBlockFromMeta(props);
|
|
1374
|
+
|
|
1375
|
+
// Build contract block — enrichment can replace/augment a11yRules and add scenarioTags
|
|
1376
|
+
const contract = buildContractBlock(componentName, props, composition, accessibility);
|
|
1377
|
+
if (contract && enrichment) {
|
|
1378
|
+
if (enrichment.a11yRules.length > 0) {
|
|
1379
|
+
contract.a11yRules = enrichment.a11yRules;
|
|
1380
|
+
}
|
|
1381
|
+
if (enrichment.scenarioTags.length > 0) {
|
|
1382
|
+
contract.scenarioTags = enrichment.scenarioTags;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const contractBlock = contract ? emitContractBlock(contract) : '';
|
|
837
1386
|
|
|
838
|
-
// Build
|
|
839
|
-
|
|
1387
|
+
// Build usage block — enriched or TODO markers
|
|
1388
|
+
let usageBlock: string;
|
|
1389
|
+
if (enrichment && (enrichment.when.length > 0 || enrichment.whenNot.length > 0)) {
|
|
1390
|
+
usageBlock = buildEnrichedUsageBlock(enrichment);
|
|
1391
|
+
} else {
|
|
1392
|
+
usageBlock = ` usage: {
|
|
1393
|
+
when: [
|
|
1394
|
+
// TODO: Describe when to use ${componentName}
|
|
1395
|
+
],
|
|
1396
|
+
whenNot: [
|
|
1397
|
+
// TODO: Describe when NOT to use ${componentName}
|
|
1398
|
+
],
|
|
1399
|
+
},`;
|
|
1400
|
+
}
|
|
840
1401
|
|
|
841
|
-
// Build
|
|
842
|
-
const
|
|
1402
|
+
// Build meta.tags from enrichment
|
|
1403
|
+
const tagsLine = enrichment && enrichment.tags.length > 0
|
|
1404
|
+
? `\n tags: [${enrichment.tags.map(t => `'${escapeQuotes(t)}'`).join(', ')}],`
|
|
1405
|
+
: '';
|
|
843
1406
|
|
|
844
|
-
// Build variants
|
|
1407
|
+
// Build variants (compound-aware)
|
|
845
1408
|
const variantsBlock = buildVariantsBlock(
|
|
846
1409
|
componentName,
|
|
847
|
-
data.storyVariants
|
|
1410
|
+
data.storyVariants,
|
|
1411
|
+
composition
|
|
848
1412
|
);
|
|
849
1413
|
|
|
850
|
-
// Build
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1414
|
+
// Build AI metadata block with composition info
|
|
1415
|
+
const aiBlock = buildAIBlock(composition);
|
|
1416
|
+
|
|
1417
|
+
// Build provenance block
|
|
1418
|
+
const provenanceBlock = buildProvenanceBlock(confidence, props);
|
|
1419
|
+
|
|
1420
|
+
// Build compound sub-components comment
|
|
1421
|
+
const compoundComment = composition && composition.parts.length > 0
|
|
1422
|
+
? `\n// Compound sub-components detected: ${composition.parts.map(p => p.name).join(', ')}`
|
|
1423
|
+
: '';
|
|
855
1424
|
|
|
856
1425
|
return `// Auto-generated by fragments init --scan | Confidence: ${confidence.score}/100
|
|
857
|
-
// ${confidence.todoFields.length} TODO(s) — search for "TODO:" and fill in human knowledge
|
|
1426
|
+
// ${confidence.todoFields.length} TODO(s) — search for "TODO:" and fill in human knowledge${compoundComment}
|
|
858
1427
|
import React from 'react';
|
|
859
1428
|
import { defineFragment } from '@fragments-sdk/core';
|
|
860
1429
|
import { ${componentName} } from '${importPath}';
|
|
861
1430
|
|
|
862
1431
|
export default defineFragment({
|
|
863
1432
|
component: ${componentName},
|
|
864
|
-
|
|
1433
|
+
|
|
865
1434
|
meta: {
|
|
866
1435
|
name: '${escapeQuotes(componentName)}',
|
|
867
1436
|
description: '${escapeQuotes(description)}',${descriptionTodo}
|
|
868
1437
|
category: '${escapeQuotes(category)}',${categoryTodo}
|
|
869
|
-
status: '${status}'
|
|
1438
|
+
status: '${status}',${tagsLine}
|
|
870
1439
|
},
|
|
871
1440
|
|
|
872
|
-
|
|
873
|
-
when: [
|
|
874
|
-
// TODO: Describe when to use ${componentName}
|
|
875
|
-
],
|
|
876
|
-
whenNot: [
|
|
877
|
-
// TODO: Describe when NOT to use ${componentName}
|
|
878
|
-
],
|
|
879
|
-
},
|
|
1441
|
+
${usageBlock}
|
|
880
1442
|
|
|
881
|
-
props: ${propsBlock},${
|
|
1443
|
+
props: ${propsBlock},${contractBlock}${aiBlock}
|
|
882
1444
|
|
|
883
1445
|
variants: [
|
|
884
1446
|
${variantsBlock}
|
|
885
1447
|
],
|
|
886
|
-
});
|
|
1448
|
+
${provenanceBlock}});
|
|
887
1449
|
`;
|
|
888
1450
|
}
|
|
889
1451
|
|
|
890
|
-
function
|
|
891
|
-
|
|
1452
|
+
function buildPropsBlockFromMeta(props: Record<string, PropMeta>): string {
|
|
1453
|
+
const entries = Object.entries(props).filter(([_, p]) => p.source === 'local');
|
|
1454
|
+
if (entries.length === 0) return "{}";
|
|
892
1455
|
|
|
893
|
-
const lines =
|
|
894
|
-
const type = prop.
|
|
1456
|
+
const lines = entries.map(([name, prop]) => {
|
|
1457
|
+
const type = prop.typeKind;
|
|
895
1458
|
const parts: string[] = [` type: '${type}'`];
|
|
896
1459
|
|
|
897
1460
|
if (prop.description) {
|
|
@@ -902,58 +1465,39 @@ function buildPropsBlock(props: ExtractedProp[]): string {
|
|
|
902
1465
|
|
|
903
1466
|
parts.push(` required: ${prop.required}`);
|
|
904
1467
|
|
|
905
|
-
if (prop.
|
|
906
|
-
parts.push(` default: ${JSON.stringify(prop.
|
|
1468
|
+
if (prop.default !== undefined) {
|
|
1469
|
+
parts.push(` default: ${JSON.stringify(prop.default)}`);
|
|
907
1470
|
}
|
|
908
1471
|
|
|
909
|
-
if (prop.
|
|
910
|
-
parts.push(` values: ${JSON.stringify(prop.
|
|
1472
|
+
if (prop.values && prop.values.length > 0) {
|
|
1473
|
+
parts.push(` values: ${JSON.stringify(prop.values)}`);
|
|
911
1474
|
}
|
|
912
1475
|
|
|
913
1476
|
const todoComment =
|
|
914
1477
|
type === "custom" ? " // TODO: Review type" : "";
|
|
915
1478
|
|
|
916
|
-
return ` ${
|
|
1479
|
+
return ` ${name}: {\n${parts.join(",\n")},\n },${todoComment}`;
|
|
917
1480
|
});
|
|
918
1481
|
|
|
919
1482
|
return `{\n${lines.join("\n")}\n }`;
|
|
920
1483
|
}
|
|
921
1484
|
|
|
922
|
-
function buildAccessibilityBlock(accessibility: {
|
|
923
|
-
role?: string;
|
|
924
|
-
requirements?: string[];
|
|
925
|
-
}): string {
|
|
926
|
-
if (
|
|
927
|
-
!accessibility.role &&
|
|
928
|
-
(!accessibility.requirements || accessibility.requirements.length === 0)
|
|
929
|
-
) {
|
|
930
|
-
return "";
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const parts: string[] = [];
|
|
934
|
-
if (accessibility.role) {
|
|
935
|
-
parts.push(` role: '${accessibility.role}'`);
|
|
936
|
-
}
|
|
937
|
-
if (accessibility.requirements && accessibility.requirements.length > 0) {
|
|
938
|
-
const reqs = accessibility.requirements
|
|
939
|
-
.map((r) => `'${escapeQuotes(r)}'`)
|
|
940
|
-
.join(", ");
|
|
941
|
-
parts.push(` requirements: [${reqs}]`);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
return `\n\n accessibility: {\n${parts.join(",\n")},\n },`;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
1485
|
function buildVariantsBlock(
|
|
948
1486
|
componentName: string,
|
|
949
|
-
storyVariants: StoryVariant[]
|
|
1487
|
+
storyVariants: StoryVariant[],
|
|
1488
|
+
composition?: CompositionMeta | null
|
|
950
1489
|
): string {
|
|
951
1490
|
const entries: string[] = [];
|
|
952
1491
|
|
|
953
1492
|
// Always include a Default variant
|
|
954
1493
|
const hasDefault = storyVariants.some((v) => v.name === "Default");
|
|
955
1494
|
if (!hasDefault) {
|
|
956
|
-
|
|
1495
|
+
if (composition && composition.pattern === 'compound' && composition.parts.length > 0) {
|
|
1496
|
+
// Generate a compound-aware default variant
|
|
1497
|
+
entries.push(formatCompoundVariantEntry(componentName, composition));
|
|
1498
|
+
} else {
|
|
1499
|
+
entries.push(formatVariantEntry(componentName, "Default", `Default ${componentName}`, {}));
|
|
1500
|
+
}
|
|
957
1501
|
}
|
|
958
1502
|
|
|
959
1503
|
for (const variant of storyVariants) {
|
|
@@ -986,6 +1530,63 @@ function formatVariantEntry(
|
|
|
986
1530
|
},`;
|
|
987
1531
|
}
|
|
988
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* Generate a compound-aware default variant showing sub-component usage.
|
|
1535
|
+
*/
|
|
1536
|
+
function formatCompoundVariantEntry(
|
|
1537
|
+
componentName: string,
|
|
1538
|
+
composition: CompositionMeta
|
|
1539
|
+
): string {
|
|
1540
|
+
const parts = composition.parts;
|
|
1541
|
+
const innerJsx = parts
|
|
1542
|
+
.map((part) => ` <${componentName}.${part.name}>...</${componentName}.${part.name}>`)
|
|
1543
|
+
.join('\n');
|
|
1544
|
+
|
|
1545
|
+
const jsxCode = `<${componentName}>\n${innerJsx}\n </${componentName}>`;
|
|
1546
|
+
|
|
1547
|
+
return ` {
|
|
1548
|
+
name: 'Default',
|
|
1549
|
+
description: 'Default ${componentName} with sub-components',
|
|
1550
|
+
code: \`${jsxCode}\`,
|
|
1551
|
+
render: () => (\n ${jsxCode}\n ),
|
|
1552
|
+
},`;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Build the ai block with composition metadata.
|
|
1557
|
+
*/
|
|
1558
|
+
function buildAIBlock(composition: CompositionMeta | null): string {
|
|
1559
|
+
if (!composition || composition.parts.length === 0) return "";
|
|
1560
|
+
|
|
1561
|
+
const subComponents = composition.parts.map((p) => `'${p.name}'`).join(', ');
|
|
1562
|
+
const pattern = `\n <Component>\n${composition.parts.map((p) => ` <Component.${p.name}>...</Component.${p.name}>`).join('\n')}\n </Component>`;
|
|
1563
|
+
|
|
1564
|
+
return `
|
|
1565
|
+
|
|
1566
|
+
ai: {
|
|
1567
|
+
compositionPattern: '${composition.pattern}',
|
|
1568
|
+
subComponents: [${subComponents}],
|
|
1569
|
+
commonPatterns: [\`${pattern}\n \`],
|
|
1570
|
+
},`;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Build _generated provenance block.
|
|
1575
|
+
*/
|
|
1576
|
+
function buildProvenanceBlock(
|
|
1577
|
+
confidence: FieldConfidence,
|
|
1578
|
+
props: Record<string, PropMeta>
|
|
1579
|
+
): string {
|
|
1580
|
+
const autoFields = Object.keys(props).filter((name) => props[name].source === 'local');
|
|
1581
|
+
return `
|
|
1582
|
+
_generated: {
|
|
1583
|
+
source: 'ai',
|
|
1584
|
+
confidence: ${(confidence.score / 100).toFixed(2)},
|
|
1585
|
+
timestamp: '${new Date().toISOString()}',
|
|
1586
|
+
},
|
|
1587
|
+
`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
989
1590
|
function buildJsxString(
|
|
990
1591
|
componentName: string,
|
|
991
1592
|
args: Record<string, unknown>
|