@deneb-ui/cli 2.0.50 → 2.0.52
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 +219 -10
- package/src/arc/adapters.cjs +19 -0
- package/src/arc/field-paths.cjs +4 -0
- package/src/arc/fivora-contract.cjs +266 -5
- package/src/arc/index.cjs +129 -30
- package/src/arc/learning.cjs +20 -1
- package/src/arc/manifest.cjs +89 -11
- package/src/arc/planner.cjs +10 -6
- 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 +47 -8
- package/src/arc/style-candidates.cjs +2 -1
- package/src/arc/transformer.cjs +335 -20
- package/src/arc/validator.cjs +43 -3
- package/src/arc/version.cjs +1 -1
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* locally rather than at upload time.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const recast = require('recast');
|
|
12
|
+
const { parseSource, getJsxName, hasJsxAttribute, collectJsxText } = require('./ast.cjs');
|
|
13
|
+
|
|
11
14
|
const MARKER_ATTRIBUTE_TO_KIND = {
|
|
12
15
|
'data-preview-field-path': 'field',
|
|
13
16
|
'data-preview-list-path': 'list',
|
|
@@ -214,6 +217,17 @@ function auditMarkerPlacement(code, filePath) {
|
|
|
214
217
|
const staticMatch = attrs.match(/\bdata-preview-static\s*=\s*(?:"([^"]*)"|'([^']*)')/);
|
|
215
218
|
const hasStatic = /\bdata-preview-static\b/.test(attrs);
|
|
216
219
|
|
|
220
|
+
const isHidden =
|
|
221
|
+
/\bhidden(?:[\s=]|\/?>)/i.test(attrs) ||
|
|
222
|
+
/\baria-hidden\s*=\s*(?:"true"|'true'|\{\s*true\s*\})/i.test(attrs) ||
|
|
223
|
+
/\bstyle\s*=\s*\{\s*\{[\s\S]*?\b(?:display\s*:\s*['"]none['"]|visibility\s*:\s*['"]hidden['"])[\s\S]*?\}\s*\}/i.test(attrs) ||
|
|
224
|
+
/\bclassName\s*=\s*(?:"[^"]*\bhidden\b[^"]*"|'[^']*\bhidden\b[^']*'|\{\s*`[^`]*\bhidden\b[^`]*`\s*\})/i.test(attrs);
|
|
225
|
+
|
|
226
|
+
if ((hasField || hasList || hasItem) && isHidden) {
|
|
227
|
+
errors.push(
|
|
228
|
+
`${filePath}:${line} data-preview field/list/item marker is hidden. Strict visual-edit targets must remain visible and clickable in the exported page.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
217
231
|
if (hasStatic && !(staticMatch?.[1] ?? staticMatch?.[2] ?? '').trim()) {
|
|
218
232
|
errors.push(`${filePath}:${line} data-preview-static requires a reason.`);
|
|
219
233
|
}
|
|
@@ -234,6 +248,33 @@ function auditMarkerPlacement(code, filePath) {
|
|
|
234
248
|
}
|
|
235
249
|
}
|
|
236
250
|
|
|
251
|
+
errors.push(...auditCoupledListMarkers(code, filePath));
|
|
252
|
+
return errors;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Detects coupling parallel arrays by index: rendering a field from list B inside an item belonging to list A.
|
|
257
|
+
*/
|
|
258
|
+
function auditCoupledListMarkers(code, filePath) {
|
|
259
|
+
const errors = [];
|
|
260
|
+
const itemBlockPattern = /<\s*([a-zA-Z][a-zA-Z0-9.:-]*)((?:[^<>{}]|\{[^{}]*\})*?)\bdata-preview-item-path\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*`([^`]*)`\s*\})([\s\S]*?)<\/\1>/g;
|
|
261
|
+
for (const match of String(code).matchAll(itemBlockPattern)) {
|
|
262
|
+
const itemPath = match[3] ?? match[4] ?? match[5] ?? '';
|
|
263
|
+
const inner = match[6] || '';
|
|
264
|
+
if (!itemPath) continue;
|
|
265
|
+
const baseList = itemPath.replace(/\[[^\]]*\]$/, '');
|
|
266
|
+
|
|
267
|
+
for (const fieldMatch of inner.matchAll(/\bdata-preview-field-path\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*`([^`]*)`\s*\})/g)) {
|
|
268
|
+
const fieldPath = fieldMatch[1] ?? fieldMatch[2] ?? fieldMatch[3] ?? '';
|
|
269
|
+
if (!fieldPath || !/\[\d+\]/.test(fieldPath)) continue;
|
|
270
|
+
const fieldList = fieldPath.replace(/\[\d+\][\s\S]*$/, '');
|
|
271
|
+
if (fieldList && baseList && fieldList !== baseList) {
|
|
272
|
+
errors.push(
|
|
273
|
+
`${filePath}:${lineNumberAt(code, match.index ?? 0)} repeated field "${fieldPath}" is rendered inside item "${itemPath}" but belongs to a different list. Model one visual card as one object-list item instead of coupling parallel arrays by index.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
237
278
|
return errors;
|
|
238
279
|
}
|
|
239
280
|
|
|
@@ -475,8 +516,71 @@ function auditPreviewRuntime(sources) {
|
|
|
475
516
|
/**
|
|
476
517
|
* Finds meaningful visible JSX text that is neither bound to a field nor marked
|
|
477
518
|
* static. Strict mode treats these as errors in the exported HTML.
|
|
519
|
+
* Prefers an AST walk so TypeScript generics and nested tags are not misread.
|
|
478
520
|
*/
|
|
479
521
|
function findUncoveredVisibleText(code, filePath) {
|
|
522
|
+
let ast;
|
|
523
|
+
try {
|
|
524
|
+
ast = parseSource(code, filePath);
|
|
525
|
+
} catch {
|
|
526
|
+
return findUncoveredVisibleTextRegex(code, filePath);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const findings = [];
|
|
530
|
+
recast.types.visit(ast, {
|
|
531
|
+
visitJSXElement(pathNode) {
|
|
532
|
+
const node = pathNode.node;
|
|
533
|
+
const tag = getJsxName(node);
|
|
534
|
+
const lower = String(tag || '').toLowerCase();
|
|
535
|
+
if (!tag || lower === 'option' || lower === 'script' || lower === 'style') {
|
|
536
|
+
this.traverse(pathNode);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (
|
|
540
|
+
hasJsxAttribute(node, 'data-preview-field-path') ||
|
|
541
|
+
hasJsxAttribute(node, 'data-preview-static')
|
|
542
|
+
) {
|
|
543
|
+
this.traverse(pathNode);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (ancestorHasStaticMarker(pathNode)) {
|
|
547
|
+
this.traverse(pathNode);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const text = collectJsxText(node);
|
|
551
|
+
if (isMeaningfulUncoveredText(text)) {
|
|
552
|
+
findings.push({
|
|
553
|
+
tag,
|
|
554
|
+
text,
|
|
555
|
+
filePath,
|
|
556
|
+
line: node.loc?.start?.line || 1,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
this.traverse(pathNode);
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
return findings;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function ancestorHasStaticMarker(pathNode) {
|
|
566
|
+
let current = pathNode.parent;
|
|
567
|
+
while (current) {
|
|
568
|
+
const node = current.node || current.value;
|
|
569
|
+
if (node && node.type === 'JSXElement' && hasJsxAttribute(node, 'data-preview-static')) return true;
|
|
570
|
+
current = current.parentPath || current.parent;
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isMeaningfulUncoveredText(text) {
|
|
576
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
577
|
+
if (!value || value.length <= 2 || !/\p{L}/u.test(value)) return false;
|
|
578
|
+
if (!/^[\p{L}\p{N}"'(¡¿#$€£]/u.test(value)) return false;
|
|
579
|
+
if (/^(?:true|false|null|undefined)$/i.test(value)) return false;
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function findUncoveredVisibleTextRegex(code, filePath) {
|
|
480
584
|
const findings = [];
|
|
481
585
|
const source = String(code);
|
|
482
586
|
const elementPattern = /<\s*([a-zA-Z][a-zA-Z0-9.:-]*)((?:[^<>{}]|\{[^{}]*\})*?)>([^<>{}]*)</g;
|
|
@@ -487,13 +591,9 @@ function findUncoveredVisibleText(code, filePath) {
|
|
|
487
591
|
const text = (match[3] || '').replace(/\s+/g, ' ').trim();
|
|
488
592
|
const offset = match.index ?? 0;
|
|
489
593
|
|
|
490
|
-
// A `<` preceded by an identifier character is a TypeScript generic
|
|
491
|
-
// (forwardRef<HTMLButtonElement, Props>), never a JSX element.
|
|
492
594
|
if (/[\w$)\]]/.test(source[offset - 1] || '')) continue;
|
|
493
595
|
if (tag.toLowerCase() === 'option') continue;
|
|
494
|
-
if (!
|
|
495
|
-
if (!/^[\p{L}\p{N}"'(¡¿#$€£]/u.test(text)) continue;
|
|
496
|
-
if (/^(?:true|false|null|undefined)$/i.test(text)) continue;
|
|
596
|
+
if (!isMeaningfulUncoveredText(text)) continue;
|
|
497
597
|
if (/\bdata-preview-(?:field-path|static)\b/.test(attrs)) continue;
|
|
498
598
|
|
|
499
599
|
findings.push({ tag, text, filePath, line: lineNumberAt(source, offset) });
|
|
@@ -502,6 +602,161 @@ function findUncoveredVisibleText(code, filePath) {
|
|
|
502
602
|
return findings;
|
|
503
603
|
}
|
|
504
604
|
|
|
605
|
+
function auditEmptyStateSource(code, filePath) {
|
|
606
|
+
const errors = [];
|
|
607
|
+
const source = String(code);
|
|
608
|
+
const andPattern =
|
|
609
|
+
/\{([^{}]{0,120}?)\s*&&\s*\(?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>/g;
|
|
610
|
+
for (const match of source.matchAll(andPattern)) {
|
|
611
|
+
errors.push(
|
|
612
|
+
`${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker is gated behind "${String(match[1]).trim()} &&". Keep the target mounted when its value is empty, false, or zero.`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
const ternaryPattern =
|
|
616
|
+
/\{([^{}]{0,80}?)\s*\?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>[\s\S]{0,200}?\?\s*(?:null|false|undefined)/g;
|
|
617
|
+
for (const match of source.matchAll(ternaryPattern)) {
|
|
618
|
+
errors.push(
|
|
619
|
+
`${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker unmounts on a falsy ternary. Keep the target mounted when its value is empty.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return errors;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function auditSelectOptions(editorSchema, pages = []) {
|
|
626
|
+
const errors = [];
|
|
627
|
+
const declaredPageIds = new Set((pages || []).map((page) => page?.id).filter(Boolean));
|
|
628
|
+
|
|
629
|
+
function checkSelect(path, options) {
|
|
630
|
+
if (!Array.isArray(options) || !options.some((option) => typeof option === 'string' && option.trim())) {
|
|
631
|
+
errors.push(
|
|
632
|
+
`editorSchema select field "${path}" must declare at least one non-empty option for strict visual editing.`
|
|
633
|
+
);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (!declaredPageIds.size) return;
|
|
637
|
+
if (!/(?:destination|action|targetpage|buttonaction|pagekey)/i.test(path)) return;
|
|
638
|
+
for (const option of options) {
|
|
639
|
+
const opt = typeof option === 'string' ? option.trim() : '';
|
|
640
|
+
if (opt && opt !== 'none' && opt !== 'external' && !declaredPageIds.has(opt)) {
|
|
641
|
+
errors.push(
|
|
642
|
+
`editorSchema select field "${path}" declares destination option "${opt}" which is not in fivora-template.json pages[].`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function walk(path, node) {
|
|
649
|
+
if (!node) return;
|
|
650
|
+
if (node.type === 'select') {
|
|
651
|
+
checkSelect(path, node.options);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (node.type === 'object') {
|
|
655
|
+
for (const field of node.fields || []) walk(appendPath(path, field.key), field);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (node.type !== 'list') return;
|
|
659
|
+
const itemPath = `${wildcardPath(path)}[*]`;
|
|
660
|
+
if (node.itemField?.type === 'select') checkSelect(itemPath, node.itemField.options);
|
|
661
|
+
for (const field of node.fields || []) walk(appendPath(itemPath, field.key), field);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const section of editorSchema?.sections || []) walk(section.path, section);
|
|
665
|
+
return errors;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function auditListBounds(editorSchema, content) {
|
|
669
|
+
const errors = [];
|
|
670
|
+
|
|
671
|
+
function validBound(value) {
|
|
672
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function readValue(path) {
|
|
676
|
+
let value = content || {};
|
|
677
|
+
for (const part of String(path).split('.')) {
|
|
678
|
+
if (!part || !isPlainObject(value)) return undefined;
|
|
679
|
+
value = value[part];
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function walkSchema(path, node) {
|
|
685
|
+
if (!node) return;
|
|
686
|
+
if (node.type === 'object') {
|
|
687
|
+
for (const field of node.fields || []) walkSchema(appendPath(path, field.key), field);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (node.type !== 'list') return;
|
|
691
|
+
const minItems = validBound(node.minItems);
|
|
692
|
+
const maxItems = validBound(node.maxItems);
|
|
693
|
+
if (node.minItems !== undefined && minItems === null) {
|
|
694
|
+
errors.push(`editorSchema list "${path}" minItems must be a non-negative safe integer.`);
|
|
695
|
+
}
|
|
696
|
+
if (node.maxItems !== undefined && maxItems === null) {
|
|
697
|
+
errors.push(`editorSchema list "${path}" maxItems must be a non-negative safe integer.`);
|
|
698
|
+
}
|
|
699
|
+
const effectiveMinimum = minItems ?? (node.required ? 1 : 0);
|
|
700
|
+
if (maxItems !== null && effectiveMinimum > maxItems) {
|
|
701
|
+
errors.push(
|
|
702
|
+
`editorSchema list "${path}" requires at least ${effectiveMinimum} item(s) but maxItems is ${maxItems}.`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const items = Array.isArray(readValue(path)) ? readValue(path) : [];
|
|
706
|
+
if (maxItems !== null && items.length > maxItems) {
|
|
707
|
+
errors.push(
|
|
708
|
+
`site-data.json content list "${path}" contains ${items.length} items, exceeding editorSchema maxItems ${maxItems}.`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const itemPath = `${wildcardPath(path)}[*]`;
|
|
712
|
+
for (const field of node.fields || []) walkSchema(appendPath(itemPath, field.key), field);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const section of editorSchema?.sections || []) walkSchema(section.path, section);
|
|
716
|
+
return errors;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function auditStaticMarkerAuthorship(code, filePath) {
|
|
720
|
+
if (!/\.[cm]?[jt]s$/i.test(filePath) || /\.(tsx|jsx)$/i.test(filePath)) return [];
|
|
721
|
+
const mutation =
|
|
722
|
+
/\b(?:setAttribute|setAttributeNS|toggleAttribute)\s*\(\s*['"`]data-preview-static['"`]/.exec(code) ||
|
|
723
|
+
/\b(?:writeFile|writeFileSync)\s*\([\s\S]{0,200}data-preview-static/.exec(code);
|
|
724
|
+
if (!mutation) return [];
|
|
725
|
+
return [
|
|
726
|
+
`${filePath}:${lineNumberAt(code, mutation.index || 0)} programmatically injects data-preview-static. Author each intentional static annotation directly on the smallest source element.`,
|
|
727
|
+
];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function auditRouteOwnedMarkerCoverage({ editorSchema, pages, markers, controlOnlyPaths }) {
|
|
731
|
+
const errors = [];
|
|
732
|
+
const pagesById = new Map((pages || []).map((page) => [page.id, page]));
|
|
733
|
+
const sections = (editorSchema?.sections || [])
|
|
734
|
+
.map((section) => ({ section, canonicalPath: canonicalizeMarkerPath(section.path) }))
|
|
735
|
+
.filter((entry) => entry.canonicalPath);
|
|
736
|
+
|
|
737
|
+
for (const marker of markers || []) {
|
|
738
|
+
if (marker.kind === 'page' || marker.kind === 'item') continue;
|
|
739
|
+
const canonical = canonicalizeMarkerPath(marker.value);
|
|
740
|
+
if (!canonical) continue;
|
|
741
|
+
if (marker.kind === 'field' && isControlOnly(canonical, controlOnlyPaths)) continue;
|
|
742
|
+
const owner = sections
|
|
743
|
+
.filter((entry) => {
|
|
744
|
+
const sectionPath = wildcardPath(entry.canonicalPath);
|
|
745
|
+
const markerPath = wildcardPath(canonical);
|
|
746
|
+
return markerPath === sectionPath || markerPath.startsWith(`${sectionPath}.`) || markerPath.startsWith(`${sectionPath}[`);
|
|
747
|
+
})
|
|
748
|
+
.sort((left, right) => right.canonicalPath.length - left.canonicalPath.length)[0];
|
|
749
|
+
if (!owner?.section.pageKey) continue;
|
|
750
|
+
const page = pagesById.get(owner.section.pageKey);
|
|
751
|
+
if (!page) {
|
|
752
|
+
errors.push(
|
|
753
|
+
`editorSchema section "${owner.section.path}" assigns ${marker.kind} "${canonical}" to unknown manifest page "${owner.section.pageKey}".`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return errors;
|
|
758
|
+
}
|
|
759
|
+
|
|
505
760
|
module.exports = {
|
|
506
761
|
BROAD_CONTENT_CONTAINERS,
|
|
507
762
|
PRIMITIVE_TYPES,
|
|
@@ -509,11 +764,17 @@ module.exports = {
|
|
|
509
764
|
enumerateSchemaPaths,
|
|
510
765
|
extractMarkers,
|
|
511
766
|
auditMarkerPlacement,
|
|
767
|
+
auditCoupledListMarkers,
|
|
512
768
|
auditActionLabelCollision,
|
|
513
769
|
auditPathCoverage,
|
|
514
770
|
auditSchemaUniqueness,
|
|
515
771
|
auditPageCoverage,
|
|
516
772
|
auditPreviewRuntime,
|
|
773
|
+
auditEmptyStateSource,
|
|
774
|
+
auditSelectOptions,
|
|
775
|
+
auditListBounds,
|
|
776
|
+
auditStaticMarkerAuthorship,
|
|
777
|
+
auditRouteOwnedMarkerCoverage,
|
|
517
778
|
findUncoveredVisibleText,
|
|
518
779
|
canonicalizeMarkerPath,
|
|
519
780
|
wildcardPath,
|
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);
|
|
@@ -256,7 +289,7 @@ async function runDenebArcAsync(projectDir, projectName, options = {}) {
|
|
|
256
289
|
0
|
|
257
290
|
);
|
|
258
291
|
const actionCount = analyses.reduce(
|
|
259
|
-
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
|
|
292
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.operation === 'form-submit-action' || c.kind === 'url').length,
|
|
260
293
|
0
|
|
261
294
|
);
|
|
262
295
|
printer.printScan(profile, graph, candidateCount, actionCount);
|
|
@@ -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);
|
|
@@ -397,7 +423,7 @@ function runDenebArcSync(projectDir, projectName, options = {}) {
|
|
|
397
423
|
0
|
|
398
424
|
);
|
|
399
425
|
const actionCount = analyses.reduce(
|
|
400
|
-
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
|
|
426
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.operation === 'form-submit-action' || c.kind === 'url').length,
|
|
401
427
|
0
|
|
402
428
|
);
|
|
403
429
|
printer.printScan(profile, graph, candidateCount, actionCount);
|
|
@@ -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 };
|
|
@@ -118,6 +135,7 @@ function mapOperation(operation) {
|
|
|
118
135
|
case 'extract-url':
|
|
119
136
|
return 'url-extraction';
|
|
120
137
|
case 'split-action-contract':
|
|
138
|
+
case 'form-submit-action':
|
|
121
139
|
return 'contract-split';
|
|
122
140
|
case 'style-bind':
|
|
123
141
|
return 'style-bind';
|
|
@@ -235,6 +253,7 @@ module.exports = {
|
|
|
235
253
|
registryArchitecture,
|
|
236
254
|
redactSecrets,
|
|
237
255
|
promoteState,
|
|
256
|
+
honestOutcome,
|
|
238
257
|
localStorePath,
|
|
239
258
|
fingerprintStorePath,
|
|
240
259
|
RULE_STATES,
|