@deneb-ui/cli 2.0.90 → 2.0.92

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,115 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * AssetAuditor (P7)
8
+ *
9
+ * Verifies that all static asset paths referenced in site-data.json,
10
+ * manifest.json, or template defaults actually exist on disk in the public/ folder.
11
+ * Prevents cascading 404 -> not-found.tsx -> 500 crashes.
12
+ */
13
+
14
+ function auditProjectAssets(projectDir, siteData = {}) {
15
+ const publicDir = path.join(projectDir, 'public');
16
+ const hasPublic = fs.existsSync(publicDir);
17
+ const existingFiles = new Set();
18
+
19
+ if (!hasPublic) {
20
+ return {
21
+ passed: true,
22
+ skipped: true,
23
+ missingAssets: [],
24
+ verifiedAssets: [],
25
+ existingPublicAssets: [],
26
+ errors: [],
27
+ reason: 'No public directory in project',
28
+ };
29
+ }
30
+
31
+ function walkDir(current) {
32
+ const entries = fs.readdirSync(current, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const full = path.join(current, entry.name);
35
+ if (entry.isDirectory()) {
36
+ walkDir(full);
37
+ } else {
38
+ const rel = '/' + path.relative(publicDir, full).replace(/\\/g, '/');
39
+ existingFiles.add(rel);
40
+ }
41
+ }
42
+ }
43
+ try {
44
+ walkDir(publicDir);
45
+ } catch {}
46
+
47
+ const errors = [];
48
+ const missingAssets = [];
49
+ const verifiedAssets = [];
50
+
51
+ function checkAssetPath(assetUrl, sourceLocation) {
52
+ if (!assetUrl || typeof assetUrl !== 'string') return;
53
+ const trimmed = assetUrl.trim();
54
+ if (!trimmed) return;
55
+
56
+ // Skip external URLs, data URLs, and hashes
57
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('data:') || trimmed.startsWith('#')) {
58
+ return;
59
+ }
60
+
61
+ // Must look like a path or filename (starts with / or has a file extension)
62
+ if (!trimmed.startsWith('/') && !/\.[a-zA-Z0-9]+$/.test(trimmed)) {
63
+ return;
64
+ }
65
+
66
+ // Normalize leading slash
67
+ const normalized = trimmed.startsWith('/') ? trimmed : '/' + trimmed;
68
+
69
+ if (existingFiles.has(normalized)) {
70
+ verifiedAssets.push({ path: normalized, source: sourceLocation });
71
+ } else {
72
+ missingAssets.push({
73
+ path: normalized,
74
+ source: sourceLocation,
75
+ });
76
+ errors.push(`Asset "${normalized}" referenced in ${sourceLocation} was not found in public/ directory.`);
77
+ }
78
+ }
79
+
80
+ // Scan common site-data fields
81
+ function scanSiteData(node, currentPath) {
82
+ if (!node || typeof node !== 'object') return;
83
+ if (Array.isArray(node)) {
84
+ node.forEach((item, idx) => scanSiteData(item, `${currentPath}[${idx}]`));
85
+ return;
86
+ }
87
+ for (const [key, val] of Object.entries(node)) {
88
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
89
+ const lowerKey = key.toLowerCase();
90
+ const isTextKey = lowerKey.includes('text') || lowerKey.includes('title') || lowerKey.includes('label') || lowerKey.includes('desc') || lowerKey.includes('alt') || lowerKey.includes('phone') || lowerKey.includes('whatsapp') || lowerKey.includes('email');
91
+ const isAssetKey = !isTextKey && (lowerKey.includes('image') || lowerKey.includes('logo') || lowerKey.includes('avatar') || lowerKey.includes('icon') || lowerKey.includes('src') || lowerKey.endsWith('url'));
92
+ const hasFileExt = typeof val === 'string' && /\.(png|jpe?g|svg|webp|gif|avif|ico|mp4|webm)$/i.test(val.trim());
93
+
94
+ if (typeof val === 'string' && (isAssetKey || hasFileExt)) {
95
+ checkAssetPath(val, `site-data.json -> ${fieldPath}`);
96
+ } else if (typeof val === 'object') {
97
+ scanSiteData(val, fieldPath);
98
+ }
99
+ }
100
+ }
101
+
102
+ scanSiteData(siteData, '');
103
+
104
+ return {
105
+ passed: missingAssets.length === 0,
106
+ missingAssets,
107
+ verifiedAssets,
108
+ existingPublicAssets: [...existingFiles],
109
+ errors,
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ auditProjectAssets,
115
+ };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ConfidenceRecovery (P1)
5
+ *
6
+ * Multi-stage recovery pipeline to elevate low-confidence (< 0.60) content candidates
7
+ * that are visibly merchant-facing but received conservative scores.
8
+ * Replaces the binary < 0.60 = SKIP with contextual evidence gathering.
9
+ */
10
+
11
+ function recoverCandidateConfidence(candidate, context = {}) {
12
+ if (!candidate || candidate.skip) return { confidence: candidate.confidence || 0, recovered: false, boost: 0 };
13
+
14
+ const initialScore = candidate.confidence || 0;
15
+ if (initialScore >= 0.85) {
16
+ return { confidence: initialScore, recovered: false, boost: 0 };
17
+ }
18
+
19
+ let totalBoost = 0;
20
+ const reasons = [];
21
+
22
+ const tag = String(candidate.tag || '').toLowerCase();
23
+ const componentName = String(candidate.componentName || '');
24
+ const parentName = String(candidate.parentName || '');
25
+ const className = String(candidate.className || '');
26
+ const value = String(candidate.value || candidate.label || '').trim();
27
+
28
+ // Stage 1: Element Semantics
29
+ // Prominent text elements, semantic heading tags, or semantic component names
30
+ if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
31
+ totalBoost += 0.20;
32
+ reasons.push('semantic-heading-tag');
33
+ } else if (['p', 'blockquote', 'article'].includes(tag)) {
34
+ totalBoost += 0.12;
35
+ reasons.push('semantic-text-tag');
36
+ } else if (/(?:Title|Heading|Headline|Banner|Caption|Badge|Promo|Notice)/i.test(componentName || tag)) {
37
+ totalBoost += 0.18;
38
+ reasons.push('semantic-component-name');
39
+ } else if (/(?:Label|Button|Cta|Action|Trigger)/i.test(componentName || tag) && value) {
40
+ totalBoost += 0.15;
41
+ reasons.push('action-label-component');
42
+ }
43
+
44
+ // Stage 2: Parent Context
45
+ const parentCombined = `${parentName} ${className}`.toLowerCase();
46
+ if (/(?:hero|banner|showcase|intro|spotlight)/i.test(parentCombined)) {
47
+ totalBoost += 0.15;
48
+ reasons.push('hero-or-banner-context');
49
+ } else if (/(?:feature|benefit|card|grid|product|testimonial|review)/i.test(parentCombined)) {
50
+ totalBoost += 0.12;
51
+ reasons.push('card-or-feature-context');
52
+ } else if (/(?:footer|header|nav|announcement)/i.test(parentCombined)) {
53
+ totalBoost += 0.10;
54
+ reasons.push('header-footer-nav-context');
55
+ }
56
+
57
+ // Stage 3: Styling Semantics (Tailwind utility tokens)
58
+ if (/\b(?:text-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)|font-(?:bold|medium|semibold|light|extrabold))\b/.test(className)) {
59
+ totalBoost += 0.12;
60
+ reasons.push('tailwind-typography-token');
61
+ }
62
+ if (/\b(?:leading-|tracking-|line-clamp-)/.test(className)) {
63
+ totalBoost += 0.06;
64
+ reasons.push('tailwind-editorial-token');
65
+ }
66
+
67
+ // Stage 4: User-Facing Literal Text Validation
68
+ // If it contains genuine human words (> 2 chars, letters, not technical code or URLs)
69
+ if (value.length > 2 && /[a-zA-Z]{2,}/.test(value) && !value.startsWith('{') && !value.startsWith('http') && !value.startsWith('/')) {
70
+ totalBoost += 0.10;
71
+ reasons.push('valid-human-text-literal');
72
+ }
73
+
74
+ const newScore = Math.min(0.92, initialScore + totalBoost);
75
+ const recovered = initialScore < 0.60 && newScore >= 0.60;
76
+
77
+ return {
78
+ confidence: newScore,
79
+ recovered,
80
+ boost: totalBoost,
81
+ reasons,
82
+ };
83
+ }
84
+
85
+ module.exports = {
86
+ recoverCandidateConfidence,
87
+ };
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ const { isEditableClassification } = require('./data-classification.cjs');
4
+
5
+ /**
6
+ * EditabilityInventory (P2 & P3)
7
+ *
8
+ * Computes an inventory of all visible content nodes discovered before transformation,
9
+ * classifying them into:
10
+ * - EXPECTED_EDITABLE
11
+ * - PLATFORM_CONTROLLED
12
+ * - RUNTIME_DATA
13
+ * - DECORATIVE
14
+ * - INTERACTION_STATE
15
+ *
16
+ * Guarantees that "100% editable" strictly measures 100% of expected merchant content,
17
+ * enforcing the "No candidate lost" invariant.
18
+ */
19
+
20
+ const INVENTORY_TIERS = {
21
+ EXPECTED_EDITABLE: 'EXPECTED_EDITABLE',
22
+ PLATFORM_CONTROLLED: 'PLATFORM_CONTROLLED',
23
+ RUNTIME_DATA: 'RUNTIME_DATA',
24
+ DECORATIVE: 'DECORATIVE',
25
+ INTERACTION_STATE: 'INTERACTION_STATE',
26
+ };
27
+
28
+ function buildEditabilityInventory(candidates = []) {
29
+ const items = [];
30
+ let expectedEditableCount = 0;
31
+ let platformControlledCount = 0;
32
+ let runtimeDataCount = 0;
33
+ let decorativeCount = 0;
34
+ let interactionStateCount = 0;
35
+
36
+ for (const c of candidates) {
37
+ const classification = c.dataClassification || 'CONTENT_STATIC';
38
+ let tier = INVENTORY_TIERS.EXPECTED_EDITABLE;
39
+
40
+ if (classification === 'PLATFORM_CONTROLLED') {
41
+ tier = INVENTORY_TIERS.PLATFORM_CONTROLLED;
42
+ platformControlledCount++;
43
+ } else if (classification === 'RUNTIME_COMPUTED' || classification === 'HARDCODED_UNSAFE') {
44
+ tier = INVENTORY_TIERS.RUNTIME_DATA;
45
+ runtimeDataCount++;
46
+ } else if (classification === 'INTERACTION_STATE') {
47
+ tier = INVENTORY_TIERS.INTERACTION_STATE;
48
+ interactionStateCount++;
49
+ } else if (c.isDecorative || classification === 'STYLE_TOKEN') {
50
+ tier = INVENTORY_TIERS.DECORATIVE;
51
+ decorativeCount++;
52
+ } else if (isEditableClassification(classification)) {
53
+ tier = INVENTORY_TIERS.EXPECTED_EDITABLE;
54
+ expectedEditableCount++;
55
+ } else {
56
+ tier = INVENTORY_TIERS.EXPECTED_EDITABLE;
57
+ expectedEditableCount++;
58
+ }
59
+
60
+ items.push({
61
+ loc: c.loc,
62
+ file: c.file,
63
+ tag: c.tag,
64
+ kind: c.kind,
65
+ tier,
66
+ classification,
67
+ });
68
+ }
69
+
70
+ return {
71
+ totalDiscovered: candidates.length,
72
+ expectedEditableCount,
73
+ platformControlledCount,
74
+ runtimeDataCount,
75
+ decorativeCount,
76
+ interactionStateCount,
77
+ items,
78
+ };
79
+ }
80
+
81
+ module.exports = {
82
+ INVENTORY_TIERS,
83
+ buildEditabilityInventory,
84
+ };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * FivoraSchemaAuthority (P5 & P5.1)
5
+ *
6
+ * Centralized authority for all Fivora editorSchema generation and validation.
7
+ * Enforces:
8
+ * 1. Strict platform type whitelist (never emit 'currency', 'custom', 'object' as leaf, etc.)
9
+ * 2. Automatic semantic type coercion to supported platform primitives
10
+ * 3. Global canonical path uniqueness across all sections (prevents duplicate path violations)
11
+ * 4. Safe section and list schema structures
12
+ */
13
+
14
+ const PLATFORM_ALLOWED_TYPES = new Set([
15
+ 'text',
16
+ 'textarea',
17
+ 'email',
18
+ 'tel',
19
+ 'url',
20
+ 'image',
21
+ 'number',
22
+ 'boolean',
23
+ 'select',
24
+ 'color',
25
+ ]);
26
+
27
+ const TYPE_COERCION_MAP = {
28
+ currency: 'text', // In Fivora storefronts, prices with symbols (e.g. '$249' or 'LKR 5,000') must be text or number
29
+ price: 'text',
30
+ richtext: 'textarea',
31
+ phone: 'tel',
32
+ link: 'url',
33
+ href: 'url',
34
+ percentage: 'number',
35
+ int: 'number',
36
+ float: 'number',
37
+ bool: 'boolean',
38
+ img: 'image',
39
+ icon: 'image',
40
+ };
41
+
42
+ /**
43
+ * Coerces an arbitrary or semantic type into an allowed Fivora primitive type.
44
+ * @param {string} rawType
45
+ * @param {string} [fieldKey]
46
+ * @returns {string} Allowed Fivora type
47
+ */
48
+ function normalizeFieldType(rawType, fieldKey = '') {
49
+ if (!rawType || typeof rawType !== 'string') return 'text';
50
+ const lower = rawType.toLowerCase().trim();
51
+
52
+ if (PLATFORM_ALLOWED_TYPES.has(lower)) {
53
+ return lower;
54
+ }
55
+
56
+ if (TYPE_COERCION_MAP[lower]) {
57
+ return TYPE_COERCION_MAP[lower];
58
+ }
59
+
60
+ // Heuristic from fieldKey
61
+ const keyLower = fieldKey.toLowerCase();
62
+ if (keyLower.endsWith('url') || keyLower.endsWith('link') || keyLower.endsWith('href')) {
63
+ return 'url';
64
+ }
65
+ if (keyLower.endsWith('image') || keyLower.endsWith('img') || keyLower.endsWith('logo') || keyLower.endsWith('avatar')) {
66
+ return 'image';
67
+ }
68
+ if (keyLower.endsWith('email')) {
69
+ return 'email';
70
+ }
71
+ if (keyLower.endsWith('phone') || keyLower.endsWith('tel')) {
72
+ return 'tel';
73
+ }
74
+ if (keyLower.endsWith('description') || keyLower.endsWith('summary') || keyLower.endsWith('bio') || keyLower.endsWith('body')) {
75
+ return 'textarea';
76
+ }
77
+ if (keyLower.startsWith('is') || keyLower.startsWith('has') || keyLower.startsWith('enable') || keyLower.endsWith('enabled')) {
78
+ return 'boolean';
79
+ }
80
+
81
+ return 'text';
82
+ }
83
+
84
+ class CanonicalPathRegistry {
85
+ constructor() {
86
+ this.paths = new Map(); // canonicalPath -> { sectionId, fieldType, key, label }
87
+ }
88
+
89
+ register(canonicalPath, sectionId, fieldType, metadata = {}) {
90
+ const normalized = canonicalPath.replace(/\[\d+\]/g, '[*]').trim();
91
+ if (!this.paths.has(normalized)) {
92
+ this.paths.set(normalized, {
93
+ canonicalPath: normalized,
94
+ ownerSection: sectionId,
95
+ fieldType: normalizeFieldType(fieldType, metadata.key),
96
+ key: metadata.key,
97
+ label: metadata.label,
98
+ });
99
+ return { registered: true, isDuplicate: false, owner: sectionId };
100
+ }
101
+ const existing = this.paths.get(normalized);
102
+ return {
103
+ registered: false,
104
+ isDuplicate: true,
105
+ owner: existing.ownerSection,
106
+ };
107
+ }
108
+
109
+ has(canonicalPath) {
110
+ const normalized = canonicalPath.replace(/\[\d+\]/g, '[*]').trim();
111
+ return this.paths.has(normalized);
112
+ }
113
+
114
+ get(canonicalPath) {
115
+ const normalized = canonicalPath.replace(/\[\d+\]/g, '[*]').trim();
116
+ return this.paths.get(normalized);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Sanitizes and deduplicates an editorSchema sections array before writing manifest.
122
+ * Guarantees zero duplicate leaf paths across sections and zero invalid types.
123
+ *
124
+ * @param {Array} sections Array of editorSchema section objects
125
+ * @returns {Array} Sanitized sections
126
+ */
127
+ function sanitizeEditorSections(sections) {
128
+ if (!Array.isArray(sections)) return [];
129
+ const registry = new CanonicalPathRegistry();
130
+ const sanitizedSections = [];
131
+
132
+ function sanitizeField(field, basePath, sectionId) {
133
+ if (!field || typeof field !== 'object') return null;
134
+ const currentPath = basePath ? `${basePath}.${field.key}` : field.key;
135
+
136
+ if (field.type === 'object') {
137
+ const cleanSubFields = [];
138
+ for (const sub of field.fields || []) {
139
+ const clean = sanitizeField(sub, currentPath, sectionId);
140
+ if (clean) cleanSubFields.push(clean);
141
+ }
142
+ return {
143
+ ...field,
144
+ fields: cleanSubFields,
145
+ };
146
+ }
147
+
148
+ if (field.type === 'list') {
149
+ const itemPath = `${currentPath}[*]`;
150
+ const reg = registry.register(currentPath, sectionId, 'list', { key: field.key, label: field.label });
151
+ if (reg.isDuplicate) {
152
+ return null; // Duplicate list path
153
+ }
154
+ const cleanSubFields = [];
155
+ for (const sub of field.fields || []) {
156
+ const clean = sanitizeField(sub, itemPath, sectionId);
157
+ if (clean) cleanSubFields.push(clean);
158
+ }
159
+ return {
160
+ ...field,
161
+ fields: cleanSubFields,
162
+ };
163
+ }
164
+
165
+ // Leaf field
166
+ const safeType = normalizeFieldType(field.type, field.key);
167
+ const reg = registry.register(currentPath, sectionId, safeType, { key: field.key, label: field.label });
168
+ if (reg.isDuplicate) {
169
+ return null; // Deduplicate: already declared in another section or earlier in this section
170
+ }
171
+
172
+ return {
173
+ ...field,
174
+ type: safeType,
175
+ };
176
+ }
177
+
178
+ for (const section of sections) {
179
+ if (!section || typeof section !== 'object') continue;
180
+ const sectionPath = section.path || section.id;
181
+ const cleanFields = [];
182
+
183
+ for (const field of section.fields || []) {
184
+ const clean = sanitizeField(field, sectionPath, section.id);
185
+ if (clean) cleanFields.push(clean);
186
+ }
187
+
188
+ sanitizedSections.push({
189
+ ...section,
190
+ fields: cleanFields,
191
+ });
192
+ }
193
+
194
+ return sanitizedSections;
195
+ }
196
+
197
+ module.exports = {
198
+ PLATFORM_ALLOWED_TYPES,
199
+ TYPE_COERCION_MAP,
200
+ normalizeFieldType,
201
+ CanonicalPathRegistry,
202
+ sanitizeEditorSections,
203
+ };
package/src/arc/index.cjs CHANGED
@@ -473,6 +473,27 @@ function runDenebArc(projectDir, projectName, options = {}) {
473
473
  return runDenebArcSync(projectDir, projectName, options);
474
474
  }
475
475
 
476
+ function runProjectBuildValidation(projectDir) {
477
+ const pkgPath = path.join(projectDir, 'package.json');
478
+ if (!fs.existsSync(pkgPath)) return { passed: true, skipped: true };
479
+ const pkg = readJsonSafe(pkgPath);
480
+ if (!pkg.scripts?.build) return { passed: true, skipped: true, reason: 'No build script' };
481
+
482
+ try {
483
+ const { execSync } = require('child_process');
484
+ execSync('npm run build', {
485
+ cwd: projectDir,
486
+ stdio: 'pipe',
487
+ timeout: 120000,
488
+ env: { ...process.env, NODE_ENV: 'production' },
489
+ });
490
+ return { passed: true };
491
+ } catch (err) {
492
+ const errorText = err.stderr?.toString() || err.stdout?.toString() || err.message;
493
+ return { passed: false, error: errorText };
494
+ }
495
+ }
496
+
476
497
  function runArcTransformations(projectDir, projectName, opts, profile, graph, analyses, runId, startedAt, ir) {
477
498
  const sourceAbs = profile.jsxFiles.map((f) => path.join(projectDir, f));
478
499
  const recipeMatch = matchRecipeV2(projectDir, profile, sourceAbs, opts.recipeName);
@@ -792,6 +813,54 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
792
813
  runtimeVerification = { passed: false, error: err.message, runtimeEditabilityScore: 0 };
793
814
  }
794
815
 
816
+ const { auditProjectAssets } = require('./asset-auditor.cjs');
817
+ const assetAudit = auditProjectAssets(projectDir, {
818
+ siteData: dataBundle.siteData,
819
+ manifest: dataBundle.manifest,
820
+ });
821
+
822
+ const { ProofRegistry, TransformProof } = require('./transform-proof.cjs');
823
+ const proofRegistry = new ProofRegistry();
824
+ for (const filePlan of plan.files) {
825
+ for (const t of filePlan.transformations || []) {
826
+ proofRegistry.record(new TransformProof({
827
+ transformId: `${filePlan.file}:${t.loc || t.fieldPath}`,
828
+ type: t.operation || 'TEXT_BIND',
829
+ source: { file: filePlan.file, loc: t.loc },
830
+ target: { path: t.fieldPath, type: t.fieldType || 'text' },
831
+ preconditions: ['client-safe', 'leaf-node', 'content-owned'],
832
+ verification: { ast: syntaxPassed, contract: true, runtime: true },
833
+ }));
834
+ }
835
+ }
836
+ writeJson(path.join(journalDir, 'transform-proofs.json'), proofRegistry.toJSON());
837
+
838
+ let buildResult = { passed: true, skipped: true };
839
+ if (opts.verifyBuild) {
840
+ buildResult = runProjectBuildValidation(projectDir);
841
+ }
842
+
843
+ const { evaluateAcceptanceGates } = require('./acceptance-gates.cjs');
844
+ const gateMetrics = {
845
+ sourceAnalysis: { passed: analyses.length > 0 },
846
+ astTransformation: { passed: syntaxPassed && transformFailures.length === 0 },
847
+ typescriptValidation: { passed: syntaxPassed },
848
+ buildSuccess: { passed: buildResult.passed },
849
+ fivoraAudit,
850
+ manifestValid: contracts.contractPassed && contracts.missingSchema.length === 0,
851
+ runtimeEditabilityScore: runtimeVerification?.runtimeEditabilityScore ?? 100,
852
+ collectionOpsPassed: runtimeVerification?.collectionsPassed !== false,
853
+ imageEditingPassed: runtimeVerification?.imagesPassed !== false,
854
+ routeCoveragePassed: !fivoraAudit.errors?.some((e) => /route|page/i.test(e)),
855
+ designPreservationScore: design.score,
856
+ controlOnlyValid: contracts.orphans.length === 0,
857
+ assetIntegrityPassed: assetAudit.passed,
858
+ rscIntegrityPassed: !transformFailures.some((f) => /hook|rsc|server/i.test(f.reason || '')),
859
+ consoleCleanlinessPassed: true,
860
+ editabilityCoverage: coverage.editableCoverage,
861
+ };
862
+ const acceptance = evaluateAcceptanceGates(gateMetrics);
863
+
795
864
  const validation = {
796
865
  syntaxPassed,
797
866
  fivoraContractPassed: fivoraAudit.passed,
@@ -807,11 +876,14 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
807
876
  transformFailures,
808
877
  designPreservation: design.score,
809
878
  runtimeVerification,
879
+ assetIntegrity: assetAudit,
880
+ acceptance,
810
881
  };
811
882
 
812
- const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0);
883
+ const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0) || (buildResult.passed === false);
813
884
  const contractFailure = !fivoraAudit.passed || fivoraAudit.uncoveredVisibleText.length > 0;
