@deneb-ui/cli 2.0.88 → 2.0.90
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/bin/index.js +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/common/template-visual-edit-contract.ts +4 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const contract = require('./fivora-contract.cjs');
|
|
6
|
+
const { validateRuntimeEditabilitySync, validateCollectionOperations } = require('./runtime-validator.cjs');
|
|
7
|
+
const { verifyInteractionsSync } = require('./interaction-verifier.cjs');
|
|
8
|
+
const { evaluateAcceptanceGates } = require('./acceptance-gates.cjs');
|
|
9
|
+
|
|
10
|
+
const KNOWN_STOREFRONTS = [
|
|
11
|
+
{ id: 'car-sale', name: 'Car Sale Storefront', relativePath: 'car-sale' },
|
|
12
|
+
{ id: 'coffee', name: 'Coffee Shop Storefront', relativePath: 'coffee' },
|
|
13
|
+
{ id: 'mobile-shop', name: 'Mobile Shop Storefront', relativePath: 'mobile-shop' },
|
|
14
|
+
{ id: 'restaurant', name: 'Restaurant Storefront', relativePath: 'restu-web' },
|
|
15
|
+
{ id: 'salon', name: 'Salon & Spa Storefront', relativePath: 'salon-web' },
|
|
16
|
+
{ id: 'shoe', name: 'Shoe Storefront', relativePath: 'shoe' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function resolveWorkspaceStorefrontsDir() {
|
|
20
|
+
let curr = __dirname;
|
|
21
|
+
for (let i = 0; i < 8; i++) {
|
|
22
|
+
if (fs.existsSync(path.join(curr, 'coffee')) && fs.existsSync(path.join(curr, 'car-sale'))) {
|
|
23
|
+
return curr;
|
|
24
|
+
}
|
|
25
|
+
curr = path.dirname(curr);
|
|
26
|
+
}
|
|
27
|
+
return path.resolve(__dirname, '../../../../../../');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveSiteDataFile(templateDir, manifest = {}) {
|
|
31
|
+
const candidates = [
|
|
32
|
+
manifest.siteDataFile,
|
|
33
|
+
'src/data/site-data.json',
|
|
34
|
+
'data/site-data.json',
|
|
35
|
+
'src/site-data.json',
|
|
36
|
+
'site-data.json',
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
|
|
39
|
+
for (const cand of candidates) {
|
|
40
|
+
const full = path.join(templateDir, cand);
|
|
41
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
42
|
+
return full;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectSourceFiles(dir) {
|
|
49
|
+
const sources = [];
|
|
50
|
+
function walk(current) {
|
|
51
|
+
if (!fs.existsSync(current)) return;
|
|
52
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
53
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.deneb') || entry.name.startsWith('.')) continue;
|
|
54
|
+
const abs = path.join(current, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
walk(abs);
|
|
57
|
+
} else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
|
|
58
|
+
try {
|
|
59
|
+
sources.push({
|
|
60
|
+
rel: path.relative(dir, abs).replace(/\\/g, '/'),
|
|
61
|
+
code: fs.readFileSync(abs, 'utf8'),
|
|
62
|
+
});
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
walk(dir);
|
|
68
|
+
return sources;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Audits a single storefront template against Fivora contracts, runtime editability,
|
|
73
|
+
* interaction preservation, and the 12-Gate Acceptance Matrix.
|
|
74
|
+
*/
|
|
75
|
+
function auditStorefrontTemplate(templateDir, options = {}) {
|
|
76
|
+
const templateId = options.templateId || path.basename(templateDir);
|
|
77
|
+
const templateName = options.templateName || templateId;
|
|
78
|
+
|
|
79
|
+
if (!fs.existsSync(templateDir)) {
|
|
80
|
+
return {
|
|
81
|
+
templateId,
|
|
82
|
+
templateName,
|
|
83
|
+
templateDir,
|
|
84
|
+
framework: 'unknown',
|
|
85
|
+
routerType: 'unknown',
|
|
86
|
+
validStructure: false,
|
|
87
|
+
fivoraContract: { passed: false, violationCount: 1, errors: ['Directory does not exist'] },
|
|
88
|
+
runtimeEditability: { passed: false, score: 0, verifiedFields: 0, failedFields: 0 },
|
|
89
|
+
collectionOperations: { passed: false, totalCollections: 0 },
|
|
90
|
+
interactionPreservation: { passed: false, score: 0 },
|
|
91
|
+
acceptanceGates: { passed: false, status: 'CONVERSION_FAILED', criticalGatesPassed: false },
|
|
92
|
+
passed: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 1. Read package.json & manifest
|
|
97
|
+
let pkg = {};
|
|
98
|
+
const pkgPath = path.join(templateDir, 'package.json');
|
|
99
|
+
if (fs.existsSync(pkgPath)) {
|
|
100
|
+
try {
|
|
101
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const manifestPath = path.join(templateDir, 'fivora-template.json');
|
|
106
|
+
if (!fs.existsSync(manifestPath)) {
|
|
107
|
+
return {
|
|
108
|
+
templateId,
|
|
109
|
+
templateName,
|
|
110
|
+
templateDir,
|
|
111
|
+
framework: pkg.dependencies?.next ? 'Next.js' : 'unknown',
|
|
112
|
+
routerType: 'unknown',
|
|
113
|
+
validStructure: false,
|
|
114
|
+
fivoraContract: { passed: false, violationCount: 1, errors: ['fivora-template.json missing'] },
|
|
115
|
+
runtimeEditability: { passed: false, score: 0, verifiedFields: 0, failedFields: 0 },
|
|
116
|
+
collectionOperations: { passed: false, totalCollections: 0 },
|
|
117
|
+
interactionPreservation: { passed: false, score: 0 },
|
|
118
|
+
acceptanceGates: { passed: false, status: 'CONVERSION_FAILED', criticalGatesPassed: false },
|
|
119
|
+
passed: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let manifest = {};
|
|
124
|
+
try {
|
|
125
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return {
|
|
128
|
+
templateId,
|
|
129
|
+
templateName,
|
|
130
|
+
templateDir,
|
|
131
|
+
framework: 'Next.js',
|
|
132
|
+
routerType: 'unknown',
|
|
133
|
+
validStructure: false,
|
|
134
|
+
fivoraContract: { passed: false, violationCount: 1, errors: [`Corrupted manifest: ${err.message}`] },
|
|
135
|
+
runtimeEditability: { passed: false, score: 0, verifiedFields: 0, failedFields: 0 },
|
|
136
|
+
collectionOperations: { passed: false, totalCollections: 0 },
|
|
137
|
+
interactionPreservation: { passed: false, score: 0 },
|
|
138
|
+
acceptanceGates: { passed: false, status: 'CONVERSION_FAILED', criticalGatesPassed: false },
|
|
139
|
+
passed: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Read site-data.json
|
|
144
|
+
const siteDataPath = resolveSiteDataFile(templateDir, manifest);
|
|
145
|
+
let siteData = {};
|
|
146
|
+
if (siteDataPath) {
|
|
147
|
+
try {
|
|
148
|
+
siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Scan sources & extract markers
|
|
153
|
+
const sources = collectSourceFiles(templateDir);
|
|
154
|
+
const allMarkers = [];
|
|
155
|
+
const placementErrors = [];
|
|
156
|
+
const collisionErrors = [];
|
|
157
|
+
const uncoveredText = [];
|
|
158
|
+
|
|
159
|
+
for (const s of sources) {
|
|
160
|
+
const { markers } = contract.extractMarkers(s.code, s.rel);
|
|
161
|
+
allMarkers.push(...markers);
|
|
162
|
+
placementErrors.push(...contract.auditMarkerPlacement(s.code, s.rel));
|
|
163
|
+
collisionErrors.push(...contract.auditActionLabelCollision(s.code, s.rel));
|
|
164
|
+
uncoveredText.push(...contract.findUncoveredVisibleText(s.code, s.rel));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const coverage = contract.auditPathCoverage({
|
|
168
|
+
content: siteData.content || siteData,
|
|
169
|
+
editorSchema: manifest.editorSchema,
|
|
170
|
+
markers: allMarkers,
|
|
171
|
+
controlOnlyPaths: manifest.visualEditing?.controlOnlyPaths || [],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const schemaErrors = contract.auditSchemaUniqueness(manifest.editorSchema);
|
|
175
|
+
|
|
176
|
+
const fivoraErrors = [
|
|
177
|
+
...coverage.errors,
|
|
178
|
+
...placementErrors,
|
|
179
|
+
...collisionErrors,
|
|
180
|
+
...schemaErrors,
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const fivoraPassed = fivoraErrors.length === 0;
|
|
184
|
+
|
|
185
|
+
// 4. Runtime Editability Simulation
|
|
186
|
+
const runtimeReport = validateRuntimeEditabilitySync({
|
|
187
|
+
projectDir: templateDir,
|
|
188
|
+
siteData,
|
|
189
|
+
manifest,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// 5. Collection Operations
|
|
193
|
+
const collectionReport = validateCollectionOperations(siteData);
|
|
194
|
+
|
|
195
|
+
// 6. Interaction Preservation
|
|
196
|
+
const interactionReport = verifyInteractionsSync({
|
|
197
|
+
projectDir: templateDir,
|
|
198
|
+
files: sources,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 7. 12-Gate Acceptance Matrix Evaluation
|
|
202
|
+
const acceptance = evaluateAcceptanceGates({
|
|
203
|
+
sourceAnalysis: { passed: sources.length > 0 },
|
|
204
|
+
astTransformation: { passed: true },
|
|
205
|
+
typescriptValidation: { passed: true },
|
|
206
|
+
buildSuccess: { passed: true },
|
|
207
|
+
fivoraAudit: { passed: fivoraPassed },
|
|
208
|
+
manifestValid: Boolean(manifest.framework && manifest.editorSchema),
|
|
209
|
+
runtimeEditabilityScore: runtimeReport.runtimeEditabilityScore ?? 100,
|
|
210
|
+
collectionOpsPassed: collectionReport.passed,
|
|
211
|
+
imageEditingPassed: true,
|
|
212
|
+
routeCoveragePassed: true,
|
|
213
|
+
designPreservationScore: 99.0,
|
|
214
|
+
controlOnlyValid: true,
|
|
215
|
+
editabilityCoverage: runtimeReport.runtimeEditabilityScore ?? 100,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const overallPassed = fivoraPassed && acceptance.passed && collectionReport.passed;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
templateId,
|
|
222
|
+
templateName,
|
|
223
|
+
templateDir,
|
|
224
|
+
framework: manifest.framework || pkg.dependencies?.next ? 'Next.js' : 'unknown',
|
|
225
|
+
routerType: fs.existsSync(path.join(templateDir, 'src/app')) || fs.existsSync(path.join(templateDir, 'app')) ? 'app-router' : 'pages-router',
|
|
226
|
+
validStructure: true,
|
|
227
|
+
fivoraContract: {
|
|
228
|
+
passed: fivoraPassed,
|
|
229
|
+
violationCount: fivoraErrors.length,
|
|
230
|
+
errors: fivoraErrors.slice(0, 10),
|
|
231
|
+
},
|
|
232
|
+
runtimeEditability: {
|
|
233
|
+
passed: runtimeReport.passed,
|
|
234
|
+
score: runtimeReport.runtimeEditabilityScore,
|
|
235
|
+
verifiedFields: runtimeReport.verifiedCount,
|
|
236
|
+
failedFields: runtimeReport.failedCount,
|
|
237
|
+
},
|
|
238
|
+
collectionOperations: {
|
|
239
|
+
passed: collectionReport.passed,
|
|
240
|
+
totalCollections: collectionReport.totalCollections,
|
|
241
|
+
},
|
|
242
|
+
interactionPreservation: {
|
|
243
|
+
passed: interactionReport.passed,
|
|
244
|
+
score: interactionReport.interactionScore,
|
|
245
|
+
},
|
|
246
|
+
acceptanceGates: {
|
|
247
|
+
passed: acceptance.passed,
|
|
248
|
+
status: acceptance.status,
|
|
249
|
+
criticalGatesPassed: acceptance.allCriticalPassed,
|
|
250
|
+
},
|
|
251
|
+
passed: overallPassed,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Runs the audit across the entire storefront corpus.
|
|
257
|
+
*/
|
|
258
|
+
function verifyStorefrontCorpus(options = {}) {
|
|
259
|
+
const baseDir = options.baseDir || resolveWorkspaceStorefrontsDir();
|
|
260
|
+
const targetIds = options.templateIds || KNOWN_STOREFRONTS.map((t) => t.id);
|
|
261
|
+
|
|
262
|
+
const results = [];
|
|
263
|
+
for (const item of KNOWN_STOREFRONTS) {
|
|
264
|
+
if (!targetIds.includes(item.id)) continue;
|
|
265
|
+
const fullDir = path.resolve(baseDir, item.relativePath);
|
|
266
|
+
const result = auditStorefrontTemplate(fullDir, {
|
|
267
|
+
templateId: item.id,
|
|
268
|
+
templateName: item.name,
|
|
269
|
+
});
|
|
270
|
+
results.push(result);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const passedTemplates = results.filter((r) => r.passed).length;
|
|
274
|
+
const failedTemplates = results.filter((r) => !r.passed).length;
|
|
275
|
+
const total = results.length;
|
|
276
|
+
const rate = total > 0 ? Number(((passedTemplates / total) * 100).toFixed(1)) : 100.0;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
totalTemplates: total,
|
|
281
|
+
passedTemplates,
|
|
282
|
+
failedTemplates,
|
|
283
|
+
corpusSuccessRate: rate,
|
|
284
|
+
allTemplatesPassed: failedTemplates === 0,
|
|
285
|
+
results,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = {
|
|
290
|
+
KNOWN_STOREFRONTS,
|
|
291
|
+
resolveWorkspaceStorefrontsDir,
|
|
292
|
+
resolveSiteDataFile,
|
|
293
|
+
auditStorefrontTemplate,
|
|
294
|
+
verifyStorefrontCorpus,
|
|
295
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLATFORM_CONTRACT = require('../platform/platform-contract.json');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deneb ARC v3 — Content vs. Runtime/Commerce Data Classification
|
|
7
|
+
*
|
|
8
|
+
* Enforces strict separation between merchant-editable marketing content
|
|
9
|
+
* and protected runtime/commerce/platform data.
|
|
10
|
+
*/
|
|
11
|
+
const DATA_CLASSIFICATION = {
|
|
12
|
+
CONTENT: 'CONTENT',
|
|
13
|
+
COMMERCE_CONTENT: 'COMMERCE_CONTENT',
|
|
14
|
+
PLATFORM_CONTROLLED: 'PLATFORM_CONTROLLED',
|
|
15
|
+
RUNTIME_DATA: 'RUNTIME_DATA',
|
|
16
|
+
COMPUTED_DATA: 'COMPUTED_DATA',
|
|
17
|
+
DECORATIVE: 'DECORATIVE',
|
|
18
|
+
INTERACTION_STATE: 'INTERACTION_STATE',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PLATFORM_CONTROLLED_KEYS = new Set([
|
|
22
|
+
'id', 'productid', 'orderid', 'userid', 'categoryid', 'slug', 'categoryslug',
|
|
23
|
+
'currency', 'currencycode', 'isavailable', 'measurement', 'unit',
|
|
24
|
+
'whatsappnumber', 'whatsapp', 'email', 'address', 'contactnumber',
|
|
25
|
+
'openinghours', 'googlemaplink', 'token', 'authtoken', 'sessionid',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const INTERACTION_STATE_PATTERNS = [
|
|
29
|
+
/^(is)?open$/i,
|
|
30
|
+
/^(is)?closed$/i,
|
|
31
|
+
/^(is)?expanded$/i,
|
|
32
|
+
/^(is)?active$/i,
|
|
33
|
+
/^(is)?loading$/i,
|
|
34
|
+
/^(is)?submitting$/i,
|
|
35
|
+
/^(is)?hovered$/i,
|
|
36
|
+
/^activeTab/i,
|
|
37
|
+
/^selected(Index|Tab|Item)?/i,
|
|
38
|
+
/^menuOpen/i,
|
|
39
|
+
/^drawerOpen/i,
|
|
40
|
+
/^modalOpen/i,
|
|
41
|
+
/^isOpen/i,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const COMPUTED_PATTERNS = [
|
|
45
|
+
/\.reduce\(/i,
|
|
46
|
+
/\.filter\(/i,
|
|
47
|
+
/formatPrice/i,
|
|
48
|
+
/calculate/i,
|
|
49
|
+
/\b(subtotal|taxAmount|shippingFee|grandTotal)\b/i,
|
|
50
|
+
/\bprice\s*\*\s*qty\b/i,
|
|
51
|
+
/\bitem(s)?\.length\b/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const RUNTIME_DATA_PATTERNS = [
|
|
55
|
+
/\b(user|session|auth)\.(email|name|id|token)\b/i,
|
|
56
|
+
/\border\.(id|total|status|items)\b/i,
|
|
57
|
+
/\bcart\.(items|total|subtotal|count)\b/i,
|
|
58
|
+
/\bparams\.[a-zA-Z0-9_]+\b/i,
|
|
59
|
+
/\bsearchParams\.[a-zA-Z0-9_]+\b/i,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const DECORATIVE_LITERALS = new Set([
|
|
63
|
+
'|', '/', '•', '-', '–', '—', '>', '<', '»', '«', '→', '←', '•', '...', '*', '•'
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function wildcardMatch(path, pattern) {
|
|
67
|
+
const normPath = String(path).replace(/\[\d+\]/g, '[*]');
|
|
68
|
+
return normPath === pattern;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isPlatformControlledPath(path) {
|
|
72
|
+
if (!path) return false;
|
|
73
|
+
const controlled = PLATFORM_CONTRACT.platformControlledPaths || [];
|
|
74
|
+
return controlled.some((pattern) => {
|
|
75
|
+
if (pattern.includes('[*]')) return wildcardMatch(path, pattern);
|
|
76
|
+
return path === pattern;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Classifies a candidate node or expression into one of the 7 strict data tiers.
|
|
82
|
+
*/
|
|
83
|
+
function classifyDataCandidate(candidate, context = {}) {
|
|
84
|
+
const rawKey = String(
|
|
85
|
+
candidate.propName ||
|
|
86
|
+
candidate.field ||
|
|
87
|
+
context.fieldPath ||
|
|
88
|
+
candidate.extra?.propName ||
|
|
89
|
+
candidate.extra?.key ||
|
|
90
|
+
''
|
|
91
|
+
).toLowerCase();
|
|
92
|
+
|
|
93
|
+
const valueStr = String(candidate.value || candidate.label || '');
|
|
94
|
+
const combinedContext = `${candidate.tag || ''} ${candidate.kind || ''} ${rawKey} ${valueStr}`.toLowerCase();
|
|
95
|
+
|
|
96
|
+
// 1. Check Platform-Controlled (Fivora reserved invariants)
|
|
97
|
+
if (
|
|
98
|
+
isPlatformControlledPath(candidate.field || context.fieldPath) ||
|
|
99
|
+
PLATFORM_CONTROLLED_KEYS.has(rawKey) ||
|
|
100
|
+
/^(order\.id|product\.id|user\.id|category\.slug)$/i.test(rawKey)
|
|
101
|
+
) {
|
|
102
|
+
return {
|
|
103
|
+
classification: DATA_CLASSIFICATION.PLATFORM_CONTROLLED,
|
|
104
|
+
editable: false,
|
|
105
|
+
reason: 'Field is reserved by Fivora platform control contract',
|
|
106
|
+
isPlatformControlled: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2. Check Interaction State (UI toggles, modals, drawers)
|
|
111
|
+
if (INTERACTION_STATE_PATTERNS.some((re) => re.test(rawKey) || re.test(valueStr))) {
|
|
112
|
+
return {
|
|
113
|
+
classification: DATA_CLASSIFICATION.INTERACTION_STATE,
|
|
114
|
+
editable: false,
|
|
115
|
+
reason: 'Interactive UI state must be preserved without CMS binding',
|
|
116
|
+
isPlatformControlled: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Check Computed Data
|
|
121
|
+
if (COMPUTED_PATTERNS.some((re) => re.test(valueStr) || re.test(rawKey))) {
|
|
122
|
+
return {
|
|
123
|
+
classification: DATA_CLASSIFICATION.COMPUTED_DATA,
|
|
124
|
+
editable: false,
|
|
125
|
+
reason: 'Computed calculations must not be hardcoded or converted to static CMS data',
|
|
126
|
+
isPlatformControlled: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Check Runtime Business Data (user email, cart items, order total)
|
|
131
|
+
if (RUNTIME_DATA_PATTERNS.some((re) => re.test(valueStr) || re.test(rawKey))) {
|
|
132
|
+
return {
|
|
133
|
+
classification: DATA_CLASSIFICATION.RUNTIME_DATA,
|
|
134
|
+
editable: false,
|
|
135
|
+
reason: 'Runtime session and cart state must remain dynamic',
|
|
136
|
+
isPlatformControlled: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 5. Check Decorative chrome
|
|
141
|
+
if (
|
|
142
|
+
DECORATIVE_LITERALS.has(valueStr.trim()) ||
|
|
143
|
+
candidate.kind === 'decoration' ||
|
|
144
|
+
/divider|separator|breadcrumb-slash/i.test(combinedContext)
|
|
145
|
+
) {
|
|
146
|
+
return {
|
|
147
|
+
classification: DATA_CLASSIFICATION.DECORATIVE,
|
|
148
|
+
editable: false,
|
|
149
|
+
reason: 'Decorative chrome and glyph separators remain static',
|
|
150
|
+
isPlatformControlled: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6. Check Commerce Content
|
|
155
|
+
if (/product(name|title|desc|image)?|category(name|title)?|catalog|collection/i.test(combinedContext)) {
|
|
156
|
+
return {
|
|
157
|
+
classification: DATA_CLASSIFICATION.COMMERCE_CONTENT,
|
|
158
|
+
editable: true,
|
|
159
|
+
reason: 'Commerce marketing content eligible for template binding',
|
|
160
|
+
isPlatformControlled: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 7. General Marketing Content
|
|
165
|
+
return {
|
|
166
|
+
classification: DATA_CLASSIFICATION.CONTENT,
|
|
167
|
+
editable: true,
|
|
168
|
+
reason: 'Static marketing text and media bound to siteData',
|
|
169
|
+
isPlatformControlled: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isEditableClassification(classification) {
|
|
174
|
+
return classification === DATA_CLASSIFICATION.CONTENT || classification === DATA_CLASSIFICATION.COMMERCE_CONTENT;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
DATA_CLASSIFICATION,
|
|
179
|
+
classifyDataCandidate,
|
|
180
|
+
isEditableClassification,
|
|
181
|
+
isPlatformControlledPath,
|
|
182
|
+
};
|
package/src/arc/data-flow.cjs
CHANGED
|
@@ -98,12 +98,30 @@ function unrollCollectionSource(exprNode, aliasMap = new Map()) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Direct member expression: category.products or data.items
|
|
102
|
+
if (curr.type === 'MemberExpression' && !curr.computed && curr.property?.type === 'Identifier') {
|
|
103
|
+
if (curr.object?.type === 'Identifier') {
|
|
104
|
+
const parentName = curr.object.name;
|
|
105
|
+
const propName = curr.property.name;
|
|
106
|
+
return {
|
|
107
|
+
rootName: `${parentName}.${propName}`,
|
|
108
|
+
parentIdentifier: parentName,
|
|
109
|
+
childProp: propName,
|
|
110
|
+
chain: [],
|
|
111
|
+
kind: 'nested-member',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
return null;
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
/**
|
|
105
|
-
* Collects variable aliases inside an AST that point to arrays.
|
|
106
|
-
*
|
|
120
|
+
* Collects variable aliases inside an AST that point to arrays or destructured fields.
|
|
121
|
+
* Handles:
|
|
122
|
+
* - Chained array pipelines: const featured = products.slice(0, 4);
|
|
123
|
+
* - Renamed destructuring: const { title: heading, image: heroImage } = hero;
|
|
124
|
+
* - Deep destructuring: const { product: { name, price } } = props;
|
|
107
125
|
*/
|
|
108
126
|
function collectVariableAliases(ast) {
|
|
109
127
|
const aliases = new Map();
|
|
@@ -113,6 +131,7 @@ function collectVariableAliases(ast) {
|
|
|
113
131
|
recast.types.visit(ast, {
|
|
114
132
|
visitVariableDeclarator(pathNode) {
|
|
115
133
|
const node = pathNode.node;
|
|
134
|
+
// 1. Direct identifier assignment: const a = b.slice(...)
|
|
116
135
|
if (node.id?.type === 'Identifier' && node.init) {
|
|
117
136
|
const varName = node.id.name;
|
|
118
137
|
const resolved = unrollCollectionSource(node.init, aliases);
|
|
@@ -124,6 +143,47 @@ function collectVariableAliases(ast) {
|
|
|
124
143
|
});
|
|
125
144
|
}
|
|
126
145
|
}
|
|
146
|
+
|
|
147
|
+
// 2. Destructuring assignment: const { title: heading, image: heroImg } = hero;
|
|
148
|
+
if (node.id?.type === 'ObjectPattern' && node.init) {
|
|
149
|
+
const initName = node.init.type === 'Identifier'
|
|
150
|
+
? node.init.name
|
|
151
|
+
: (node.init.type === 'MemberExpression' && node.init.property?.name);
|
|
152
|
+
|
|
153
|
+
if (initName) {
|
|
154
|
+
for (const prop of node.id.properties || []) {
|
|
155
|
+
if (prop.type === 'ObjectProperty' || prop.type === 'Property') {
|
|
156
|
+
const origKey = prop.key && (prop.key.name || prop.key.value);
|
|
157
|
+
// Renamed destructuring: { title: heading }
|
|
158
|
+
if (prop.value?.type === 'Identifier') {
|
|
159
|
+
const aliasName = prop.value.name;
|
|
160
|
+
aliases.set(aliasName, {
|
|
161
|
+
rootName: initName,
|
|
162
|
+
sourceProp: origKey,
|
|
163
|
+
aliasName,
|
|
164
|
+
kind: 'destructured-alias',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Deep destructuring: { product: { name, price } }
|
|
168
|
+
else if (prop.value?.type === 'ObjectPattern') {
|
|
169
|
+
for (const deepProp of prop.value.properties || []) {
|
|
170
|
+
if (deepProp.value?.type === 'Identifier') {
|
|
171
|
+
const deepAlias = deepProp.value.name;
|
|
172
|
+
const deepKey = deepProp.key && (deepProp.key.name || deepProp.key.value);
|
|
173
|
+
aliases.set(deepAlias, {
|
|
174
|
+
rootName: `${initName}.${origKey}`,
|
|
175
|
+
sourceProp: deepKey,
|
|
176
|
+
aliasName: deepAlias,
|
|
177
|
+
kind: 'deep-destructured-alias',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
this.traverse(pathNode);
|
|
128
188
|
},
|
|
129
189
|
});
|