@fragments-sdk/cli 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +419 -410
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
  5. package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
  6. package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
  7. package/dist/chunk-DXX6HADE.js +443 -0
  8. package/dist/chunk-DXX6HADE.js.map +1 -0
  9. package/dist/chunk-EYXVAMEX.js +626 -0
  10. package/dist/chunk-EYXVAMEX.js.map +1 -0
  11. package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
  12. package/dist/chunk-FO6EBJWP.js.map +1 -0
  13. package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
  14. package/dist/chunk-QM7SVOGF.js.map +1 -0
  15. package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
  16. package/dist/chunk-RF3C6LGA.js.map +1 -0
  17. package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
  18. package/dist/chunk-SM674YAS.js.map +1 -0
  19. package/dist/chunk-SXTKFDCR.js +104 -0
  20. package/dist/chunk-SXTKFDCR.js.map +1 -0
  21. package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
  22. package/dist/core/index.js +13 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-XK6PRUE5.js +636 -0
  28. package/dist/init-XK6PRUE5.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
  31. package/dist/scan-generate-U3RFVDTX.js +1115 -0
  32. package/dist/scan-generate-U3RFVDTX.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
  34. package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
  35. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
  36. package/dist/static-viewer-KKCR4KXR.js.map +1 -0
  37. package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
  38. package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
  39. package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
  40. package/dist/viewer-M2EQQSGE.js.map +1 -0
  41. package/package.json +11 -9
  42. package/src/ai-client.ts +156 -0
  43. package/src/bin.ts +99 -2
  44. package/src/build.ts +95 -33
  45. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  46. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  47. package/src/commands/enhance.ts +11 -35
  48. package/src/commands/govern.ts +122 -0
  49. package/src/commands/init.ts +288 -260
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/sync.ts +357 -0
  54. package/src/commands/validate.ts +43 -1
  55. package/src/core/component-extractor.test.ts +282 -0
  56. package/src/core/component-extractor.ts +1030 -0
  57. package/src/core/discovery.ts +93 -7
  58. package/src/service/enhance/props-extractor.ts +235 -13
  59. package/src/validators.ts +236 -0
  60. package/src/viewer/vite-plugin.ts +1 -1
  61. package/dist/chunk-5G3VZH43.js.map +0 -1
  62. package/dist/chunk-OQO55NKV.js.map +0 -1
  63. package/dist/chunk-WXSR2II7.js.map +0 -1
  64. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  65. package/dist/init-UFGK5TCN.js +0 -867
  66. package/dist/init-UFGK5TCN.js.map +0 -1
  67. package/dist/scan-generate-SJAN5MVI.js +0 -691
  68. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  69. package/dist/viewer-DLLJIMCK.js.map +0 -1
  70. package/src/ai.ts +0 -266
  71. package/src/commands/init-framework.ts +0 -414
  72. package/src/mcp/bin.ts +0 -36
  73. package/src/migrate/bin.ts +0 -114
  74. package/src/theme/index.ts +0 -77
  75. package/src/viewer/bin.ts +0 -86
  76. package/src/viewer/cli/health.ts +0 -256
  77. package/src/viewer/cli/index.ts +0 -33
  78. package/src/viewer/cli/scan.ts +0 -124
  79. package/src/viewer/cli/utils.ts +0 -174
  80. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  81. /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
  82. /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
  83. /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
  84. /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
  85. /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
  86. /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
  87. /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
  88. /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
  89. /package/dist/{tokens-CE46OTMD.js.map → tokens-L46MK5AW.js.map} +0 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Shared AI client infrastructure for LLM-powered CLI commands.