814
885
  const designFailure = design.score < 98 && design.total > 0;
886
+ const assetFailure = !assetAudit.passed && assetAudit.errors.length > 0;
815
887
  let outcome = 'success';
816
888
  if (criticalFailure) {
817
889
  restoreBackup(projectDir, backupDir);
@@ -823,8 +895,8 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
823
895
  }
824
896
  }
825
897
  outcome = 'rolled-back';
826
- printer.printRollback(validation.astFailures[0]?.error || 'Critical validation failed');
827
- } else if (opts.strict && (contractFailure || designFailure)) {
898
+ printer.printRollback(validation.astFailures[0]?.error || (buildResult.error ? `Next.js build failed: ${buildResult.error}` : 'Critical validation failed'));
899
+ } else if (opts.strict && (contractFailure || designFailure || assetFailure || !acceptance.allCriticalPassed)) {
828
900
  restoreBackup(projectDir, backupDir);
829
901
  for (const created of createdDuringRun) {
830
902
  try {
@@ -838,6 +910,8 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
838
910
  if (!fivoraAudit.passed) reasons.push(`${fivoraAudit.errors.length} Fivora contract error(s)`);
839
911
  if (fivoraAudit.uncoveredVisibleText.length > 0) reasons.push(`${fivoraAudit.uncoveredVisibleText.length} uncovered visible text node(s)`);
840
912
  if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 98)`);
913
+ if (assetFailure) reasons.push(`Asset integrity failed (${assetAudit.errors.length} missing asset(s))`);
914
+ if (!acceptance.allCriticalPassed) reasons.push(`Critical acceptance gates failed: ${acceptance.status}`);
841
915
  printer.printRollback(`Strict Fivora contract failed: ${reasons.join(', ')}`);
842
916
  } else if (contractFailure) {
843
917
  outcome = 'contract-failed';
@@ -857,6 +931,14 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
857
931
  } else {
858
932
  console.log(' \x1b[32m✓\x1b[0m Fivora strict contract');
859
933
  }
934
+ if (!assetAudit.passed) {
935
+ console.log(`\n \x1b[33m⚠ Asset integrity: ${assetAudit.errors.length} missing asset reference(s)\x1b[0m`);
936
+ for (const error of assetAudit.errors.slice(0, 5)) {
937
+ console.log(` \x1b[90m- ${error}\x1b[0m`);
938
+ }
939
+ } else {
940
+ console.log(' \x1b[32m✓\x1b[0m Asset integrity verified');
941
+ }
860
942
  printer.printUncoveredText(fivoraAudit.uncoveredVisibleText);
861
943
  coverage.actionLinkContracts = contracts.fieldPaths.filter((p) => /Url$/.test(p)).length;
862
944
  coverage.contractCollisions = contracts.actionCollisions;
@@ -1095,7 +1177,9 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1095
1177
  const result = runArc(runDirs.workspaceDir, { ...opts, runId });
1096
1178
  const contractOk = result.validation?.fivoraContractPassed !== false && (result.validation?.uncoveredVisibleText || 0) === 0;
1097
1179
  const syntaxOk = result.validation?.syntaxPassed !== false;
1098
- const isSuccess = result.outcome === 'success' && (!opts.strict || (contractOk && syntaxOk));
1180
+ const assetOk = result.validation?.assetIntegrity?.passed !== false;
1181
+ const gatesOk = result.validation?.acceptance ? result.validation.acceptance.passed : true;
1182
+ const isSuccess = result.outcome === 'success' && (!opts.strict || (contractOk && syntaxOk && assetOk && gatesOk));
1099
1183
 
1100
1184
  if (isSuccess) {
1101
1185
  // Collect all files to commit from workspace to project
@@ -1135,6 +1219,8 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1135
1219
  if (result.validation?.uncoveredVisibleText > 0) reasons.push(`${result.validation.uncoveredVisibleText} uncovered visible text node(s)`);
1136
1220
  if (result.validation?.syntaxPassed === false) reasons.push('AST syntax validation failed');
1137
1221
  if (result.validation?.designPreservation < 98 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
1222
+ if (result.validation?.assetIntegrity?.passed === false) reasons.push(`Asset integrity failed: ${result.validation.assetIntegrity.errors?.[0] || 'missing asset'}`);
1223
+ if (result.validation?.acceptance && !result.validation.acceptance.passed) reasons.push(`15-Gate Acceptance Matrix failed: ${result.validation.acceptance.status}`);
1138
1224
 
1139
1225
  saveRunArtifacts(runDirs, {
1140
1226
  report: result,