@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 +2 -2
- package/src/arc/__tests__/arc.test.cjs +38 -1
- package/src/arc/__tests__/composite-fragments.test.cjs +48 -0
- package/src/arc/__tests__/fifteen-gates.test.cjs +47 -0
- package/src/arc/__tests__/fivora-schema-authority.test.cjs +75 -0
- package/src/arc/__tests__/recovery-and-inventory.test.cjs +65 -0
- package/src/arc/__tests__/routing-and-assets.test.cjs +54 -0
- package/src/arc/__tests__/site-data-scope.test.cjs +94 -0
- package/src/arc/__tests__/transform-proof.test.cjs +43 -0
- package/src/arc/__tests__/vanta-regressions.test.cjs +110 -0
- package/src/arc/acceptance-gates.cjs +21 -3
- package/src/arc/asset-auditor.cjs +115 -0
- package/src/arc/confidence-recovery.cjs +87 -0
- package/src/arc/editability-inventory.cjs +84 -0
- package/src/arc/fivora-schema-authority.cjs +203 -0
- package/src/arc/index.cjs +90 -4
- package/src/arc/manifest.cjs +16 -4
- package/src/arc/planner.cjs +13 -1
- package/src/arc/transform-proof.cjs +111 -0
- package/src/arc/transforms/healing/sanitizer.cjs +29 -2
- package/src/arc/transforms/primitives/text.cjs +18 -2
- package/src/arc/transforms/routing/page-key.cjs +43 -24
- package/src/arc/transforms/runtime/provider.cjs +51 -21
- package/src/arc/transforms/runtime/site-data-scope.cjs +227 -0
- package/src/sites/universal-page-selection.ts +25 -0
- package/src/tools/deneb-template-validator.cjs +19 -18
|
@@ -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
|
|
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,
|