@deneb-ui/cli 2.0.49 → 2.0.51

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.
@@ -0,0 +1,450 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Deneb ARC — AI Evaluator & Self-Healing Engine
5
+ *
6
+ * Automatically audits the transformed project before finalizing `deneb init --ai`
7
+ * to guarantee that:
8
+ * 1. React Server Components (RSC) vs Client Component boundaries are strictly respected
9
+ * (preventing "Element type is invalid: expected string/function but got: undefined").
10
+ * 2. Every imported component in page.tsx/layout.tsx is properly exported.
11
+ * 3. Fivora strict leaf contracts are satisfied (no field-path on broad containers).
12
+ * 4. Manifest routes correspond 1:1 with real page files on disk.
13
+ * 5. Uses ChatGPT (OpenAI API) to evaluate, explain, and self-heal any edge cases.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { parseSource, printSource, getJsxName, b } = require('./ast.cjs');
19
+ const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
20
+ const { callOpenAI, checkAiReady, loadEnv } = require('./ai-agent.cjs');
21
+ const { recordEvaluatorFix } = require('./learning.cjs');
22
+
23
+ /**
24
+ * Audit runtime integrity of the transformed project.
25
+ *
26
+ * @param {string} projectDir - Root path of the project being transformed
27
+ * @param {object} profile - Project profile from scanner.cjs
28
+ * @returns {Array<{ type: string, file: string, message: string, severity: 'error' | 'warning', meta?: any }>}
29
+ */
30
+ function auditRuntimeIntegrity(projectDir, profile) {
31
+ const issues = [];
32
+
33
+ // 1. Check RSC Provider Boundaries in layout.tsx
34
+ const layoutFile = profile.appDir ? path.join(projectDir, 'src', 'app', 'layout.tsx') : null;
35
+ const altLayout = profile.appDir ? path.join(projectDir, 'app', 'layout.tsx') : null;
36
+ const targetLayout = (layoutFile && fs.existsSync(layoutFile)) ? layoutFile : ((altLayout && fs.existsSync(altLayout)) ? altLayout : null);
37
+
38
+ if (targetLayout) {
39
+ const layoutCode = fs.readFileSync(targetLayout, 'utf8');
40
+ const isServerComponent = !/['"]use client['"]/.test(layoutCode.slice(0, 300));
41
+
42
+ if (isServerComponent) {
43
+ // Check if client-only SiteDataProvider or createContext is directly rendered in Server Component
44
+ const hasSiteDataImport = /import\s+.*?\bSiteDataProvider\b.*?from\s+['"]@deneb-ui\/(?:ui|core)['"]/.test(layoutCode);
45
+ const rendersSiteDataProvider = /<SiteDataProvider\b/.test(layoutCode);
46
+ const hasProvidersWrapper = /<Providers\b/.test(layoutCode) || /from\s+['"]@\/components\/providers['"]/.test(layoutCode);
47
+
48
+ if (rendersSiteDataProvider && hasProvidersWrapper) {
49
+ issues.push({
50
+ type: 'rsc-duplicate-provider',
51
+ file: path.relative(projectDir, targetLayout).replace(/\\/g, '/'),
52
+ absPath: targetLayout,
53
+ message: 'Server layout renders <SiteDataProvider> inside <Providers>, causing undefined component in React Server Components.',
54
+ severity: 'error',
55
+ meta: { hasProvidersWrapper, hasSiteDataImport },
56
+ });
57
+ } else if (rendersSiteDataProvider && hasSiteDataImport) {
58
+ issues.push({
59
+ type: 'rsc-unwrapped-provider',
60
+ file: path.relative(projectDir, targetLayout).replace(/\\/g, '/'),
61
+ absPath: targetLayout,
62
+ message: 'Server layout renders client-only <SiteDataProvider> without a "use client" boundary.',
63
+ severity: 'error',
64
+ meta: { hasProvidersWrapper, hasSiteDataImport },
65
+ });
66
+ }
67
+ }
68
+
69
+ // Check Global CSS Import in layout.tsx
70
+ const hasCssImport = /import\s+['"][^'"]+\.css['"]/.test(layoutCode);
71
+ if (!hasCssImport) {
72
+ const cssCandidates = [
73
+ path.join(path.dirname(targetLayout), 'globals.css'),
74
+ path.join(path.dirname(targetLayout), 'global.css'),
75
+ path.join(projectDir, 'src', 'app', 'globals.css'),
76
+ path.join(projectDir, 'src', 'styles', 'globals.css'),
77
+ path.join(projectDir, 'styles', 'globals.css'),
78
+ ];
79
+ const foundCss = cssCandidates.find((c) => fs.existsSync(c));
80
+ if (foundCss) {
81
+ const relPath = path.relative(path.dirname(targetLayout), foundCss).replace(/\\/g, '/');
82
+ const relImport = relPath.startsWith('.') ? relPath : `./${relPath}`;
83
+ issues.push({
84
+ type: 'missing-global-css-import',
85
+ file: path.relative(projectDir, targetLayout).replace(/\\/g, '/'),
86
+ absPath: targetLayout,
87
+ message: `Root layout is missing global stylesheet import (${path.basename(foundCss)}), which would cause unstyled HTML.`,
88
+ severity: 'error',
89
+ meta: { cssRel: relImport },
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ // 2. Check Component Export / Import Integrity in page.tsx
96
+ const pageFiles = [
97
+ path.join(projectDir, 'src', 'app', 'page.tsx'),
98
+ path.join(projectDir, 'app', 'page.tsx'),
99
+ path.join(projectDir, 'src', 'pages', 'index.tsx'),
100
+ path.join(projectDir, 'pages', 'index.tsx'),
101
+ ];
102
+ const targetPage = pageFiles.find((p) => fs.existsSync(p));
103
+
104
+ if (targetPage) {
105
+ const pageCode = fs.readFileSync(targetPage, 'utf8');
106
+ const importRegex = /import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
107
+ let match;
108
+
109
+ while ((match = importRegex.exec(pageCode)) !== null) {
110
+ const clause = match[1].trim();
111
+ const specifier = match[2].trim();
112
+
113
+ if (!specifier.startsWith('@/') && !specifier.startsWith('.')) continue;
114
+
115
+ let resolvedFile = null;
116
+ if (specifier.startsWith('@/')) {
117
+ const candidate = path.join(projectDir, 'src', specifier.slice(2));
118
+ for (const ext of ['', '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts']) {
119
+ if (fs.existsSync(candidate + ext) && !fs.statSync(candidate + ext).isDirectory()) {
120
+ resolvedFile = candidate + ext;
121
+ break;
122
+ }
123
+ }
124
+ } else {
125
+ const candidate = path.resolve(path.dirname(targetPage), specifier);
126
+ for (const ext of ['', '.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts']) {
127
+ if (fs.existsSync(candidate + ext) && !fs.statSync(candidate + ext).isDirectory()) {
128
+ resolvedFile = candidate + ext;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+
134
+ if (!resolvedFile || resolvedFile.endsWith('.json') || resolvedFile.endsWith('.css')) continue;
135
+
136
+ const targetSrc = fs.readFileSync(resolvedFile, 'utf8');
137
+
138
+ // Check named imports
139
+ const namedMatch = clause.match(/\{([^}]+)\}/);
140
+ if (namedMatch) {
141
+ const names = namedMatch[1].split(',').map((s) => s.trim().split(/\s+as\s+/)[0]);
142
+ for (const name of names) {
143
+ if (!name || name === 'type') continue;
144
+ const re = new RegExp(`\\bexport\\s+(?:const|function|class|interface|type)\\s+${name}\\b|\\bexport\\s*\\{[^}]*\\b${name}\\b`);
145
+ if (!re.test(targetSrc)) {
146
+ issues.push({
147
+ type: 'missing-named-export',
148
+ file: path.relative(projectDir, resolvedFile).replace(/\\/g, '/'),
149
+ absPath: resolvedFile,
150
+ message: `Component "${name}" imported in ${path.basename(targetPage)} is not exported from ${path.basename(resolvedFile)}.`,
151
+ severity: 'error',
152
+ meta: { componentName: name, importerFile: targetPage },
153
+ });
154
+ }
155
+ }
156
+ }
157
+
158
+ // Check default imports
159
+ const defaultMatch = clause.match(/^([A-Za-z0-9_$]+)(?:\s*,|\s*$)/);
160
+ if (defaultMatch && !clause.includes('{')) {
161
+ const defName = defaultMatch[1];
162
+ if (defName !== 'type' && !/export\s+default\b/.test(targetSrc)) {
163
+ issues.push({
164
+ type: 'missing-default-export',
165
+ file: path.relative(projectDir, resolvedFile).replace(/\\/g, '/'),
166
+ absPath: resolvedFile,
167
+ message: `File ${path.basename(resolvedFile)} lacks a default export for "${defName}".`,
168
+ severity: 'error',
169
+ meta: { componentName: defName, importerFile: targetPage },
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ // 3. Check for broad container markers across all component files
177
+ for (const relFile of profile.jsxFiles || []) {
178
+ const absPath = path.join(projectDir, relFile);
179
+ if (!fs.existsSync(absPath)) continue;
180
+ const src = fs.readFileSync(absPath, 'utf8');
181
+
182
+ const broadRegex = /<(div|section|article|aside|header|footer|nav|main|form|ul|ol|table|thead|tbody|tr)\b([^>]*\bdata-preview-field-path\s*=[^>]*)/gi;
183
+ let bMatch;
184
+ while ((bMatch = broadRegex.exec(src)) !== null) {
185
+ issues.push({
186
+ type: 'broad-container-field-marker',
187
+ file: relFile.replace(/\\/g, '/'),
188
+ absPath,
189
+ message: `data-preview-field-path cannot be placed on broad <${bMatch[1]}> in ${relFile}.`,
190
+ severity: 'error',
191
+ meta: { tag: bMatch[1], snippet: bMatch[0].slice(0, 80) },
192
+ });
193
+ }
194
+ }
195
+
196
+ // 4. Check manifest routes vs disk
197
+ const manifestPath = path.join(projectDir, 'fivora-template.json');
198
+ if (fs.existsSync(manifestPath)) {
199
+ try {
200
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
201
+ for (const p of manifest.pages || []) {
202
+ if (!p || !p.route) continue;
203
+ if (p.route === '/') continue;
204
+
205
+ const cleanRoute = p.route.replace(/^\/+/, '');
206
+ const exists =
207
+ fs.existsSync(path.join(projectDir, 'src', 'app', cleanRoute, 'page.tsx')) ||
208
+ fs.existsSync(path.join(projectDir, 'src', 'app', cleanRoute, 'page.jsx')) ||
209
+ fs.existsSync(path.join(projectDir, 'app', cleanRoute, 'page.tsx')) ||
210
+ fs.existsSync(path.join(projectDir, 'app', cleanRoute, 'page.jsx')) ||
211
+ fs.existsSync(path.join(projectDir, 'src', 'pages', `${cleanRoute}.tsx`)) ||
212
+ fs.existsSync(path.join(projectDir, 'src', 'pages', `${cleanRoute}.jsx`)) ||
213
+ fs.existsSync(path.join(projectDir, 'pages', `${cleanRoute}.tsx`)) ||
214
+ fs.existsSync(path.join(projectDir, 'pages', `${cleanRoute}.jsx`));
215
+
216
+ if (!exists) {
217
+ issues.push({
218
+ type: 'orphan-manifest-route',
219
+ file: 'fivora-template.json',
220
+ absPath: manifestPath,
221
+ message: `Manifest page "${p.id}" route "${p.route}" has no page file on disk.`,
222
+ severity: 'error',
223
+ meta: { pageId: p.id, route: p.route },
224
+ });
225
+ }
226
+ }
227
+ } catch {
228
+ // ignore JSON parse error
229
+ }
230
+ }
231
+
232
+ return issues;
233
+ }
234
+
235
+ /**
236
+ * Heals detected runtime integrity issues automatically.
237
+ * Applies deterministic AST fixes for known patterns, and uses ChatGPT API for complex self-healing.
238
+ *
239
+ * @param {string} projectDir
240
+ * @param {object} profile
241
+ * @param {Array} issues
242
+ * @param {object} options
243
+ * @returns {Promise<{ healedCount: number, remainingCount: number, log: string[] }>}
244
+ */
245
+ async function healRuntimeIntegrity(projectDir, profile, issues, options = {}) {
246
+ let healedCount = 0;
247
+ const log = [];
248
+
249
+ for (const issue of issues) {
250
+ // Healing Pattern 1: RSC Duplicate or Server-rendered SiteDataProvider in layout.tsx
251
+ if (issue.type === 'rsc-duplicate-provider' && issue.absPath && fs.existsSync(issue.absPath)) {
252
+ try {
253
+ let code = fs.readFileSync(issue.absPath, 'utf8');
254
+
255
+ // If <Providers> exists, unwrap the redundant <SiteDataProvider> from layout.tsx
256
+ code = code.replace(
257
+ /<SiteDataProvider\s+initialSiteData=\{[^}]+\}>([\s\S]*?)<\/SiteDataProvider>/g,
258
+ '$1'
259
+ );
260
+ // Remove unused SiteDataProvider and initialSiteData imports
261
+ code = code.replace(/import\s+.*?\bSiteDataProvider\b.*?from\s+['"]@deneb-ui\/(?:ui|core)['"];?\n?/g, '');
262
+ code = code.replace(/import\s+initialSiteData\s+from\s+['"][^'"]+['"];?\n?/g, '');
263
+
264
+ fs.writeFileSync(issue.absPath, code, 'utf8');
265
+ healedCount++;
266
+ recordEvaluatorFix({
267
+ projectDir,
268
+ issueType: issue.type,
269
+ file: issue.file,
270
+ action: 'remove-redundant-server-site-data-provider',
271
+ success: true,
272
+ });
273
+ log.push(`Healed RSC boundary: removed redundant <SiteDataProvider> from server component ${issue.file}`);
274
+ } catch (err) {
275
+ log.push(`Failed to heal ${issue.file}: ${err.message}`);
276
+ }
277
+ continue;
278
+ }
279
+
280
+ // Healing Pattern 1b: Missing global CSS stylesheet import in layout.tsx
281
+ if (issue.type === 'missing-global-css-import' && issue.absPath && fs.existsSync(issue.absPath)) {
282
+ try {
283
+ let code = fs.readFileSync(issue.absPath, 'utf8');
284
+ const cssImport = `import "${issue.meta?.cssRel || './globals.css'}";\n`;
285
+ const firstImportIdx = code.indexOf('import ');
286
+ if (firstImportIdx !== -1) {
287
+ const nextLineIdx = code.indexOf('\n', firstImportIdx);
288
+ code = code.slice(0, nextLineIdx + 1) + cssImport + code.slice(nextLineIdx + 1);
289
+ } else {
290
+ code = cssImport + code;
291
+ }
292
+ fs.writeFileSync(issue.absPath, code, 'utf8');
293
+ healedCount++;
294
+ recordEvaluatorFix({
295
+ projectDir,
296
+ issueType: issue.type,
297
+ file: issue.file,
298
+ action: 'restore-global-css-import',
299
+ success: true,
300
+ });
301
+ log.push(`Healed layout styles: restored ${issue.meta?.cssRel || './globals.css'} import in ${issue.file}`);
302
+ } catch (err) {
303
+ log.push(`Failed to heal global CSS import in ${issue.file}: ${err.message}`);
304
+ }
305
+ continue;
306
+ }
307
+
308
+ // Healing Pattern 2: Broad container field path
309
+ if (issue.type === 'broad-container-field-marker' && issue.absPath && fs.existsSync(issue.absPath)) {
310
+ try {
311
+ let code = fs.readFileSync(issue.absPath, 'utf8');
312
+ // Replace <div ... data-preview-field-path="..."> with <span className="block..." ...>
313
+ code = code.replace(
314
+ /<div\b([^>]*\bdata-preview-field-path\s*=[^>]*)>([\s\S]*?)<\/div>/gi,
315
+ (match, attrs, inner) => {
316
+ let nextAttrs = attrs;
317
+ if (/className\s*=\s*['"]/.test(nextAttrs)) {
318
+ nextAttrs = nextAttrs.replace(/className\s*=\s*(['"])/, 'className=$1block ');
319
+ } else {
320
+ nextAttrs = ` className="block"${nextAttrs}`;
321
+ }
322
+ return `<span${nextAttrs}>${inner}</span>`;
323
+ }
324
+ );
325
+ fs.writeFileSync(issue.absPath, code, 'utf8');
326
+ healedCount++;
327
+ recordEvaluatorFix({
328
+ projectDir,
329
+ issueType: issue.type,
330
+ file: issue.file,
331
+ action: 'convert-broad-container-to-inline-span',
332
+ success: true,
333
+ });
334
+ log.push(`Healed broad container: converted <${issue.meta?.tag || 'div'}> to inline-block <span data-preview-field-path> in ${issue.file}`);
335
+ } catch (err) {
336
+ log.push(`Failed to heal broad container in ${issue.file}: ${err.message}`);
337
+ }
338
+ continue;
339
+ }
340
+
341
+ // Healing Pattern 3: Orphan manifest route
342
+ if (issue.type === 'orphan-manifest-route' && issue.absPath && fs.existsSync(issue.absPath)) {
343
+ try {
344
+ const manifest = JSON.parse(fs.readFileSync(issue.absPath, 'utf8'));
345
+ if (Array.isArray(manifest.pages)) {
346
+ manifest.pages = manifest.pages.filter((p) => p.id !== issue.meta?.pageId && p.route !== issue.meta?.route);
347
+ fs.writeFileSync(issue.absPath, JSON.stringify(manifest, null, 2), 'utf8');
348
+ healedCount++;
349
+ recordEvaluatorFix({
350
+ projectDir,
351
+ issueType: issue.type,
352
+ file: issue.file,
353
+ action: 'prune-orphan-manifest-route',
354
+ success: true,
355
+ });
356
+ log.push(`Healed manifest: pruned non-existent route "${issue.meta?.route}" (${issue.meta?.pageId})`);
357
+ }
358
+ } catch (err) {
359
+ log.push(`Failed to heal manifest route: ${err.message}`);
360
+ }
361
+ continue;
362
+ }
363
+
364
+ // Healing Pattern 4: AI-Assisted Self-Healing for missing exports or complex issues
365
+ if (issue.type.startsWith('missing-') && options.aiEnabled && issue.absPath && fs.existsSync(issue.absPath)) {
366
+ loadEnv(projectDir);
367
+ const aiReady = checkAiReady();
368
+ if (aiReady.ready) {
369
+ try {
370
+ const fileSrc = fs.readFileSync(issue.absPath, 'utf8');
371
+ const prompt = `You are a senior React/TypeScript engineer on the DENEB UI team.
372
+ Fix this export error in the file:
373
+ ERROR: ${issue.message}
374
+
375
+ FILE SOURCE:
376
+ \`\`\`tsx
377
+ ${fileSrc}
378
+ \`\`\`
379
+
380
+ INSTRUCTIONS:
381
+ 1. Ensure the component "${issue.meta?.componentName}" is properly exported with a named export \`export function ${issue.meta?.componentName}()\` or proper export syntax.
382
+ 2. Preserve all existing JSX, props, logic, and styling.
383
+ 3. Output ONLY the raw TypeScript (.tsx) code. No markdown fences, no explanations.`;
384
+
385
+ const aiResult = await callOpenAI(prompt);
386
+ if (aiResult && aiResult.content) {
387
+ fs.writeFileSync(issue.absPath, aiResult.content, 'utf8');
388
+ healedCount++;
389
+ recordEvaluatorFix({
390
+ projectDir,
391
+ issueType: issue.type,
392
+ file: issue.file,
393
+ action: 'ai-generate-missing-export',
394
+ success: true,
395
+ });
396
+ log.push(`AI healed export in ${issue.file}: added export for ${issue.meta?.componentName}`);
397
+ }
398
+ } catch (err) {
399
+ log.push(`AI heal failed for ${issue.file}: ${err.message}`);
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ // Re-audit after healing to calculate remaining issues
406
+ const remaining = auditRuntimeIntegrity(projectDir, profile);
407
+ return {
408
+ healedCount,
409
+ remainingCount: remaining.length,
410
+ remainingIssues: remaining,
411
+ log,
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Complete evaluation pipeline called by `init --ai`.
417
+ *
418
+ * @param {string} projectDir - Project directory
419
+ * @param {object} profile - Project profile
420
+ * @param {object} [options] - Options (aiEnabled, etc.)
421
+ * @returns {Promise<{ passed: boolean, issuesFound: number, healed: number, log: string[] }>}
422
+ */
423
+ async function runAiEvaluatorPipeline(projectDir, profile, options = {}) {
424
+ const initialIssues = auditRuntimeIntegrity(projectDir, profile);
425
+
426
+ if (initialIssues.length === 0) {
427
+ return {
428
+ passed: true,
429
+ issuesFound: 0,
430
+ healed: 0,
431
+ log: ['All runtime integrity checks passed cleanly.'],
432
+ };
433
+ }
434
+
435
+ const healResult = await healRuntimeIntegrity(projectDir, profile, initialIssues, options);
436
+
437
+ return {
438
+ passed: healResult.remainingCount === 0,
439
+ issuesFound: initialIssues.length,
440
+ healed: healResult.healedCount,
441
+ remainingIssues: healResult.remainingIssues,
442
+ log: healResult.log,
443
+ };
444
+ }
445
+
446
+ module.exports = {
447
+ auditRuntimeIntegrity,
448
+ healRuntimeIntegrity,
449
+ runAiEvaluatorPipeline,
450
+ };
@@ -124,6 +124,7 @@ Your task is to create an editable wrapper component for an existing component.
124
124
  10. **Use \`'use strict'\` is NOT needed** — this is a .tsx file.
125
125
  11. **Do NOT import React hooks** like useState, useEffect, useContext. The component must be stateless.
126
126
  12. **Do NOT import useSiteData** — the wrapper component does not need it directly.
127
+ 13. **NEVER place data-preview-field-path on <div>, <section>, <article>, or broad containers**. ` + '`data-preview-field-path`' + ` must ONLY be on leaf text/media/control elements (<span>, <p>, <h1>-<h6>, <a>, <button>, EditableText, EditableImage).
127
128
 
128
129
  ## REFERENCE PATTERN (follow this structure exactly):
129
130
 
package/src/arc/ast.cjs CHANGED
@@ -20,9 +20,17 @@ const BABEL_PLUGIN_SETS = [
20
20
  ['jsx'],
21
21
  ];
22
22
 
23
- function parseWithBabel(code) {
23
+ function parseWithBabel(code, filePath = 'file.tsx') {
24
+ const isTs =
25
+ /\.(tsx|ts|mts|cts)$/i.test(filePath) ||
26
+ /(?:interface\s+[A-Za-z0-9_$]+|type\s+[A-Za-z0-9_$]+\s*=|:\s*(?:string|number|boolean|any|void|unknown|React\.)|as\s+[A-Za-z0-9_$]+)/.test(code);
27
+
28
+ const pluginSets = isTs
29
+ ? BABEL_PLUGIN_SETS.filter((set) => set.includes('typescript'))
30
+ : BABEL_PLUGIN_SETS;
31
+
24
32
  let lastError = null;
25
- for (const plugins of BABEL_PLUGIN_SETS) {
33
+ for (const plugins of pluginSets) {
26
34
  try {
27
35
  return babelParser.parse(code, {
28
36
  sourceType: 'unambiguous',
@@ -52,7 +60,7 @@ function parseSource(code, filePath = 'file.tsx') {
52
60
  ...options,
53
61
  parser: {
54
62
  parse(source) {
55
- return parseWithBabel(source);
63
+ return parseWithBabel(source, filePath);
56
64
  },
57
65
  },
58
66
  });
@@ -47,6 +47,7 @@ function inferSection(context) {
47
47
  ['testimonials', /testimonial/],
48
48
  ['map', /map\b|location-map|google-map/],
49
49
  ['faq', /faq|accordion/],
50
+ ['form', /form|booking|inquiry|registration/],
50
51
  ['contact', /contact|whatsapp|mailto/],
51
52
  ['featuredProducts', /featured|product-grid|collection/],
52
53
  ['newsletter', /newsletter|subscribe/],
@@ -65,6 +66,7 @@ function inferSection(context) {
65
66
 
66
67
  function inferFieldName(kind, tag, text, extra = {}) {
67
68
  if (kind === 'url') {
69
+ if (extra.action === 'form-submit') return 'formWhatsappUrl';
68
70
  if (extra.platform) return `${extra.platform}Url`;
69
71
  if (extra.action === 'whatsapp') return 'whatsappUrl';
70
72
  if (extra.action === 'phone') return 'phoneUrl';
@@ -74,6 +76,7 @@ function inferFieldName(kind, tag, text, extra = {}) {
74
76
  return fromText || (extra.action ? `${extra.action}Url` : 'ctaUrl');
75
77
  }
76
78
  if (kind === 'label' && extra.paired) {
79
+ if (extra.action === 'form-submit') return 'formSubmitLabel';
77
80
  if (extra.action === 'whatsapp') return 'whatsappLabel';
78
81
  if (extra.action === 'phone') return 'phoneLabel';
79
82
  if (extra.action === 'email') return 'emailLabel';
@@ -100,6 +103,7 @@ function inferFieldName(kind, tag, text, extra = {}) {
100
103
  Description: 'description',
101
104
  Typography: 'text',
102
105
  Badge: 'badge',
106
+ label: 'label',
103
107
  button: 'label',
104
108
  Button: 'label',
105
109
  span: 'label',
package/src/arc/index.cjs CHANGED
@@ -39,6 +39,7 @@ const printer = require('./printer.cjs');
39
39
  const { classifyComponents } = require('./component-registry.cjs');
40
40
  const { checkAiReady, adaptComponent, generateDocsPage, loadEnv } = require('./ai-agent.cjs');
41
41
  const { checkGithubReady, createComponentPR } = require('./pr-agent.cjs');
42
+ const { runAiEvaluatorPipeline } = require('./ai-evaluator.cjs');
42
43
 
43
44
  function parseArcOptions(raw = {}) {
44
45
  return {
@@ -255,7 +256,7 @@ async function runDenebArcAsync(projectDir, projectName, options = {}) {
255
256
  0
256
257
  );
257
258
  const actionCount = analyses.reduce(
258
- (n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
259
+ (n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.operation === 'form-submit-action' || c.kind === 'url').length,
259
260
  0
260
261
  );
261
262
  printer.printScan(profile, graph, candidateCount, actionCount);
@@ -341,7 +342,33 @@ async function runDenebArcAsync(projectDir, projectName, options = {}) {
341
342
  }
342
343
  }
343
344
 
344
- return runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt);
345
+ const result = runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt);
346
+
347
+ if (!opts.dryRun && result && result.outcome === 'success') {
348
+ printer.printAiEvaluatorStart();
349
+ try {
350
+ const evalResult = await runAiEvaluatorPipeline(projectDir, profile, {
351
+ dryRun: opts.aiDryRun,
352
+ aiEnabled: opts.aiEnabled,
353
+ });
354
+ if (evalResult.healed > 0) {
355
+ for (const logItem of evalResult.log) {
356
+ printer.printAiEvaluatorHealed(logItem);
357
+ }
358
+ }
359
+ if (evalResult.remainingIssues && evalResult.remainingIssues.length > 0) {
360
+ for (const issue of evalResult.remainingIssues) {
361
+ printer.printAiEvaluatorIssue(`${issue.file}: ${issue.message}`);
362
+ }
363
+ } else {
364
+ printer.printAiEvaluatorPass('All runtime integrity checks passed (RSC boundaries, exports, Fivora contracts).');
365
+ }
366
+ } catch (err) {
367
+ printer.warn(`AI Evaluator audit encountered an issue: ${err.message}`);
368
+ }
369
+ }
370
+
371
+ return result;
345
372
  }
346
373
 
347
374
  function runDenebArcSync(projectDir, projectName, options = {}) {
@@ -370,7 +397,7 @@ function runDenebArcSync(projectDir, projectName, options = {}) {
370
397
  0
371
398
  );
372
399
  const actionCount = analyses.reduce(
373
- (n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
400
+ (n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.operation === 'form-submit-action' || c.kind === 'url').length,
374
401
  0
375
402
  );
376
403
  printer.printScan(profile, graph, candidateCount, actionCount);
@@ -118,6 +118,7 @@ function mapOperation(operation) {
118
118
  case 'extract-url':
119
119
  return 'url-extraction';
120
120
  case 'split-action-contract':
121
+ case 'form-submit-action':
121
122
  return 'contract-split';
122
123
  case 'style-bind':
123
124
  return 'style-bind';
@@ -199,8 +200,38 @@ function redactSecrets(value) {
199
200
  .replace(/-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]*?-----END [A-Z ]+PRIVATE KEY-----/g, '***REDACTED KEY***');
200
201
  }
201
202
 
203
+ function recordEvaluatorFix({ projectDir, issueType, file, action, success = true }) {
204
+ const record = {
205
+ engineVersion: ARC_VERSION,
206
+ type: 'evaluator-fix',
207
+ timestamp: new Date().toISOString(),
208
+ issueType,
209
+ file,
210
+ action,
211
+ success: Boolean(success),
212
+ };
213
+ try {
214
+ if (projectDir) {
215
+ fs.mkdirSync(path.dirname(experiencePath(projectDir)), { recursive: true });
216
+ const local = loadJsonArray(experiencePath(projectDir));
217
+ writeJson(experiencePath(projectDir), [...local, record].slice(-400));
218
+ }
219
+ } catch {
220
+ // ignore
221
+ }
222
+ try {
223
+ fs.mkdirSync(path.dirname(localStorePath()), { recursive: true });
224
+ const global = loadJsonArray(localStorePath());
225
+ writeJson(localStorePath(), [...global, record].slice(-800));
226
+ } catch {
227
+ // ignore
228
+ }
229
+ return record;
230
+ }
231
+
202
232
  module.exports = {
203
233
  recordExperience,
234
+ recordEvaluatorFix,
204
235
  loadFingerprintBoost,
205
236
  registryArchitecture,
206
237
  redactSecrets,