@deneb-ui/cli 2.0.91 → 2.0.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.91",
3
+ "version": "2.0.93",
4
4
  "description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
51
  "@babel/parser": "^7.28.0",
52
- "@deneb-ui/core": "^2.0.91",
52
+ "@deneb-ui/core": "^2.0.93",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -21,6 +21,44 @@ const { classifyActionIntent } = require('../adapters.cjs');
21
21
  const { validateGeneratedCode } = require('../ai-agent.cjs');
22
22
  const { auditRuntimeIntegrity, healRuntimeIntegrity, runAiEvaluatorPipeline } = require('../ai-evaluator.cjs');
23
23
 
24
+ test('DENEB preflight rejects a Next router destination with a duplicated base path before install', () => {
25
+ const starter = path.resolve(
26
+ __dirname,
27
+ '../../../../../packages/create-template/template'
28
+ );
29
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-navigation-preflight-'));
30
+ fs.cpSync(starter, tempDir, { recursive: true });
31
+ const componentDir = path.join(tempDir, 'src', 'components');
32
+ fs.mkdirSync(componentDir, { recursive: true });
33
+ fs.writeFileSync(
34
+ path.join(componentDir, 'BrokenNavigation.tsx'),
35
+ "router.push(withBasePath(pageRoute('about')));\n"
36
+ );
37
+
38
+ try {
39
+ const validator = path.resolve(
40
+ __dirname,
41
+ '../../tools/deneb-template-validator.cjs'
42
+ );
43
+ const result = require('node:child_process').spawnSync(
44
+ process.execPath,
45
+ [validator, 'validate', tempDir, '--skip-install', '--skip-build'],
46
+ { encoding: 'utf8' }
47
+ );
48
+ const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
49
+
50
+ assert.equal(result.status, 1);
51
+ assert.match(
52
+ output,
53
+ /Template navigation validation failed before dependency installation\./
54
+ );
55
+ assert.match(output, /src\/components\/BrokenNavigation\.tsx:1/);
56
+ assert.doesNotMatch(output, /Install dependencies/);
57
+ } finally {
58
+ fs.rmSync(tempDir, { recursive: true, force: true });
59
+ }
60
+ });
61
+
24
62
  test('field-paths recognizes list action CTA keys', () => {
25
63
  assert.equal(isListActionCtaKey('preOrderCta'), true);
26
64
  assert.equal(isListActionCtaKey('addToTrayCta'), true);
@@ -3885,4 +3923,3 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
3885
3923
 
3886
3924
 
3887
3925
 
3888
-
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { parseSource, printSource } = require('../ast.cjs');
6
+ const { replaceTextChildren } = require('../transforms/primitives/text.cjs');
7
+ const { splitActionChildren } = require('../transforms/primitives/action.cjs');
8
+
9
+ test('composite-fragments: preserves dynamic runtime expression and wraps literal text in editable span', () => {
10
+ const code = '<h1>Shop {category.name} Collection</h1>';
11
+ const ast = parseSource(code, 'heading.tsx');
12
+ const h1Node = ast.program.body[0].expression;
13
+
14
+ replaceTextChildren(h1Node, 'shop.heading', 'Shop', 'text');
15
+ const result = printSource(ast, code);
16
+
17
+ // Dynamic expression must be preserved
18
+ assert.match(result, /\{category\.name\}/);
19
+ // Literal text must be wrapped in editable span with field-path
20
+ assert.match(result, /data-preview-field-path="shop\.heading"/);
21
+ });
22
+
23
+ test('composite-fragments: replaces pure static text directly without extra spans', () => {
24
+ const code = '<h1>Featured Products</h1>';
25
+ const ast = parseSource(code, 'heading.tsx');
26
+ const h1Node = ast.program.body[0].expression;
27
+
28
+ replaceTextChildren(h1Node, 'home.featuredTitle', 'Featured Products', 'text');
29
+ const result = printSource(ast, code);
30
+
31
+ assert.match(result, /<h1>\{siteData\?\.content\?\.home\?\.featuredTitle \?\? "Featured Products"\}<\/h1>/);
32
+ });
33
+
34
+ test('composite-fragments: splitActionChildren wraps only text and leaves icon untouched', () => {
35
+ const code = '<button><Icon className="w-4 h-4" /> Add to Cart</button>';
36
+ const ast = parseSource(code, 'button.tsx');
37
+ const btnNode = ast.program.body[0].expression;
38
+
39
+ splitActionChildren(btnNode, 'product.addToCartLabel', 'Add to Cart');
40
+ const result = printSource(ast, code);
41
+
42
+ // Icon must remain untouched
43
+ assert.match(result, /<Icon className="w-4 h-4" \/>/);
44
+ // Outer button must NOT have data-preview-field-path
45
+ assert.doesNotMatch(result, /<button[^>]*data-preview-field-path/);
46
+ // Inner text must be in span
47
+ assert.match(result, /<span[\s\S]*?data-preview-field-path="product\.addToCartLabel"/);
48
+ });
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { ACCEPTANCE_GATES, evaluateAcceptanceGates } = require('../acceptance-gates.cjs');
6
+
7
+ test('fifteen-gates: evaluates all 15 gates and passes when all critical pass', () => {
8
+ const allGateKeys = Object.keys(ACCEPTANCE_GATES);
9
+ assert.equal(allGateKeys.length, 15, 'Must have exactly 15 acceptance gates');
10
+
11
+ const evalResult = evaluateAcceptanceGates({
12
+ sourceAnalysis: { passed: true },
13
+ astTransformation: { passed: true },
14
+ typescriptValidation: { passed: true },
15
+ buildSuccess: { passed: true },
16
+ fivoraAudit: { passed: true },
17
+ manifestValid: true,
18
+ runtimeEditabilityScore: 100,
19
+ collectionOpsPassed: true,
20
+ imageEditingPassed: true,
21
+ routeCoveragePassed: true,
22
+ designPreservationScore: 100,
23
+ controlOnlyValid: true,
24
+ assetIntegrityPassed: true,
25
+ rscIntegrityPassed: true,
26
+ consoleCleanlinessPassed: true,
27
+ editabilityCoverage: 100,
28
+ });
29
+
30
+ assert.equal(evalResult.passed, true);
31
+ assert.equal(evalResult.status, 'CONVERSION_PASSED');
32
+ assert.equal(Object.keys(evalResult.gates).length, 15);
33
+ });
34
+
35
+ test('fifteen-gates: fails conversion if asset integrity or RSC integrity fails', () => {
36
+ const failedAsset = evaluateAcceptanceGates({
37
+ assetIntegrityPassed: false,
38
+ });
39
+ assert.equal(failedAsset.passed, false);
40
+ assert.equal(failedAsset.status, 'CONVERSION_FAILED');
41
+
42
+ const failedRsc = evaluateAcceptanceGates({
43
+ rscIntegrityPassed: false,
44
+ });
45
+ assert.equal(failedRsc.passed, false);
46
+ assert.equal(failedRsc.status, 'CONVERSION_FAILED');
47
+ });
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const {
6
+ PLATFORM_ALLOWED_TYPES,
7
+ normalizeFieldType,
8
+ CanonicalPathRegistry,
9
+ sanitizeEditorSections,
10
+ } = require('../fivora-schema-authority.cjs');
11
+
12
+ test('fivora-schema-authority: coerces unsupported types to platform whitelist', () => {
13
+ assert.equal(normalizeFieldType('currency'), 'text');
14
+ assert.equal(normalizeFieldType('price'), 'text');
15
+ assert.equal(normalizeFieldType('richText'), 'textarea');
16
+ assert.equal(normalizeFieldType('phone'), 'tel');
17
+ assert.equal(normalizeFieldType('link'), 'url');
18
+ assert.equal(normalizeFieldType('unknown_custom_type'), 'text');
19
+
20
+ // Allowed types remain untouched
21
+ for (const t of PLATFORM_ALLOWED_TYPES) {
22
+ assert.equal(normalizeFieldType(t), t);
23
+ }
24
+ });
25
+
26
+ test('fivora-schema-authority: CanonicalPathRegistry tracks unique paths and detects duplicates', () => {
27
+ const reg = new CanonicalPathRegistry();
28
+ const res1 = reg.register('site.announcement.linkUrl', 'site_announcement', 'url');
29
+ assert.equal(res1.registered, true);
30
+ assert.equal(res1.isDuplicate, false);
31
+
32
+ const res2 = reg.register('site.announcement.linkUrl', 'site', 'url');
33
+ assert.equal(res2.registered, false);
34
+ assert.equal(res2.isDuplicate, true);
35
+ assert.equal(res2.owner, 'site_announcement');
36
+ });
37
+
38
+ test('fivora-schema-authority: sanitizeEditorSections strips duplicate paths and fixes unsupported types', () => {
39
+ const sections = [
40
+ {
41
+ id: 'site_announcement',
42
+ path: 'site.announcement',
43
+ type: 'object',
44
+ fields: [
45
+ { key: 'linkUrl', type: 'url', label: 'Announcement Link' },
46
+ { key: 'priceTag', type: 'currency', label: 'Discount Tag' },
47
+ ],
48
+ },
49
+ {
50
+ id: 'site',
51
+ path: 'site',
52
+ type: 'object',
53
+ fields: [
54
+ { key: 'announcement.linkUrl', type: 'url', label: 'Duplicate Announcement Link' },
55
+ { key: 'brandTitle', type: 'text', label: 'Brand Title' },
56
+ ],
57
+ },
58
+ ];
59
+
60
+ const sanitized = sanitizeEditorSections(sections);
61
+ assert.equal(sanitized.length, 2);
62
+
63
+ // Section 1: priceTag coerced from currency to text
64
+ const priceField = sanitized[0].fields.find((f) => f.key === 'priceTag');
65
+ assert.equal(priceField.type, 'text');
66
+
67
+ // Section 2: duplicate announcement.linkUrl removed
68
+ const dupField = sanitized[1].fields.find((f) => f.key === 'announcement.linkUrl');
69
+ assert.equal(dupField, undefined, 'Duplicate field path must be eliminated');
70
+
71
+ // Non-duplicate brandTitle remains
72
+ const brandField = sanitized[1].fields.find((f) => f.key === 'brandTitle');
73
+ assert.ok(brandField);
74
+ assert.equal(brandField.type, 'text');
75
+ });
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { recoverCandidateConfidence } = require('../confidence-recovery.cjs');
6
+ const { buildEditabilityInventory } = require('../editability-inventory.cjs');
7
+
8
+ test('confidence-recovery: elevates low-confidence heading candidate from 0.45 to >= 0.65', () => {
9
+ const candidate = {
10
+ tag: 'h2',
11
+ componentName: 'FeatureTitle',
12
+ className: 'text-3xl font-bold tracking-tight text-white',
13
+ value: 'Engineered for Performance and Comfort',
14
+ confidence: 0.45,
15
+ };
16
+
17
+ const rec = recoverCandidateConfidence(candidate);
18
+ assert.equal(rec.recovered, true);
19
+ assert.ok(rec.confidence >= 0.65, `Expected >= 0.65, got ${rec.confidence}`);
20
+ assert.ok(rec.reasons.includes('semantic-heading-tag'));
21
+ assert.ok(rec.reasons.includes('tailwind-typography-token'));
22
+ });
23
+
24
+ test('confidence-recovery: elevates hero promotional badge from 0.40 to >= 0.60', () => {
25
+ const candidate = {
26
+ tag: 'span',
27
+ componentName: 'PromoBadge',
28
+ parentName: 'HeroSection',
29
+ className: 'text-sm font-semibold uppercase',
30
+ value: 'Limited Time Offer',
31
+ confidence: 0.40,
32
+ };
33
+
34
+ const rec = recoverCandidateConfidence(candidate);
35
+ assert.equal(rec.recovered, true);
36
+ assert.ok(rec.confidence >= 0.60, `Expected >= 0.60, got ${rec.confidence}`);
37
+ assert.ok(rec.reasons.includes('semantic-component-name'));
38
+ });
39
+
40
+ test('editability-inventory: builds accurate inventory and accounts for 100% of discovered candidates', () => {
41
+ const candidates = [
42
+ { loc: '1:1', tag: 'h1', kind: 'text', dataClassification: 'CONTENT_STATIC' },
43
+ { loc: '2:1', tag: 'p', kind: 'text', dataClassification: 'CONTENT_STATIC' },
44
+ { loc: '3:1', tag: 'img', kind: 'image', dataClassification: 'CONTENT_STATIC' },
45
+ { loc: '4:1', tag: 'span', kind: 'text', dataClassification: 'PLATFORM_CONTROLLED' },
46
+ { loc: '5:1', tag: 'span', kind: 'text', dataClassification: 'RUNTIME_COMPUTED' },
47
+ { loc: '6:1', tag: 'div', kind: 'bg', dataClassification: 'STYLE_TOKEN' },
48
+ ];
49
+
50
+ const inventory = buildEditabilityInventory(candidates);
51
+ assert.equal(inventory.totalDiscovered, 6);
52
+ assert.equal(inventory.expectedEditableCount, 3);
53
+ assert.equal(inventory.platformControlledCount, 1);
54
+ assert.equal(inventory.runtimeDataCount, 1);
55
+ assert.equal(inventory.decorativeCount, 1);
56
+
57
+ // Invariant: sum of all tiers must exactly equal totalDiscovered
58
+ const sumTiers =
59
+ inventory.expectedEditableCount +
60
+ inventory.platformControlledCount +
61
+ inventory.runtimeDataCount +
62
+ inventory.decorativeCount +
63
+ inventory.interactionStateCount;
64
+ assert.equal(sumTiers, inventory.totalDiscovered, 'No candidate lost invariant must hold');
65
+ });
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { instrumentPageKey } = require('../transforms/routing/page-key.cjs');
9
+ const { auditProjectAssets } = require('../asset-auditor.cjs');
10
+
11
+ test('routing-and-assets: instrumentPageKey stamps custom root components like PlatformProductDetail', () => {
12
+ const code = `
13
+ export default function ProductDetailPage() {
14
+ return <PlatformProductDetail initialProduct={product} />;
15
+ }
16
+ `;
17
+ const result = instrumentPageKey(code, 'src/app/products/detail/page.tsx', 'products_detail');
18
+ assert.equal(result.updated, true);
19
+ assert.match(result.code, /<PlatformProductDetail\s+data-preview-page-key="products_detail"/);
20
+ });
21
+
22
+ test('routing-and-assets: instrumentPageKey updates mismatched existing page key to canonical route id', () => {
23
+ const code = `
24
+ export default function VantaPage() {
25
+ return <main data-preview-page-key="products"><h1>Vanta Aero</h1></main>;
26
+ }
27
+ `;
28
+ const result = instrumentPageKey(code, 'src/app/products/vanta-aero-x/page.tsx', 'products_vanta-aero-x');
29
+ assert.equal(result.updated, true);
30
+ assert.match(result.code, /data-preview-page-key="products_vanta-aero-x"/);
31
+ });
32
+
33
+ test('routing-and-assets: auditProjectAssets reports missing assets against public directory', () => {
34
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-assets-'));
35
+ const publicDir = path.join(tempDir, 'public');
36
+ fs.mkdirSync(publicDir, { recursive: true });
37
+ fs.writeFileSync(path.join(publicDir, 'logo.svg'), '<svg></svg>');
38
+
39
+ const siteData = {
40
+ content: {
41
+ common: {
42
+ logoUrl: '/logo.svg',
43
+ brokenLogo: '/fivora-logo.png',
44
+ },
45
+ },
46
+ };
47
+
48
+ const audit = auditProjectAssets(tempDir, siteData);
49
+ assert.equal(audit.passed, false);
50
+ assert.equal(audit.missingAssets.length, 1);
51
+ assert.equal(audit.missingAssets[0].path, '/fivora-logo.png');
52
+ assert.equal(audit.verifiedAssets.length, 1);
53
+ assert.equal(audit.verifiedAssets[0].path, '/logo.svg');
54
+ });
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { parseSource } = require('../ast.cjs');
6
+ const {
7
+ SCOPE_STATUS,
8
+ classifySiteDataRequirement,
9
+ } = require('../transforms/runtime/site-data-scope.cjs');
10
+ const { injectSiteDataHook } = require('../transforms/runtime/provider.cjs');
11
+
12
+ test('site-data-scope: classifies function with siteData parameter as PROP_BOUND', () => {
13
+ const code = 'export function getRequiredPages(siteData: any) { return siteData.pages; }';
14
+ const ast = parseSource(code, 'requiredPages.ts');
15
+ const fn = ast.program.body[0].declaration;
16
+ const status = classifySiteDataRequirement(fn, 'getRequiredPages', { filePath: 'src/lib/requiredPages.ts' });
17
+ assert.equal(status, SCOPE_STATUS.PROP_BOUND);
18
+
19
+ const injected = injectSiteDataHook(ast, 'src/lib/requiredPages.ts');
20
+ assert.equal(injected, false, 'Should not inject hook when siteData is already a prop/param');
21
+ });
22
+
23
+ test('site-data-scope: classifies RootLayout mounting SiteDataProvider as PROVIDER_OWNER', () => {
24
+ const code = `
25
+ export default function RootLayout({ children }: { children: any }) {
26
+ return (
27
+ <html>
28
+ <body>
29
+ <SiteDataProvider initialSiteData={initialSiteData}>
30
+ {children}
31
+ </SiteDataProvider>
32
+ </body>
33
+ </html>
34
+ );
35
+ }
36
+ `;
37
+ const ast = parseSource(code, 'layout.tsx');
38
+ const fn = ast.program.body[0].declaration;
39
+ const status = classifySiteDataRequirement(fn, 'RootLayout', {
40
+ filePath: 'src/app/layout.tsx',
41
+ mountsProvider: true,
42
+ isServerComponent: true,
43
+ });
44
+ assert.equal(status, SCOPE_STATUS.PROVIDER_OWNER);
45
+
46
+ const injected = injectSiteDataHook(ast, 'src/app/layout.tsx');
47
+ assert.equal(injected, false, 'Should not inject hook into RootLayout or Provider owner');
48
+ });
49
+
50
+ test('site-data-scope: classifies async function as SERVER_BOUNDARY', () => {
51
+ const code = `
52
+ export default async function Page() {
53
+ const data = await fetch('https://api.example.com');
54
+ return <div>{siteData.title}</div>;
55
+ }
56
+ `;
57
+ const ast = parseSource(code, 'page.tsx');
58
+ const fn = ast.program.body[0].declaration;
59
+ const status = classifySiteDataRequirement(fn, 'Page', { filePath: 'src/app/page.tsx', isServerComponent: true });
60
+ assert.equal(status, SCOPE_STATUS.SERVER_BOUNDARY);
61
+
62
+ const injected = injectSiteDataHook(ast, 'src/app/page.tsx', { isServerComponent: true });
63
+ assert.equal(injected, false, 'Should not inject hook into async server component');
64
+ });
65
+
66
+ test('site-data-scope: classifies plain utility function without JSX as UTILITY_FUNCTION', () => {
67
+ const code = `
68
+ export function calculateTotal(items: any[]) {
69
+ return items.reduce((acc, x) => acc + x.price, 0);
70
+ }
71
+ `;
72
+ const ast = parseSource(code, 'utils.ts');
73
+ const fn = ast.program.body[0].declaration;
74
+ const status = classifySiteDataRequirement(fn, 'calculateTotal', { filePath: 'src/lib/utils.ts' });
75
+ assert.equal(status, SCOPE_STATUS.UTILITY_FUNCTION);
76
+
77
+ const injected = injectSiteDataHook(ast, 'src/lib/utils.ts');
78
+ assert.equal(injected, false, 'Should not inject hook into utility functions');
79
+ });
80
+
81
+ test('site-data-scope: classifies React component referencing siteData as HOOK_SAFE', () => {
82
+ const code = `
83
+ export function HeroBanner() {
84
+ return <h1>{siteData.home.heroTitle}</h1>;
85
+ }
86
+ `;
87
+ const ast = parseSource(code, 'HeroBanner.tsx');
88
+ const fn = ast.program.body[0].declaration;
89
+ const status = classifySiteDataRequirement(fn, 'HeroBanner', { filePath: 'src/components/HeroBanner.tsx', isClientComponent: true });
90
+ assert.equal(status, SCOPE_STATUS.HOOK_SAFE);
91
+
92
+ const injected = injectSiteDataHook(ast, 'src/components/HeroBanner.tsx', { isClientComponent: true });
93
+ assert.equal(injected, true, 'Should safely inject hook into valid UI component');
94
+ });
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { TransformProof, ProofRegistry } = require('../transform-proof.cjs');
6
+
7
+ test('transform-proof: records valid proof object and verifies preconditions', () => {
8
+ const proof = new TransformProof({
9
+ type: 'TEXT_BIND',
10
+ source: { file: 'Hero.tsx', loc: '42:6', tag: 'h1', originalText: 'Vanta Aero' },
11
+ target: { path: 'home.hero.title', type: 'text', section: 'hero' },
12
+ preconditions: ['client-safe', 'leaf-node', 'content-owned'],
13
+ verification: { astParsed: true, contractValid: true, designPreserved: true },
14
+ });
15
+
16
+ assert.equal(proof.isVerified(), true);
17
+ assert.equal(proof.type, 'TEXT_BIND');
18
+ assert.equal(proof.target.path, 'home.hero.title');
19
+ assert.equal(proof.preconditions.length, 3);
20
+ });
21
+
22
+ test('transform-proof: ProofRegistry enforces phase invariants against planned count', () => {
23
+ const registry = new ProofRegistry();
24
+ registry.record({
25
+ type: 'TEXT_BIND',
26
+ source: { file: 'Hero.tsx', loc: '10:2' },
27
+ target: { path: 'home.hero.title', type: 'text' },
28
+ });
29
+ registry.record({
30
+ type: 'ACTION_SPLIT',
31
+ source: { file: 'Hero.tsx', loc: '15:2' },
32
+ target: { path: 'home.hero.ctaLabel', type: 'text' },
33
+ });
34
+
35
+ // Test passed invariants
36
+ const passCheck = registry.verifyPhaseInvariants({ plannedCount: 2, boundCount: 2, schemaPathCount: 2 });
37
+ assert.equal(passCheck.passed, true);
38
+
39
+ // Test violation when planned exceeds recorded proofs
40
+ const failCheck = registry.verifyPhaseInvariants({ plannedCount: 3, boundCount: 2, schemaPathCount: 2 });
41
+ assert.equal(failCheck.passed, false);
42
+ assert.ok(failCheck.violations[0].includes('Phase Invariant Violation'));
43
+ });
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { parseSource, printSource } = require('../ast.cjs');
9
+ const { injectSiteDataHook } = require('../transforms/runtime/provider.cjs');
10
+ const { instrumentPageKey } = require('../transforms/routing/page-key.cjs');
11
+ const { normalizeFieldType, sanitizeEditorSections } = require('../fivora-schema-authority.cjs');
12
+ const { auditProjectAssets } = require('../asset-auditor.cjs');
13
+ const { recoverCandidateConfidence } = require('../confidence-recovery.cjs');
14
+
15
+ test('vanta-regressions: RootLayout server component is never injected with useSiteData hook', () => {
16
+ const code = `
17
+ export default function RootLayout({ children }: { children: any }) {
18
+ return (
19
+ <html>
20
+ <body>
21
+ <SiteDataProvider initialSiteData={initialSiteData}>
22
+ {children}
23
+ </SiteDataProvider>
24
+ </body>
25
+ </html>
26
+ );
27
+ }
28
+ `;
29
+ const ast = parseSource(code, 'layout.tsx');
30
+ const injected = injectSiteDataHook(ast, 'src/app/layout.tsx', { isServerComponent: true, mountsProvider: true });
31
+ assert.equal(injected, false, 'RootLayout mounting SiteDataProvider must NEVER receive useSiteData');
32
+ const result = printSource(ast, code);
33
+ assert.doesNotMatch(result, /useSiteData\(\)/);
34
+ });
35
+
36
+ test('vanta-regressions: getRequiredPages(siteData) parameter is never shadowed or duplicated', () => {
37
+ const code = `
38
+ export function getRequiredPages(siteData: any) {
39
+ if (Array.isArray(siteData?.pages)) return siteData.pages;
40
+ return ['home', 'shop'];
41
+ }
42
+ `;
43
+ const ast = parseSource(code, 'requiredPages.ts');
44
+ const injected = injectSiteDataHook(ast, 'src/lib/requiredPages.ts');
45
+ assert.equal(injected, false, 'Functions with siteData parameter must NEVER have hook injected');
46
+ const result = printSource(ast, code);
47
+ assert.doesNotMatch(result, /const siteData = useSiteData\(\)/);
48
+ });
49
+
50
+ test('vanta-regressions: schema type "currency" is auto-normalized to "text"', () => {
51
+ assert.equal(normalizeFieldType('currency', 'priceLkrLabel'), 'text');
52
+ });
53
+
54
+ test('vanta-regressions: duplicate path site.announcement.linkUrl across sections is eliminated', () => {
55
+ const sections = [
56
+ {
57
+ id: 'site_announcement',
58
+ path: 'site.announcement',
59
+ type: 'object',
60
+ fields: [{ key: 'linkUrl', type: 'url', label: 'Link' }],
61
+ },
62
+ {
63
+ id: 'site',
64
+ path: 'site',
65
+ type: 'object',
66
+ fields: [{ key: 'announcement.linkUrl', type: 'url', label: 'Duplicate' }],
67
+ },
68
+ ];
69
+ const clean = sanitizeEditorSections(sections);
70
+ const dup = clean[1].fields.find((f) => f.key === 'announcement.linkUrl');
71
+ assert.equal(dup, undefined, 'Duplicate leaf path must be eliminated');
72
+ });
73
+
74
+ test('vanta-regressions: custom component on /products/detail receives data-preview-page-key', () => {
75
+ const code = 'export default function Detail() { return <PlatformProductDetail item={shoe} />; }';
76
+ const res = instrumentPageKey(code, 'src/app/products/detail/page.tsx', 'products_detail');
77
+ assert.equal(res.updated, true);
78
+ assert.match(res.code, /data-preview-page-key="products_detail"/);
79
+ });
80
+
81
+ test('vanta-regressions: missing /fivora-logo.png is flagged before runtime', () => {
82
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deneb-vanta-'));
83
+ const publicDir = path.join(tempDir, 'public');
84
+ fs.mkdirSync(publicDir, { recursive: true });
85
+ fs.writeFileSync(path.join(publicDir, 'logo.svg'), '<svg></svg>');
86
+
87
+ const siteData = {
88
+ content: {
89
+ common: {
90
+ logoUrl: '/fivora-logo.png',
91
+ },
92
+ },
93
+ };
94
+ const audit = auditProjectAssets(tempDir, siteData);
95
+ assert.equal(audit.passed, false);
96
+ assert.equal(audit.missingAssets[0].path, '/fivora-logo.png');
97
+ });
98
+
99
+ test('vanta-regressions: low-confidence candidate (< 0.60) is elevated by recovery pipeline', () => {
100
+ const candidate = {
101
+ tag: 'h2',
102
+ componentName: 'FeatureBanner',
103
+ className: 'text-2xl font-bold text-slate-900',
104
+ value: 'Designed in Colombo, Built for the Street',
105
+ confidence: 0.45,
106
+ };
107
+ const rec = recoverCandidateConfidence(candidate);
108
+ assert.equal(rec.recovered, true);
109
+ assert.ok(rec.confidence >= 0.65);
110
+ });
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Deneb ARC v3 — Production Acceptance Gates Engine
5
- * Implements the 12-Gate Acceptance Matrix recommended for enterprise Next.js -> Fivora compilation.
4
+ * Deneb ARC v3 — Production 15-Gate Acceptance Matrix (P12)
5
+ * Comprehensive compiler gates guaranteeing 100% verifiable Fivora conversion.
6
6
  */
7
7
  const ACCEPTANCE_GATES = {
8
8
  SOURCE_ANALYSIS: 'Source Analysis',
@@ -17,10 +17,13 @@ const ACCEPTANCE_GATES = {
17
17
  ROUTE_COVERAGE: 'Route Coverage',
18
18
  DESIGN_PRESERVATION: 'Design Preservation',
19
19
  CONTROL_ONLY_PROTECTION: 'Control-Only Protection',
20
+ ASSET_INTEGRITY: 'Asset Integrity',
21
+ RSC_CLIENT_INTEGRITY: 'RSC & Client Hook Integrity',
22
+ BROWSER_CONSOLE_CLEANLINESS: 'Browser Console Cleanliness',
20
23
  };
21
24
 
22
25
  /**
23
- * Evaluates compilation metrics against the 12-Gate Acceptance Matrix.
26
+ * Evaluates compilation metrics against the 15-Gate Acceptance Matrix.
24
27
  */
25
28
  function evaluateAcceptanceGates(metrics = {}) {
26
29
  const gates = {
@@ -88,6 +91,21 @@ function evaluateAcceptanceGates(metrics = {}) {
88
91
  passed: Boolean(metrics.controlOnlyValid !== false),
89
92
  critical: true,
90
93
  },
94
+ assetIntegrity: {
95
+ name: ACCEPTANCE_GATES.ASSET_INTEGRITY,
96
+ passed: Boolean(metrics.assetIntegrityPassed !== false),
97
+ critical: true,
98
+ },
99
+ rscClientIntegrity: {
100
+ name: ACCEPTANCE_GATES.RSC_CLIENT_INTEGRITY,
101
+ passed: Boolean(metrics.rscIntegrityPassed !== false),
102
+ critical: true,
103
+ },
104
+ browserConsoleCleanliness: {
105
+ name: ACCEPTANCE_GATES.BROWSER_CONSOLE_CLEANLINESS,
106
+ passed: Boolean(metrics.consoleCleanlinessPassed !== false),
107
+ critical: false,
108
+ },
91
109
  };
92
110
 
93
111
  const criticalGates = Object.values(gates).filter((g) => g.critical);