3
+ *
4
+ * Used by:
5
+ * - `fragments enhance` (usage analysis + documentation generation)
6
+ * - `fragments init --scan --enrich` (knowledge field enrichment)
7
+ *
8
+ * Supports Anthropic (Claude) and OpenAI APIs via dynamic import.
9
+ * No new dependencies — uses existing @anthropic-ai/sdk and openai.
10
+ */
11
+
12
+ export type AIProvider = 'anthropic' | 'openai' | 'none';
13
+
14
+ /**
15
+ * Default lightweight models for enrichment (cheap, fast).
16
+ * Enhance uses its own heavier models locally.
17
+ */
18
+ export const ENRICHMENT_MODELS: Record<AIProvider, string> = {
19
+ anthropic: 'claude-haiku-4-5-20251001',
20
+ openai: 'gpt-4o-mini',
21
+ none: '',
22
+ };
23
+
24
+ /**
25
+ * Detect which AI provider to use based on available API keys and options.
26
+ */
27
+ export function detectProvider(opts?: {
28
+ provider?: AIProvider;
29
+ apiKey?: string;
30
+ }): AIProvider {
31
+ if (opts?.provider) return opts.provider;
32
+ if (opts?.apiKey) {
33
+ if (opts.apiKey.startsWith('sk-ant-')) return 'anthropic';
34
+ if (opts.apiKey.startsWith('sk-')) return 'openai';
35
+ }
36
+ if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
37
+ if (process.env.OPENAI_API_KEY) return 'openai';
38
+ return 'none';
39
+ }
40
+
41
+ /**
42
+ * Resolve the API key for a given provider.
43
+ */
44
+ export function getApiKey(provider: AIProvider, explicitKey?: string): string | undefined {
45
+ if (explicitKey) return explicitKey;
46
+ if (provider === 'anthropic') return process.env.ANTHROPIC_API_KEY;
47
+ if (provider === 'openai') return process.env.OPENAI_API_KEY;
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * Create an AI client for the given provider via dynamic import.
53
+ */
54
+ export async function createAIClient(provider: AIProvider, apiKey: string): Promise<unknown> {
55
+ if (provider === 'anthropic') {
56
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
57
+ return new Anthropic({ apiKey });
58
+ }
59
+ if (provider === 'openai') {
60
+ const OpenAI = (await import('openai')).default;
61
+ return new OpenAI({ apiKey });
62
+ }
63
+ throw new Error(`Unknown provider: ${provider}`);
64
+ }
65
+
66
+ export interface CompletionResult {
67
+ text: string;
68
+ inputTokens: number;
69
+ outputTokens: number;
70
+ }
71
+
72
+ /**
73
+ * Generate a completion using the appropriate provider API.
74
+ */
75
+ export async function generateCompletion(
76
+ client: unknown,
77
+ provider: AIProvider,
78
+ model: string,
79
+ system: string,
80
+ user: string,
81
+ maxTokens: number = 1024
82
+ ): Promise<CompletionResult> {
83
+ if (provider === 'anthropic') {
84
+ const anthropic = client as import('@anthropic-ai/sdk').default;
85
+ const response = await anthropic.messages.create({
86
+ model,
87
+ max_tokens: maxTokens,
88
+ system,
89
+ messages: [{ role: 'user', content: user }],
90
+ });
91
+
92
+ const content = response.content[0];
93
+ if (content.type !== 'text') {
94
+ throw new Error('Unexpected response type');
95
+ }
96
+
97
+ return {
98
+ text: content.text,
99
+ inputTokens: response.usage?.input_tokens || 0,
100
+ outputTokens: response.usage?.output_tokens || 0,
101
+ };
102
+ }
103
+
104
+ if (provider === 'openai') {
105
+ const openai = client as import('openai').default;
106
+ const response = await openai.chat.completions.create({
107
+ model,
108
+ max_tokens: maxTokens,
109
+ messages: [
110
+ { role: 'system', content: system },
111
+ { role: 'user', content: user },
112
+ ],
113
+ });
114
+
115
+ const content = response.choices[0]?.message?.content;
116
+ if (!content) {
117
+ throw new Error('No response from OpenAI');
118
+ }
119
+
120
+ return {
121
+ text: content,
122
+ inputTokens: response.usage?.prompt_tokens || 0,
123
+ outputTokens: response.usage?.completion_tokens || 0,
124
+ };
125
+ }
126
+
127
+ throw new Error(`Unknown provider: ${provider}`);
128
+ }
129
+
130
+ /**
131
+ * Parse a JSON response from an LLM, handling ```json fences and raw JSON.
132
+ */
133
+ export function parseJSONResponse<T = unknown>(text: string): T {
134
+ const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
135
+ const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[0]) : text;
136
+ return JSON.parse(jsonStr);
137
+ }
138
+
139
+ /**
140
+ * Calculate estimated cost based on model and token usage.
141
+ */
142
+ export function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
143
+ // Approximate costs per 1M tokens (input/output)
144
+ const pricing: Record<string, { input: number; output: number }> = {
145
+ // Anthropic
146
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00 },
147
+ 'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
148
+ // OpenAI
149
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
150
+ 'gpt-4o': { input: 2.50, output: 10.00 },
151
+ };
152
+
153
+ const modelPricing = pricing[model] || { input: 3.00, output: 15.00 };
154
+ return (inputTokens / 1_000_000) * modelPricing.input +
155
+ (outputTokens / 1_000_000) * modelPricing.output;
156
+ }
package/src/bin.ts CHANGED
@@ -39,6 +39,8 @@ import { graph } from './commands/graph.js';
39
39
  import { perf } from './commands/perf.js';
