@deneb-ui/cli 2.0.51 → 2.0.53
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 +12 -0
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +96 -11
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +127 -28
- package/src/arc/learning.cjs +19 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +2 -1
- package/src/arc/printer.cjs +9 -0
- package/src/arc/residual.cjs +400 -0
- package/src/arc/scanner.cjs +46 -2
- package/src/arc/semantic.cjs +8 -4
- package/src/arc/transformer.cjs +321 -28
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
package/src/arc/index.cjs
CHANGED
|
@@ -14,10 +14,11 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { ARC_NAME, ARC_VERSION, SCHEMA_VERSION, ENGINE_ID } = require('./version.cjs');
|
|
16
16
|
const { walkFiles, isJsxFile, rel, copyFilePreserve, writeJson, readJsonSafe, findFirstExisting } = require('./fs-utils.cjs');
|
|
17
|
-
const { scanProject, buildDependencyGraph, inferOwnerScope } = require('./scanner.cjs');
|
|
17
|
+
const { scanProject, buildDependencyGraph, inferOwnerScope, resolvePageFileOnDisk } = require('./scanner.cjs');
|
|
18
18
|
const { analyzeFile, collectDesignSnapshot } = require('./semantic.cjs');
|
|
19
19
|
const { planTransformations } = require('./planner.cjs');
|
|
20
20
|
const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, resolveSiteDataRuntimeSpecifier, rewriteRecursiveSiteDataContext, ensureJsonModule, sanitizeContradictoryMarkersInSource } = require('./transformer.cjs');
|
|
21
|
+
const { applyResidualPass } = require('./residual.cjs');
|
|
21
22
|
const { parseSource } = require('./ast.cjs');
|
|
22
23
|
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
|
|
23
24
|
const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
|
|
@@ -34,6 +35,11 @@ const {
|
|
|
34
35
|
auditPageCoverage,
|
|
35
36
|
auditPreviewRuntime,
|
|
36
37
|
findUncoveredVisibleText,
|
|
38
|
+
auditEmptyStateSource,
|
|
39
|
+
auditSelectOptions,
|
|
40
|
+
auditListBounds,
|
|
41
|
+
auditStaticMarkerAuthorship,
|
|
42
|
+
auditRouteOwnedMarkerCoverage,
|
|
37
43
|
} = require('./fivora-contract.cjs');
|
|
38
44
|
const printer = require('./printer.cjs');
|
|
39
45
|
const { classifyComponents } = require('./component-registry.cjs');
|
|
@@ -52,6 +58,7 @@ function parseArcOptions(raw = {}) {
|
|
|
52
58
|
json: Boolean(raw.json),
|
|
53
59
|
aiEnabled: Boolean(raw.aiEnabled),
|
|
54
60
|
aiDryRun: Boolean(raw.aiDryRun),
|
|
61
|
+
strict: Boolean(raw.strict),
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -151,6 +158,8 @@ function auditFivoraContract({ profile, siteData, manifest, inventory }) {
|
|
|
151
158
|
const placement = [];
|
|
152
159
|
const collisions = [];
|
|
153
160
|
const uncoveredText = [];
|
|
161
|
+
const emptyState = [];
|
|
162
|
+
const staticAuthorship = [];
|
|
154
163
|
const allMarkers = [];
|
|
155
164
|
|
|
156
165
|
for (const source of inventory.sources) {
|
|
@@ -159,6 +168,8 @@ function auditFivoraContract({ profile, siteData, manifest, inventory }) {
|
|
|
159
168
|
placement.push(...auditMarkerPlacement(source.code, source.rel));
|
|
160
169
|
collisions.push(...auditActionLabelCollision(source.code, source.rel));
|
|
161
170
|
uncoveredText.push(...findUncoveredVisibleText(source.code, source.rel));
|
|
171
|
+
emptyState.push(...auditEmptyStateSource(source.code, source.rel));
|
|
172
|
+
staticAuthorship.push(...auditStaticMarkerAuthorship(source.code, source.rel));
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
const coverage = auditPathCoverage({
|
|
@@ -177,19 +188,34 @@ function auditFivoraContract({ profile, siteData, manifest, inventory }) {
|
|
|
177
188
|
...coverage.errors,
|
|
178
189
|
...placement,
|
|
179
190
|
...collisions,
|
|
191
|
+
...emptyState,
|
|
192
|
+
...staticAuthorship,
|
|
180
193
|
...auditSchemaUniqueness(manifest.editorSchema),
|
|
194
|
+
...auditSelectOptions(manifest.editorSchema, manifest.pages),
|
|
195
|
+
...auditListBounds(manifest.editorSchema, siteData.content),
|
|
181
196
|
...auditPageCoverage({
|
|
182
197
|
pages: manifest.pages,
|
|
183
198
|
routeFiles,
|
|
184
199
|
pageMarkersByFile: inventory.pageKeysByFile,
|
|
185
200
|
}),
|
|
201
|
+
...auditRouteOwnedMarkerCoverage({
|
|
202
|
+
editorSchema: manifest.editorSchema,
|
|
203
|
+
pages: manifest.pages,
|
|
204
|
+
markers: allMarkers,
|
|
205
|
+
controlOnlyPaths: manifest.visualEditing?.controlOnlyPaths || [],
|
|
206
|
+
}),
|
|
186
207
|
...auditPreviewRuntime(inventory.sources.map((source) => source.code)),
|
|
187
208
|
];
|
|
188
209
|
|
|
189
210
|
return {
|
|
190
|
-
passed: errors.length === 0,
|
|
211
|
+
passed: errors.length === 0 && uncoveredText.length === 0,
|
|
191
212
|
errors: [...new Set(errors)],
|
|
192
213
|
uncoveredVisibleText: uncoveredText,
|
|
214
|
+
emptyStatePassed: emptyState.length === 0,
|
|
215
|
+
fieldMarkers: coverage.fieldMarkers,
|
|
216
|
+
listMarkers: coverage.listMarkers,
|
|
217
|
+
itemMarkers: coverage.itemMarkers,
|
|
218
|
+
pathCoverage: coverage,
|
|
193
219
|
};
|
|
194
220
|
}
|
|
195
221
|
|
|
@@ -230,6 +256,21 @@ function analyzeProjectFiles(profile, graph) {
|
|
|
230
256
|
return analyses;
|
|
231
257
|
}
|
|
232
258
|
|
|
259
|
+
function mergeDetectedPages(profile, detectedPages) {
|
|
260
|
+
if (!Array.isArray(detectedPages) || !detectedPages.length) return;
|
|
261
|
+
const scannedIds = new Set(profile.routes.map((route) => route.id));
|
|
262
|
+
const scannedRoutes = new Set(profile.routes.map((route) => route.route));
|
|
263
|
+
for (const page of detectedPages) {
|
|
264
|
+
if (!page || !page.id) continue;
|
|
265
|
+
if (scannedIds.has(page.id) || scannedRoutes.has(page.route)) continue;
|
|
266
|
+
const file = resolvePageFileOnDisk(profile.root, page);
|
|
267
|
+
if (!file) continue;
|
|
268
|
+
profile.routes.push({ ...page, file });
|
|
269
|
+
scannedIds.add(page.id);
|
|
270
|
+
scannedRoutes.add(page.route);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
233
274
|
async function runDenebArcAsync(projectDir, projectName, options = {}) {
|
|
234
275
|
const opts = parseArcOptions(options);
|
|
235
276
|
const runId = createRunId();
|
|
@@ -238,15 +279,7 @@ async function runDenebArcAsync(projectDir, projectName, options = {}) {
|
|
|
238
279
|
printer.printBanner(opts.dryRun ? 'dry-run' : opts.explain ? 'explain' : 'run');
|
|
239
280
|
|
|
240
281
|
const profile = scanProject(projectDir);
|
|
241
|
-
|
|
242
|
-
const scannedIds = new Set(profile.routes.map((r) => r.id));
|
|
243
|
-
for (const page of opts.detectedPages) {
|
|
244
|
-
if (page && page.id && !scannedIds.has(page.id)) {
|
|
245
|
-
profile.routes.push(page);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
282
|
+
mergeDetectedPages(profile, opts.detectedPages);
|
|
250
283
|
printer.printProfile(profile);
|
|
251
284
|
const graph = buildDependencyGraph(profile);
|
|
252
285
|
const analyses = analyzeProjectFiles(profile, graph);
|
|
@@ -379,14 +412,7 @@ function runDenebArcSync(projectDir, projectName, options = {}) {
|
|
|
379
412
|
printer.printBanner(opts.dryRun ? 'dry-run' : opts.explain ? 'explain' : 'run');
|
|
380
413
|
|
|
381
414
|
const profile = scanProject(projectDir);
|
|
382
|
-
|
|
383
|
-
const scannedIds = new Set(profile.routes.map((r) => r.id));
|
|
384
|
-
for (const page of opts.detectedPages) {
|
|
385
|
-
if (page && page.id && !scannedIds.has(page.id)) {
|
|
386
|
-
profile.routes.push(page);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
415
|
+
mergeDetectedPages(profile, opts.detectedPages);
|
|
390
416
|
|
|
391
417
|
printer.printProfile(profile);
|
|
392
418
|
const graph = buildDependencyGraph(profile);
|
|
@@ -545,6 +571,48 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
545
571
|
}
|
|
546
572
|
}
|
|
547
573
|
|
|
574
|
+
const residualFields = [];
|
|
575
|
+
const usedPaths = new Set(plan.usedPaths || []);
|
|
576
|
+
for (const relativeFile of profile.jsxFiles || []) {
|
|
577
|
+
const abs = path.join(projectDir, relativeFile);
|
|
578
|
+
if (!fs.existsSync(abs)) continue;
|
|
579
|
+
const original = fs.readFileSync(abs, 'utf8');
|
|
580
|
+
const analysis = analyses.find((item) => item.relativeFile === relativeFile);
|
|
581
|
+
const residual = applyResidualPass({
|
|
582
|
+
code: original,
|
|
583
|
+
file: relativeFile,
|
|
584
|
+
ownerScope: analysis?.ownerScope || inferOwnerScope(profile, graph, relativeFile),
|
|
585
|
+
usedPaths,
|
|
586
|
+
componentName: analysis?.componentMeta?.name,
|
|
587
|
+
role: analysis?.componentMeta?.role,
|
|
588
|
+
});
|
|
589
|
+
if (!residual.changed) continue;
|
|
590
|
+
try {
|
|
591
|
+
parseSource(residual.code, relativeFile);
|
|
592
|
+
} catch {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
backupFile(projectDir, backupDir, abs);
|
|
596
|
+
fs.writeFileSync(abs, residual.code, 'utf8');
|
|
597
|
+
if (!changedFiles.includes(relativeFile)) changedFiles.push(relativeFile);
|
|
598
|
+
afterFiles[relativeFile] = residual.code;
|
|
599
|
+
appliedCount += residual.applied || 0;
|
|
600
|
+
residualFields.push(...(residual.fields || []));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
for (const relativeFile of profile.jsxFiles || []) {
|
|
604
|
+
const abs = path.join(projectDir, relativeFile);
|
|
605
|
+
if (!fs.existsSync(abs)) continue;
|
|
606
|
+
const original = fs.readFileSync(abs, 'utf8');
|
|
607
|
+
const sanitized = sanitizeContradictoryMarkersInSource(original, relativeFile);
|
|
608
|
+
if (sanitized.updated && sanitized.code !== original) {
|
|
609
|
+
backupFile(projectDir, backupDir, abs);
|
|
610
|
+
fs.writeFileSync(abs, sanitized.code, 'utf8');
|
|
611
|
+
if (!changedFiles.includes(relativeFile)) changedFiles.push(relativeFile);
|
|
612
|
+
afterFiles[relativeFile] = sanitized.code;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
548
616
|
if (profile.framework === 'nextjs') {
|
|
549
617
|
const before = findNextConfig(projectDir);
|
|
550
618
|
if (before) backupFile(projectDir, backupDir, before.abs);
|
|
@@ -589,6 +657,8 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
589
657
|
existingSiteData: existing.siteData,
|
|
590
658
|
existingManifest: existing.manifest,
|
|
591
659
|
boundFieldPaths: markerInventory.fieldPaths,
|
|
660
|
+
boundListPaths: markerInventory.listPaths,
|
|
661
|
+
extraFields: residualFields,
|
|
592
662
|
markerRoutes: markerInventory.markerRoutes,
|
|
593
663
|
});
|
|
594
664
|
|
|
@@ -611,13 +681,6 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
611
681
|
const design = designPreservationScore(plan.files, afterFiles);
|
|
612
682
|
const alreadyEditable = analyses.filter((a) => a.alreadyEditable).length;
|
|
613
683
|
const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
|
|
614
|
-
const coverage = coverageMetrics({
|
|
615
|
-
analyses,
|
|
616
|
-
plan,
|
|
617
|
-
appliedCount,
|
|
618
|
-
skippedDynamic,
|
|
619
|
-
alreadyEditable,
|
|
620
|
-
});
|
|
621
684
|
|
|
622
685
|
const fivoraAudit = auditFivoraContract({
|
|
623
686
|
profile,
|
|
@@ -626,11 +689,24 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
626
689
|
inventory: collectMarkerInventory(projectDir, profile, graph),
|
|
627
690
|
});
|
|
628
691
|
|
|
692
|
+
const coverage = coverageMetrics({
|
|
693
|
+
analyses,
|
|
694
|
+
plan,
|
|
695
|
+
appliedCount,
|
|
696
|
+
skippedDynamic,
|
|
697
|
+
alreadyEditable,
|
|
698
|
+
content: dataBundle.siteData.content,
|
|
699
|
+
controlOnlyPaths: dataBundle.manifest.visualEditing?.controlOnlyPaths || [],
|
|
700
|
+
pathCoverage: fivoraAudit.pathCoverage,
|
|
701
|
+
uncoveredVisibleText: fivoraAudit.uncoveredVisibleText,
|
|
702
|
+
});
|
|
703
|
+
|
|
629
704
|
const validation = {
|
|
630
705
|
syntaxPassed,
|
|
631
706
|
fivoraContractPassed: fivoraAudit.passed,
|
|
632
707
|
fivoraContractErrors: fivoraAudit.errors,
|
|
633
708
|
uncoveredVisibleText: fivoraAudit.uncoveredVisibleText.length,
|
|
709
|
+
emptyStatePassed: fivoraAudit.emptyStatePassed !== false,
|
|
634
710
|
contractPassed: contracts.contractPassed,
|
|
635
711
|
orphans: contracts.orphans.length,
|
|
636
712
|
missingSchema: contracts.missingSchema.length,
|
|
@@ -638,9 +714,12 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
638
714
|
staticAncestorCollisions: contracts.staticAncestorCollisions,
|
|
639
715
|
astFailures: astResults.filter((r) => !r.passed),
|
|
640
716
|
transformFailures,
|
|
717
|
+
designPreservation: design.score,
|
|
641
718
|
};
|
|
642
719
|
|
|
643
|
-
const criticalFailure = !syntaxPassed || contracts.actionCollisions > 0 && appliedCount === 0;
|
|
720
|
+
const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0);
|
|
721
|
+
const contractFailure = !fivoraAudit.passed || fivoraAudit.uncoveredVisibleText.length > 0;
|
|
722
|
+
const designFailure = design.score < 98 && design.total > 0;
|
|
644
723
|
let outcome = 'success';
|
|
645
724
|
if (criticalFailure) {
|
|
646
725
|
restoreBackup(projectDir, backupDir);
|
|
@@ -653,6 +732,21 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
653
732
|
}
|
|
654
733
|
outcome = 'rolled-back';
|
|
655
734
|
printer.printRollback(validation.astFailures[0]?.error || 'Critical validation failed');
|
|
735
|
+
} else if (opts.strict && (contractFailure || designFailure)) {
|
|
736
|
+
restoreBackup(projectDir, backupDir);
|
|
737
|
+
for (const created of createdDuringRun) {
|
|
738
|
+
try {
|
|
739
|
+
if (fs.existsSync(created)) fs.rmSync(created, { force: true });
|
|
740
|
+
} catch {
|
|
741
|
+
// ignore
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
outcome = 'rolled-back';
|
|
745
|
+
printer.printRollback('Strict Fivora contract failed');
|
|
746
|
+
} else if (contractFailure) {
|
|
747
|
+
outcome = 'contract-failed';
|
|
748
|
+
} else if (designFailure) {
|
|
749
|
+
outcome = 'design-regression';
|
|
656
750
|
}
|
|
657
751
|
|
|
658
752
|
printer.printValidation(validation, coverage, design);
|
|
@@ -686,7 +780,10 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
686
780
|
validation: {
|
|
687
781
|
syntaxPassed,
|
|
688
782
|
contractPassed: contracts.contractPassed,
|
|
689
|
-
|
|
783
|
+
fivoraContractPassed: fivoraAudit.passed,
|
|
784
|
+
emptyStatePassed: fivoraAudit.emptyStatePassed !== false,
|
|
785
|
+
uncoveredVisibleText: fivoraAudit.uncoveredVisibleText.length,
|
|
786
|
+
visualPassed: design.score >= 98,
|
|
690
787
|
idempotencyPassed: true,
|
|
691
788
|
},
|
|
692
789
|
outcome,
|
|
@@ -749,6 +846,8 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
749
846
|
if (outcome === 'success') {
|
|
750
847
|
printer.printSuccess();
|
|
751
848
|
printer.printDeveloperNextSteps();
|
|
849
|
+
} else if (outcome === 'contract-failed' || outcome === 'design-regression') {
|
|
850
|
+
printer.printContractFailed();
|
|
752
851
|
}
|
|
753
852
|
return result;
|
|
754
853
|
}
|
package/src/arc/learning.cjs
CHANGED
|
@@ -34,6 +34,7 @@ function recordExperience({ projectDir, profile, plan, validation, outcome, tele
|
|
|
34
34
|
if (telemetry && telemetry !== 'off') {
|
|
35
35
|
// Network upload is intentionally unimplemented. Local persistence only.
|
|
36
36
|
}
|
|
37
|
+
const learningOutcome = honestOutcome(validation, outcome);
|
|
37
38
|
const records = [];
|
|
38
39
|
for (const file of plan.files || []) {
|
|
39
40
|
for (const t of file.transformations || []) {
|
|
@@ -51,10 +52,12 @@ function recordExperience({ projectDir, profile, plan, validation, outcome, tele
|
|
|
51
52
|
typecheckPassed: validation.typecheckPassed,
|
|
52
53
|
buildPassed: validation.buildPassed,
|
|
53
54
|
contractPassed: Boolean(validation.contractPassed),
|
|
55
|
+
fivoraContractPassed: Boolean(validation.fivoraContractPassed),
|
|
56
|
+
emptyStatePassed: validation.emptyStatePassed !== false,
|
|
54
57
|
visualPassed: validation.visualPassed,
|
|
55
58
|
idempotencyPassed: validation.idempotencyPassed,
|
|
56
59
|
},
|
|
57
|
-
outcome,
|
|
60
|
+
outcome: learningOutcome,
|
|
58
61
|
anonymizedFeatures: {
|
|
59
62
|
tag: t.tag,
|
|
60
63
|
operation: t.operation,
|
|
@@ -85,6 +88,20 @@ function recordExperience({ projectDir, profile, plan, validation, outcome, tele
|
|
|
85
88
|
return records;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
function honestOutcome(validation, outcome) {
|
|
92
|
+
if (outcome === 'rolled-back') return 'rolled-back';
|
|
93
|
+
const passed =
|
|
94
|
+
validation &&
|
|
95
|
+
validation.syntaxPassed !== false &&
|
|
96
|
+
validation.contractPassed !== false &&
|
|
97
|
+
validation.fivoraContractPassed !== false &&
|
|
98
|
+
validation.emptyStatePassed !== false &&
|
|
99
|
+
!(validation.uncoveredVisibleText > 0);
|
|
100
|
+
if (!passed) return 'failure';
|
|
101
|
+
if (outcome && outcome !== 'success' && outcome !== 'dry-run') return 'failure';
|
|
102
|
+
return outcome === 'dry-run' ? 'dry-run' : 'success';
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
function loadFingerprintBoost(fingerprint) {
|
|
89
106
|
if (!fingerprint) {
|
|
90
107
|
return { boost: 0, skip: false, state: null };
|
|
@@ -236,6 +253,7 @@ module.exports = {
|
|
|
236
253
|
registryArchitecture,
|
|
237
254
|
redactSecrets,
|
|
238
255
|
promoteState,
|
|
256
|
+
honestOutcome,
|
|
239
257
|
localStorePath,
|
|
240
258
|
fingerprintStorePath,
|
|
241
259
|
RULE_STATES,
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -11,6 +11,43 @@ const {
|
|
|
11
11
|
wildcardPath,
|
|
12
12
|
} = require('./fivora-contract.cjs');
|
|
13
13
|
|
|
14
|
+
function pruneUnboundLeaves(content, isBound) {
|
|
15
|
+
function walk(node, path) {
|
|
16
|
+
if (Array.isArray(node)) {
|
|
17
|
+
node.forEach((item, index) => {
|
|
18
|
+
if (item && typeof item === 'object') walk(item, `${path}[${index}]`);
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!isPlainObject(node)) return;
|
|
23
|
+
for (const key of Object.keys(node)) {
|
|
24
|
+
const next = path ? `${path}.${key}` : key;
|
|
25
|
+
const value = node[key];
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const listBound =
|
|
28
|
+
isBound(next) || isBound(`${next}[0]`) || isBound(`${next}[*]`);
|
|
29
|
+
if (!listBound && !isAllowedControlOnly(next)) {
|
|
30
|
+
delete node[key];
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
walk(value, next);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (isPlainObject(value)) {
|
|
37
|
+
walk(value, next);
|
|
38
|
+
if (Object.keys(value).length === 0 && !isBound(next) && !isAllowedControlOnly(next)) {
|
|
39
|
+
delete node[key];
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!isBound(next) && !isAllowedControlOnly(next)) {
|
|
44
|
+
delete node[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(content, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function setDeep(target, pathStr, value) {
|
|
15
52
|
const parts = String(pathStr).split('.').filter(Boolean);
|
|
16
53
|
let curr = target;
|
|
@@ -254,11 +291,32 @@ function baseContent(projectName, routes) {
|
|
|
254
291
|
|
|
255
292
|
/**
|
|
256
293
|
* Fivora strict mode requires every concrete site-data field to either render a
|
|
257
|
-
* data-preview-field-path marker or be declared control-only.
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* instead of being silently shipped as an ingest failure.
|
|
294
|
+
* data-preview-field-path marker or be declared control-only. Control-only is
|
|
295
|
+
* reserved for ids, internal flags, and the unrendered merchant baseline — not
|
|
296
|
+
* as a dump for fields ARC planned but failed to bind.
|
|
261
297
|
*/
|
|
298
|
+
const BASELINE_CONTROL_ONLY = /^(common\.(websiteTitle|shortDescription|logoUrl|headerCtaLabel|copyright|navLabels(\.[^.]+)?|business(\.[^.]+)*))$/;
|
|
299
|
+
const SYSTEM_FIELD = /(^|\.)(id|key|slug|internalId|sku|_id)$/i;
|
|
300
|
+
|
|
301
|
+
function slimListItems(items, itemFields) {
|
|
302
|
+
const keys = (itemFields || []).map((field) => field.key).filter(Boolean);
|
|
303
|
+
if (!keys.length || !Array.isArray(items)) return items || [];
|
|
304
|
+
return items.map((item) => {
|
|
305
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const key of keys) {
|
|
308
|
+
if (!(key in item)) continue;
|
|
309
|
+
if (Array.isArray(item[key])) continue;
|
|
310
|
+
out[key] = item[key];
|
|
311
|
+
}
|
|
312
|
+
return Object.keys(out).length ? out : item;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isAllowedControlOnly(path) {
|
|
317
|
+
return BASELINE_CONTROL_ONLY.test(path) || SYSTEM_FIELD.test(path);
|
|
318
|
+
}
|
|
319
|
+
|
|
262
320
|
function computeControlOnlyPaths(content, boundPaths, declared = []) {
|
|
263
321
|
const inventory = enumerateContentPaths(content);
|
|
264
322
|
const bound = new Set([...(boundPaths || [])].map(wildcardPath));
|
|
@@ -266,9 +324,7 @@ function computeControlOnlyPaths(content, boundPaths, declared = []) {
|
|
|
266
324
|
|
|
267
325
|
for (const declaredPath of declared) {
|
|
268
326
|
const canonical = canonicalizeMarkerPath(declaredPath);
|
|
269
|
-
if (!canonical) continue;
|
|
270
|
-
// Drop stale declarations that no longer exist in content; the platform
|
|
271
|
-
// rejects controlOnlyPaths entries it cannot resolve.
|
|
327
|
+
if (!canonical || !isAllowedControlOnly(canonical)) continue;
|
|
272
328
|
const known = [...inventory.fieldPatterns, ...inventory.concreteFields].some(
|
|
273
329
|
(path) => wildcardPath(path) === wildcardPath(canonical)
|
|
274
330
|
);
|
|
@@ -276,7 +332,8 @@ function computeControlOnlyPaths(content, boundPaths, declared = []) {
|
|
|
276
332
|
}
|
|
277
333
|
|
|
278
334
|
for (const path of inventory.concreteFields) {
|
|
279
|
-
if (
|
|
335
|
+
if (bound.has(wildcardPath(path))) continue;
|
|
336
|
+
if (isAllowedControlOnly(path)) controlOnly.add(path);
|
|
280
337
|
}
|
|
281
338
|
|
|
282
339
|
return [...controlOnly].sort();
|
|
@@ -314,6 +371,8 @@ function buildSiteDataAndManifest({
|
|
|
314
371
|
existingSiteData,
|
|
315
372
|
existingManifest,
|
|
316
373
|
boundFieldPaths = [],
|
|
374
|
+
boundListPaths = [],
|
|
375
|
+
extraFields = [],
|
|
317
376
|
markerRoutes = {},
|
|
318
377
|
}) {
|
|
319
378
|
const routes = (profile.routes && profile.routes.length ? profile.routes : [{ id: 'home', label: 'Home', route: '/', required: true }])
|
|
@@ -329,7 +388,15 @@ function buildSiteDataAndManifest({
|
|
|
329
388
|
// Recipes may hint schema shape, but must not dump another storefront's content
|
|
330
389
|
// into an unrelated project. Extracted values always win.
|
|
331
390
|
|
|
332
|
-
const plannedFields = collectFieldsFromPlan(plan);
|
|
391
|
+
const plannedFields = [...collectFieldsFromPlan(plan), ...(extraFields || [])];
|
|
392
|
+
const boundSet = new Set(
|
|
393
|
+
[...(boundFieldPaths || []), ...(boundListPaths || [])].map((path) => wildcardPath(path))
|
|
394
|
+
);
|
|
395
|
+
function isBound(path) {
|
|
396
|
+
if (!path) return false;
|
|
397
|
+
const wild = wildcardPath(path);
|
|
398
|
+
return boundSet.has(wild) || [...boundSet].some((marker) => wildcardPath(marker) === wild);
|
|
399
|
+
}
|
|
333
400
|
|
|
334
401
|
// A second `init` run re-reads already-transformed sources, where the former
|
|
335
402
|
// literals are now site-data expressions and therefore no longer detectable.
|
|
@@ -373,8 +440,15 @@ function buildSiteDataAndManifest({
|
|
|
373
440
|
|
|
374
441
|
for (const field of plannedFields) {
|
|
375
442
|
if (field.type === 'list') {
|
|
376
|
-
|
|
377
|
-
|
|
443
|
+
if (!isBound(field.path) && !isBound(`${field.path}[0]`) && !isBound(`${field.path}[*]`)) continue;
|
|
444
|
+
const value = slimListItems(field.value ?? [], field.itemFields);
|
|
445
|
+
setDeep(content, field.path, value);
|
|
446
|
+
upsertSchemaList(editorSections, field.path, field.itemFields, value);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (!isBound(field.path) && !isAllowedControlOnly(field.path)) continue;
|
|
450
|
+
if (!isBound(field.path) && isAllowedControlOnly(field.path)) {
|
|
451
|
+
setDeep(content, field.path, field.value ?? '');
|
|
378
452
|
continue;
|
|
379
453
|
}
|
|
380
454
|
setDeep(content, field.path, field.value ?? '');
|
|
@@ -387,12 +461,15 @@ function buildSiteDataAndManifest({
|
|
|
387
461
|
// Planned extracted values should win over empty recipe defaults when existing is absent,
|
|
388
462
|
// but never erase merchant-configured existing values.
|
|
389
463
|
for (const field of plannedFields) {
|
|
464
|
+
if (!isBound(field.path) && field.type !== 'list' && !isAllowedControlOnly(field.path)) continue;
|
|
390
465
|
const existingVal = getDeep(existingSiteData.content, field.path);
|
|
391
466
|
if (existingVal !== undefined) setDeep(content, field.path, existingVal);
|
|
392
467
|
else if (field.value !== undefined) setDeep(content, field.path, field.value);
|
|
393
468
|
}
|
|
394
469
|
}
|
|
395
470
|
|
|
471
|
+
pruneUnboundLeaves(content, isBound);
|
|
472
|
+
|
|
396
473
|
const siteData = {
|
|
397
474
|
denebVersion: existingSiteData?.denebVersion || undefined,
|
|
398
475
|
arcVersion: ARC_VERSION,
|
|
@@ -503,4 +580,5 @@ module.exports = {
|
|
|
503
580
|
upsertSchemaList,
|
|
504
581
|
enrichSchemasFromContent,
|
|
505
582
|
isListActionCtaKey,
|
|
583
|
+
isAllowedControlOnly,
|
|
506
584
|
};
|
package/src/arc/planner.cjs
CHANGED
|
@@ -189,7 +189,8 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
189
189
|
type: typeof sample[k] === 'number' ? 'number' : /image|photo|avatar/i.test(k) ? 'image' : /url|link/i.test(k) ? 'url' : 'text',
|
|
190
190
|
}));
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
transform.hasComponentRef = Boolean(extra.hasComponentRef);
|
|
193
|
+
if (!transform.itemFields.length || !extra.objectItems) transform.decision = 'skip';
|
|
193
194
|
} else {
|
|
194
195
|
transform.field = buildFieldPath({
|
|
195
196
|
scope,
|
package/src/arc/printer.cjs
CHANGED
|
@@ -84,9 +84,12 @@ function printValidation(validation, coverage, design) {
|
|
|
84
84
|
else warn('AST validation reported parse issues');
|
|
85
85
|
if (validation.contractPassed) ok('editable contracts');
|
|
86
86
|
else warn('editable contract issues detected');
|
|
87
|
+
if (validation.fivoraContractPassed) ok('Fivora strict contract');
|
|
88
|
+
else warn('Fivora strict contract failed — package is not upload-ready');
|
|
87
89
|
ok('manifest');
|
|
88
90
|
if (validation.idempotencyPassed !== false) ok('idempotency');
|
|
89
91
|
console.log('');
|
|
92
|
+
console.log(` Visual coverage: ${coverage.visualCoverage != null ? coverage.visualCoverage : coverage.editableCoverage}%`);
|
|
90
93
|
console.log(` Editable coverage: ${coverage.editableCoverage}%`);
|
|
91
94
|
console.log(` Design preservation: ${design.score}%`);
|
|
92
95
|
}
|
|
@@ -135,6 +138,11 @@ function printSuccess() {
|
|
|
135
138
|
console.log(`\n${C.green}${C.bold}Deneb ARC completed successfully.${C.reset}\n`);
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
function printContractFailed() {
|
|
142
|
+
console.log(`\n${C.yellow}${C.bold}Deneb ARC finished with Fivora contract findings.${C.reset}`);
|
|
143
|
+
console.log(`${C.dim}Files were kept for debugging. This package is not upload-ready. Re-run with --strict to roll back.${C.reset}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
function printUncoveredText(findings = []) {
|
|
139
147
|
if (!findings.length) return;
|
|
140
148
|
warn(`${findings.length} visible text node(s) still uncovered — run deneb validate . before packaging`);
|
|
@@ -229,6 +237,7 @@ module.exports = {
|
|
|
229
237
|
printDryRun,
|
|
230
238
|
printError,
|
|
231
239
|
printSuccess,
|
|
240
|
+
printContractFailed,
|
|
232
241
|
printUncoveredText,
|
|
233
242
|
printDeveloperNextSteps,
|
|
234
243
|
printRollback,
|