40
40
  import { doctor } from './commands/doctor.js';
41
41
  import { setup } from './commands/setup.js';
42
+ import { sync } from './commands/sync.js';
43
+ import { governCheck, governInit, governReport } from './commands/govern.js';
42
44
 
43
45
  // Import existing commands that were already extracted
44
46
  import { runScreenshotCommand } from './screenshot.js';
@@ -62,6 +64,8 @@ program
62
64
  .option('--schema', 'Validate fragment schema only')
63
65
  .option('--coverage', 'Validate coverage only')
64
66
  .option('--snippets', 'Validate snippet/render policy only')
67
+ .option('--drift', 'Detect metadata drift between source and fragments')
68
+ .option('--tsconfig <path>', 'Path to tsconfig.json (for drift detection)')
65
69
  .option('--snippet-mode <mode>', 'Override snippet policy mode (warn|error)')
66
70
  .option('--component-start <name>', 'Start component name for alphabetical snippet batch validation')
67
71
  .option('--component-limit <n>', 'Component count for alphabetical snippet batch validation', (value) => Number.parseInt(value, 10))
@@ -77,6 +81,33 @@ program
77
81
  }
78
82
  });
79
83
 
84
+ // ============================================================================
85
+ // SYNC COMMAND
86
+ // ============================================================================
87
+ program
88
+ .command('sync')
89
+ .description('Auto-update fragment files from component source')
90
+ .option('-c, --config <path>', 'Path to config file')
91
+ .option('--tsconfig <path>', 'Path to tsconfig.json')
92
+ .option('--dry-run', 'Preview changes without writing')
93
+ .option('--component <name>', 'Sync specific component only')
94
+ .action(async (options) => {
95
+ try {
96
+ const result = await sync({
97
+ config: options.config,
98
+ tsconfig: options.tsconfig,
99
+ dryRun: options.dryRun,
100
+ component: options.component,
101
+ });
102
+ if (!result.success) {
103
+ process.exit(1);
104
+ }
105
+ } catch (error) {
106
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
107
+ process.exit(1);
108
+ }
109
+ });
110
+
80
111
  // ============================================================================
81
112
  // BUILD COMMAND
82
113
  // ============================================================================
@@ -794,10 +825,16 @@ program
794
825
  // ============================================================================
795
826
  program
796
827
  .command('init')
797
- .description('Initialize fragments in a project (interactive by default)')
828
+ .description('Initialize fragments in a project (zero-config by default)')
798
829
  .option('--force', 'Overwrite existing config')
799
- .option('-y, --yes', 'Non-interactive mode - auto-detect and use defaults')
830
+ .option('-y, --yes', 'Non-interactive mode (now the default)')
831
+ .option('--configure', 'Interactive mode for theme seeds, snapshots, etc.')
800
832
  .option('--scan <path>', 'Scan a TypeScript component directory and generate fragment files')
833
+ .option('--enrich', 'Use AI to fill knowledge fields during --scan (requires API key)')
834
+ .option('--dry-run', 'Show what --enrich would generate without calling API')
835
+ .option('--provider <provider>', 'AI provider for enrichment: anthropic or openai')
836
+ .option('--api-key <key>', 'API key for AI enrichment')
837
+ .option('--model <model>', 'Override AI model for enrichment')
801
838
  .action(async (options) => {
802
839
  try {
803
840
  const { init } = await import('./commands/init.js');
@@ -806,6 +843,12 @@ program
806
843
  force: options.force,
807
844
  yes: options.scan ? true : options.yes,
808
845
  scan: options.scan,
846
+ configure: options.configure,
847
+ enrich: options.enrich,
848
+ dryRun: options.dryRun,
849
+ provider: options.provider,
850
+ apiKey: options.apiKey,
851
+ model: options.model,
809
852
  });
810
853
 
811
854
  if (!result.success) {
@@ -1073,5 +1116,59 @@ program
1073
1116
  }
1074
1117
  });
1075
1118
 
1119
+ // ============================================================================
1120
+ // GOVERN COMMAND
1121
+ // ============================================================================
1122
+ const governCmd = program
1123
+ .command('govern')
1124
+ .description('AI UI governance checks');
1125
+
1126
+ governCmd
1127
+ .command('check')
1128
+ .description('Validate a UISpec against governance policies')
1129
+ .option('-i, --input <path>', 'Path to UISpec JSON file (or - for stdin)')
1130
+ .option('-c, --config <path>', 'Path to govern.config.ts')
1131
+ .option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
1132
+ .option('-q, --quiet', 'Suppress non-error output')
1133
+ .action(async (options) => {
1134
+ try {
1135
+ const { exitCode } = await governCheck({
1136
+ input: options.input,
1137
+ config: options.config,
1138
+ format: options.format,
1139
+ quiet: options.quiet,
1140
+ });
1141
+ process.exit(exitCode);
1142
+ } catch (error) {
1143
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1144
+ process.exit(1);
1145
+ }
1146
+ });
1147
+
1148
+ governCmd
1149
+ .command('init')
1150
+ .description('Generate a govern.config.ts template')
1151
+ .option('-o, --output <path>', 'Output path', 'govern.config.ts')
1152
+ .action(async (options) => {
1153
+ try {
1154
+ await governInit({ output: options.output });
1155
+ } catch (error) {
1156
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1157
+ process.exit(1);
1158
+ }
1159
+ });
1160
+
1161
+ governCmd
1162
+ .command('report')
1163
+ .description('Summarize governance audit log')
1164
+ .action(async () => {
1165
+ try {
1166
+ await governReport();
1167
+ } catch (error) {
1168
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1169
+ process.exit(1);
1170
+ }
1171
+ });
1172
+
1076
1173
  // Parse command line arguments
1077
1174
  program.parse();
package/src/build.ts CHANGED
@@ -21,10 +21,13 @@ import {
21
21
  generateContextMd,
22
22
  } from "./core/node.js";
23
23
  import {
24
- extractCustomPropsFromComponentFile,
25
24
  resolveComponentSourcePath,
26
- type AutoDetectedPropDefinition,
27
25
  } from "./core/auto-props.js";
26
+ import {
27
+ createComponentExtractor,
28
+ type PropMeta,
29
+ type ComponentMeta,
30
+ } from "./core/component-extractor.js";
28
31
  import { buildComponentGraph } from "./core/graph-extractor.js";
29
32
  import { serializeGraph } from "@fragments-sdk/context/graph";
30
33
  import { resolvePerformanceConfig } from "./core/index.js";
@@ -53,17 +56,21 @@ function normalizeParsedProps(
53
56
 
54
57
  function mergeDocumentedAndAutoProps(
55
58
  documentedProps: Record<string, CompiledProp>,
56
- autoProps: Record<string, AutoDetectedPropDefinition>
59
+ autoProps: Record<string, PropMeta>
57
60
  ): Record<string, CompiledProp> {
58
61
  return Object.fromEntries(
59
- Object.keys(autoProps).map((name) => {
62
+ Object.keys(autoProps)
63
+ // Strip inherited HTML/React props — they're identical across all components
64
+ // and bloat fragments.json. MCP consumers know these exist implicitly.
65
+ .filter((name) => autoProps[name].source === 'local' || name in documentedProps)
66
+ .map((name) => {
60
67
  const documented = documentedProps[name];
61
68
  const auto = autoProps[name];
62
69
 
63
70
  return [
64
71
  name,
65
72
  {
66
- type: auto.type,
73
+ type: auto.typeKind,
67
74
  description: documented?.description ?? auto.description ?? "",
68
75
  default: auto.default !== undefined ? auto.default : documented?.default,
69
76
  required: auto.required,
@@ -75,6 +82,30 @@ function mergeDocumentedAndAutoProps(
75
82
  );
76
83
  }
77
84
 
85
+ /**
86
+ * Auto-compile a propsSummary for the contract from extracted props.
87
+ * Format: "variant: primary|secondary|ghost (required)"
88
+ */
89
+ function compilePropsSummary(props: Record<string, PropMeta>): string[] {
90
+ return Object.entries(props)
91
+ .filter(([_, p]) => p.source === 'local')
92
+ .map(([name, prop]) => {
93
+ let summary = name + ': ';
94
+ if (prop.values && prop.values.length > 0) {
95
+ summary += prop.values.join('|');
96
+ } else {
97
+ summary += prop.typeKind;
98
+ }
99
+ if (prop.default !== undefined) {
100
+ summary += ` (default: ${prop.default})`;
101
+ }
102
+ if (prop.required) {
103
+ summary += ' (required)';
104
+ }
105
+ return summary;
106
+ });
107
+ }
108
+
78
109
  export interface BuildResult {
79
110
  success: boolean;
80
111
  outputPath: string;
@@ -98,6 +129,15 @@ export async function buildFragments(
98
129
  const warnings: Array<{ file: string; warning: string }> = [];
99
130
  const fragments: CompiledFragmentsFile["fragments"] = {};
100
131
 
132
+ // Create a persistent extractor — shared LanguageService across all fragments
133
+ // Try to find a tsconfig.json in the config directory
134
+ const tsconfigCandidates = [
135
+ resolve(configDir, 'tsconfig.json'),
136
+ resolve(configDir, '..', 'tsconfig.json'),
137
+ ];
138
+ const tsconfigPath = tsconfigCandidates.find((p) => existsSync(p));
139
+ const extractor = createComponentExtractor(tsconfigPath);
140
+
101
141
  for (const file of files) {
102
142
  try {
103
143
  // Read file content as text
@@ -139,38 +179,38 @@ export async function buildFragments(
139
179
  parsed.componentImport
140
180
  );
141
181
 
182
+ // Extract full component metadata using persistent LanguageService
183
+ let extractedMeta: ComponentMeta | null = null;
142
184
  if (componentExportName && componentSourcePath) {
143
- const autoPropsResult = extractCustomPropsFromComponentFile(
144
- componentSourcePath,
145
- componentExportName
146
- );
147
-
148
- for (const warning of autoPropsResult.warnings) {
149
- warnings.push({ file: file.relativePath, warning });
185
+ try {
186
+ extractedMeta = extractor.extract(componentSourcePath, componentExportName);
187
+ } catch {
188
+ // Extraction failure is non-fatal — fall back to documented props
150
189
  }
151
190
 
152
- const hasAutoProps = Object.keys(autoPropsResult.props).length > 0;
153
- if (autoPropsResult.resolved && hasAutoProps) {
154
- const removedDocumentedProps = Object.keys(documentedProps).filter(
155
- (propName) => !(propName in autoPropsResult.props)
156
- );
191
+ if (extractedMeta) {
192
+ const autoProps = extractedMeta.props;
193
+ const hasAutoProps = Object.keys(autoProps).length > 0;
157
194
 
158
- if (removedDocumentedProps.length > 0) {
195
+ if (hasAutoProps) {
196
+ const removedDocumentedProps = Object.keys(documentedProps).filter(
197
+ (propName) => !(propName in autoProps)
198
+ );
199
+
200
+ if (removedDocumentedProps.length > 0) {
201
+ warnings.push({
202
+ file: file.relativePath,
203
+ warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
204
+ });
205
+ }
206
+
207
+ mergedProps = mergeDocumentedAndAutoProps(documentedProps, autoProps);
208
+ } else if (Object.keys(documentedProps).length > 0) {
159
209
  warnings.push({
160
210
  file: file.relativePath,
161
- warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
211
+ warning: "Auto-props extraction returned no props; falling back to documented props",
162
212
  });
163
213
  }
164
-
165
- mergedProps = mergeDocumentedAndAutoProps(
166
- documentedProps,
167
- autoPropsResult.props
168
- );
169
- } else if (autoPropsResult.resolved && !hasAutoProps && Object.keys(documentedProps).length > 0) {
170
- warnings.push({
171
- file: file.relativePath,
172
- warning: "Auto-props extraction returned no custom props; falling back to documented props",
173
- });
174
214
  }
175
215
  } else if (!componentExportName) {
176
216
  warnings.push({
@@ -184,6 +224,26 @@ export async function buildFragments(
184
224
  });
185
225
  }
186
226
 
227
+ // Auto-compile contract if not manually authored
228
+ let contract = parsed.contract;
229
+ if (!contract?.propsSummary && extractedMeta) {
230
+ const summary = compilePropsSummary(extractedMeta.props);
231
+ if (summary.length > 0) {
232
+ contract = { ...contract, propsSummary: summary };
233
+ }
234
+ }
235
+
236
+ // Auto-enrich AI metadata from extractor's composition data
237
+ let ai = parsed.ai;
238
+ if (extractedMeta?.composition) {
239
+ const comp = extractedMeta.composition;
240
+ ai = {
241
+ compositionPattern: comp.pattern,
242
+ subComponents: comp.parts.map((p) => p.name),
243
+ ...ai, // Manually authored ai fields take precedence
244
+ };
245
+ }
246
+
187
247
  // Build compiled fragment from parsed metadata
188
248
  const compiled: CompiledFragment = {
189
249
  filePath: file.relativePath,
@@ -221,10 +281,10 @@ export async function buildFragments(
221
281
  ...(v.figma && { figma: v.figma }),
222
282
  ...(v.args && { args: v.args }),
223
283
  })),
224
- // Include AI metadata if present
225
- ...(parsed.ai && { ai: parsed.ai }),
226
- // Include contract metadata if present
227
- ...(parsed.contract && { contract: parsed.contract }),
284
+ // Include AI metadata (auto-enriched or manual)
285
+ ...(ai && { ai }),
286
+ // Include contract metadata (auto-compiled or manual)
287
+ ...(contract && { contract }),
228
288
  };
229
289
 
230
290
  fragments[parsed.meta.name] = compiled;
@@ -236,6 +296,8 @@ export async function buildFragments(
236
296
  }
237
297
  }
238
298
 
299
+ extractor.dispose();
300
+
239
301
  // Discover and compile block files
240
302
  const blocks: Record<string, CompiledBlock> = {};
241
303
  try {