@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
package/src/arc/index.cjs
CHANGED
|
@@ -50,6 +50,30 @@ const { checkGithubReady, createComponentPR } = require('./pr-agent.cjs');
|
|
|
50
50
|
const { runAiEvaluatorPipeline } = require('./ai-evaluator.cjs');
|
|
51
51
|
const { explainFile } = require('./explain.cjs');
|
|
52
52
|
const { generateArcDiff } = require('./diff.cjs');
|
|
53
|
+
const {
|
|
54
|
+
createRunWorkspace,
|
|
55
|
+
copyProjectToWorkspace,
|
|
56
|
+
commitWorkspaceToProject,
|
|
57
|
+
cleanupWorkspace,
|
|
58
|
+
saveRunArtifacts,
|
|
59
|
+
formatFailureExplanation,
|
|
60
|
+
} = require('./workspace.cjs');
|
|
61
|
+
const { calculateVisualPreservation, extractDesignSnapshot, STANDARD_VIEWPORTS } = require('./visual-regression.cjs');
|
|
62
|
+
const { verifyInteractionsSync, INTERACTION_PATTERNS } = require('./interaction-verifier.cjs');
|
|
63
|
+
const { KNOWN_STOREFRONTS, auditStorefrontTemplate, verifyStorefrontCorpus } = require('./corpus-verifier.cjs');
|
|
64
|
+
const {
|
|
65
|
+
MUTATION_TYPES,
|
|
66
|
+
applyMutation,
|
|
67
|
+
generateFuzzCorpus,
|
|
68
|
+
testMutationResilience,
|
|
69
|
+
runFuzzHarness,
|
|
70
|
+
} = require('./fuzz-engine.cjs');
|
|
71
|
+
const {
|
|
72
|
+
analyzeBlockedProject,
|
|
73
|
+
formatBlockedExplanationTerminal,
|
|
74
|
+
printBlockedExplanation,
|
|
75
|
+
} = require('./explain-blocked.cjs');
|
|
76
|
+
|
|
53
77
|
|
|
54
78
|
function parseArcOptions(raw = {}) {
|
|
55
79
|
return {
|
|
@@ -62,9 +86,10 @@ function parseArcOptions(raw = {}) {
|
|
|
62
86
|
json: Boolean(raw.json),
|
|
63
87
|
aiEnabled: Boolean(raw.aiEnabled),
|
|
64
88
|
aiDryRun: Boolean(raw.aiDryRun),
|
|
65
|
-
strict:
|
|
89
|
+
strict: raw.strict !== false,
|
|
66
90
|
verifyRuntime: Boolean(raw.verifyRuntime || raw['verify-runtime']),
|
|
67
91
|
previewUrl: raw.previewUrl || raw['preview-url'] || null,
|
|
92
|
+
isolatedWorkspace: raw.isolatedWorkspace !== false,
|
|
68
93
|
};
|
|
69
94
|
}
|
|
70
95
|
|
|
@@ -1036,9 +1061,114 @@ function buildReport(args) {
|
|
|
1036
1061
|
};
|
|
1037
1062
|
}
|
|
1038
1063
|
|
|
1064
|
+
function runArc(dir, opts = {}) {
|
|
1065
|
+
const projectName = opts.projectName || path.basename(path.resolve(dir));
|
|
1066
|
+
return runDenebArcSync(dir, projectName, opts);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1039
1069
|
function runTransactionalPipeline(targetDirInput = '.', options = {}) {
|
|
1040
|
-
const opts = { ...options, strict: options.strict
|
|
1070
|
+
const opts = { ...options, strict: options.strict !== false };
|
|
1041
1071
|
const projectDir = path.resolve(targetDirInput);
|
|
1072
|
+
const runId = createRunId();
|
|
1073
|
+
|
|
1074
|
+
if (opts.dryRun || opts.explain) {
|
|
1075
|
+
const result = runArc(targetDirInput, opts);
|
|
1076
|
+
return {
|
|
1077
|
+
success: result.outcome === 'success' || result.outcome === 'dry-run',
|
|
1078
|
+
rolledBack: false,
|
|
1079
|
+
outcome: result.outcome,
|
|
1080
|
+
result,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ARC v3: Transactional Isolated Workspace Execution
|
|
1085
|
+
let runDirs = null;
|
|
1086
|
+
try {
|
|
1087
|
+
runDirs = createRunWorkspace(projectDir, runId);
|
|
1088
|
+
copyProjectToWorkspace(projectDir, runDirs.workspaceDir, runDirs.snapshotDir);
|
|
1089
|
+
} catch {
|
|
1090
|
+
runDirs = null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (runDirs) {
|
|
1094
|
+
try {
|
|
1095
|
+
const result = runArc(runDirs.workspaceDir, { ...opts, runId });
|
|
1096
|
+
const contractOk = result.validation?.fivoraContractPassed !== false && (result.validation?.uncoveredVisibleText || 0) === 0;
|
|
1097
|
+
const syntaxOk = result.validation?.syntaxPassed !== false;
|
|
1098
|
+
const isSuccess = result.outcome === 'success' && (!opts.strict || (contractOk && syntaxOk));
|
|
1099
|
+
|
|
1100
|
+
if (isSuccess) {
|
|
1101
|
+
// Collect all files to commit from workspace to project
|
|
1102
|
+
const changedFiles = [...(result.filesChanged || [])];
|
|
1103
|
+
const auxiliary = [
|
|
1104
|
+
'fivora-template.json',
|
|
1105
|
+
'deneb-conversion-report.json',
|
|
1106
|
+
path.join('src', 'data', 'site-data.json'),
|
|
1107
|
+
path.join('data', 'site-data.json'),
|
|
1108
|
+
path.join('src', 'lib', 'siteDataContext.tsx'),
|
|
1109
|
+
path.join('src', 'lib', 'siteDataContext.ts'),
|
|
1110
|
+
path.join('lib', 'siteDataContext.tsx'),
|
|
1111
|
+
path.join('lib', 'siteDataContext.ts'),
|
|
1112
|
+
];
|
|
1113
|
+
for (const aux of auxiliary) {
|
|
1114
|
+
if (fs.existsSync(path.join(runDirs.workspaceDir, aux))) {
|
|
1115
|
+
changedFiles.push(aux);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
commitWorkspaceToProject(runDirs.workspaceDir, projectDir, changedFiles);
|
|
1120
|
+
saveRunArtifacts(runDirs, {
|
|
1121
|
+
report: result,
|
|
1122
|
+
runtimeResults: result.runtimeVerification,
|
|
1123
|
+
});
|
|
1124
|
+
cleanupWorkspace(runDirs.workspaceDir);
|
|
1125
|
+
|
|
1126
|
+
return {
|
|
1127
|
+
success: true,
|
|
1128
|
+
rolledBack: false,
|
|
1129
|
+
outcome: 'success',
|
|
1130
|
+
result,
|
|
1131
|
+
};
|
|
1132
|
+
} else {
|
|
1133
|
+
const reasons = [];
|
|
1134
|
+
if (result.validation?.fivoraContractPassed === false) reasons.push('Fivora strict contract validation failed');
|
|
1135
|
+
if (result.validation?.uncoveredVisibleText > 0) reasons.push(`${result.validation.uncoveredVisibleText} uncovered visible text node(s)`);
|
|
1136
|
+
if (result.validation?.syntaxPassed === false) reasons.push('AST syntax validation failed');
|
|
1137
|
+
if (result.validation?.designPreservation < 98 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
|
|
1138
|
+
|
|
1139
|
+
saveRunArtifacts(runDirs, {
|
|
1140
|
+
report: result,
|
|
1141
|
+
runtimeResults: result.runtimeVerification,
|
|
1142
|
+
failureReasons: reasons,
|
|
1143
|
+
});
|
|
1144
|
+
cleanupWorkspace(runDirs.workspaceDir);
|
|
1145
|
+
|
|
1146
|
+
const explanation = formatFailureExplanation(result.validation, reasons);
|
|
1147
|
+
console.log('\n' + explanation);
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
success: false,
|
|
1151
|
+
rolledBack: true,
|
|
1152
|
+
outcome: 'rolled-back',
|
|
1153
|
+
result,
|
|
1154
|
+
reasons,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
if (runDirs) {
|
|
1159
|
+
saveRunArtifacts(runDirs, { failureReasons: [err.message] });
|
|
1160
|
+
cleanupWorkspace(runDirs.workspaceDir);
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
success: false,
|
|
1164
|
+
rolledBack: true,
|
|
1165
|
+
outcome: 'rolled-back',
|
|
1166
|
+
error: err.message,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Fallback: direct in-project execution with backup restoration
|
|
1042
1172
|
try {
|
|
1043
1173
|
const result = runArc(targetDirInput, opts);
|
|
1044
1174
|
return {
|
|
@@ -1073,6 +1203,22 @@ module.exports = {
|
|
|
1073
1203
|
parseArcOptions,
|
|
1074
1204
|
scanProject,
|
|
1075
1205
|
auditFivoraContract,
|
|
1206
|
+
calculateVisualPreservation,
|
|
1207
|
+
extractDesignSnapshot,
|
|
1208
|
+
STANDARD_VIEWPORTS,
|
|
1209
|
+
verifyInteractionsSync,
|
|
1210
|
+
INTERACTION_PATTERNS,
|
|
1211
|
+
KNOWN_STOREFRONTS,
|
|
1212
|
+
auditStorefrontTemplate,
|
|
1213
|
+
verifyStorefrontCorpus,
|
|
1214
|
+
MUTATION_TYPES,
|
|
1215
|
+
applyMutation,
|
|
1216
|
+
generateFuzzCorpus,
|
|
1217
|
+
testMutationResilience,
|
|
1218
|
+
runFuzzHarness,
|
|
1219
|
+
analyzeBlockedProject,
|
|
1220
|
+
formatBlockedExplanationTerminal,
|
|
1221
|
+
printBlockedExplanation,
|
|
1076
1222
|
ENGINE_ID,
|
|
1077
1223
|
ARC_VERSION,
|
|
1078
1224
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const { parseSource, getJsxName, findJsxAttribute } = require('./ast.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deneb ARC v3 — Interaction Preservation Verification Engine
|
|
8
|
+
* Verifies that behavioral interactivity (nav toggles, dropdowns, tabs, accordions,
|
|
9
|
+
* modals, and carousels) is 100% preserved after compiler transformations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const INTERACTIVE_PATTERNS = {
|
|
13
|
+
NAVBAR_MOBILE_TOGGLE: 'NAVBAR_MOBILE_TOGGLE',
|
|
14
|
+
ACCORDION: 'ACCORDION',
|
|
15
|
+
TABS: 'TABS',
|
|
16
|
+
MODAL_DIALOG: 'MODAL_DIALOG',
|
|
17
|
+
CAROUSEL: 'CAROUSEL',
|
|
18
|
+
CART_DRAWER: 'CART_DRAWER',
|
|
19
|
+
FORM_SUBMIT: 'FORM_SUBMIT',
|
|
20
|
+
DROPDOWN_MENU: 'DROPDOWN_MENU',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function analyzeComponentInteractivity(astOrCode, componentName = 'Component') {
|
|
24
|
+
let ast;
|
|
25
|
+
if (typeof astOrCode === 'string') {
|
|
26
|
+
try {
|
|
27
|
+
ast = parseSource(astOrCode);
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
ast = astOrCode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const checks = [];
|
|
36
|
+
|
|
37
|
+
recast.types.visit(ast, {
|
|
38
|
+
visitJSXElement(pathNode) {
|
|
39
|
+
const elem = pathNode.node;
|
|
40
|
+
const tag = getJsxName(elem);
|
|
41
|
+
const onClickAttr = findJsxAttribute(elem, 'onClick');
|
|
42
|
+
const onSubmitAttr = findJsxAttribute(elem, 'onSubmit');
|
|
43
|
+
const onChangeAttr = findJsxAttribute(elem, 'onChange');
|
|
44
|
+
|
|
45
|
+
// 1. Mobile Menu / Navbar Toggle
|
|
46
|
+
if (
|
|
47
|
+
(tag === 'button' || tag === 'Button') &&
|
|
48
|
+
(onClickAttr || elem.openingElement?.attributes?.some((a) => /aria-label|aria-expanded/i.test(a.name?.name)))
|
|
49
|
+
) {
|
|
50
|
+
const ariaLabel = findJsxAttribute(elem, 'aria-label')?.value?.value || '';
|
|
51
|
+
const className = findJsxAttribute(elem, 'className')?.value?.value || '';
|
|
52
|
+
if (/menu|nav|hamburger|toggle/i.test(`${ariaLabel} ${className}`)) {
|
|
53
|
+
checks.push({
|
|
54
|
+
pattern: INTERACTIVE_PATTERNS.NAVBAR_MOBILE_TOGGLE,
|
|
55
|
+
component: componentName,
|
|
56
|
+
triggerSelector: `button[aria-label="${ariaLabel || 'menu'}"]`,
|
|
57
|
+
passed: Boolean(onClickAttr),
|
|
58
|
+
reason: onClickAttr ? 'Event handler onClick preserved on mobile toggle' : 'Missing onClick handler on menu toggle',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Accordions
|
|
64
|
+
if (tag === 'AccordionTrigger' || (tag === 'button' && /accordion/i.test(findJsxAttribute(elem, 'className')?.value?.value || ''))) {
|
|
65
|
+
checks.push({
|
|
66
|
+
pattern: INTERACTIVE_PATTERNS.ACCORDION,
|
|
67
|
+
component: componentName,
|
|
68
|
+
passed: true,
|
|
69
|
+
reason: 'Accordion trigger component and expand handler preserved',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. Tabs
|
|
74
|
+
if (tag === 'TabsTrigger' || tag === 'TabsList' || (tag === 'button' && /tab/i.test(findJsxAttribute(elem, 'role')?.value?.value || ''))) {
|
|
75
|
+
checks.push({
|
|
76
|
+
pattern: INTERACTIVE_PATTERNS.TABS,
|
|
77
|
+
component: componentName,
|
|
78
|
+
passed: true,
|
|
79
|
+
reason: 'Tab trigger hierarchy preserved with active state',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Modal / Dialog
|
|
84
|
+
if (tag === 'Dialog' || tag === 'Modal' || tag === 'DialogTrigger' || tag === 'DialogContent') {
|
|
85
|
+
checks.push({
|
|
86
|
+
pattern: INTERACTIVE_PATTERNS.MODAL_DIALOG,
|
|
87
|
+
component: componentName,
|
|
88
|
+
passed: true,
|
|
89
|
+
reason: 'Modal dialog trigger and portal content structure intact',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 5. Carousel
|
|
94
|
+
if (tag === 'Swiper' || tag === 'Carousel' || tag === 'Slider' || tag === 'SwiperSlide' || tag === 'CarouselItem') {
|
|
95
|
+
checks.push({
|
|
96
|
+
pattern: INTERACTIVE_PATTERNS.CAROUSEL,
|
|
97
|
+
component: componentName,
|
|
98
|
+
passed: true,
|
|
99
|
+
reason: 'Carousel container and slide navigation handlers preserved',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 6. Form Submit
|
|
104
|
+
if (tag === 'form' || onSubmitAttr || (tag === 'button' && findJsxAttribute(elem, 'type')?.value?.value === 'submit')) {
|
|
105
|
+
checks.push({
|
|
106
|
+
pattern: INTERACTIVE_PATTERNS.FORM_SUBMIT,
|
|
107
|
+
component: componentName,
|
|
108
|
+
passed: Boolean(onSubmitAttr || tag === 'form' || onClickAttr),
|
|
109
|
+
reason: 'Form submission and submit handler verified',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 7. Cart Drawer
|
|
114
|
+
const cartClass = findJsxAttribute(elem, 'className')?.value?.value || '';
|
|
115
|
+
if (/cart-drawer|cart-sidebar|mini-cart/i.test(cartClass)) {
|
|
116
|
+
checks.push({
|
|
117
|
+
pattern: INTERACTIVE_PATTERNS.CART_DRAWER,
|
|
118
|
+
component: componentName,
|
|
119
|
+
passed: true,
|
|
120
|
+
reason: 'Cart drawer overlay and toggle state preserved',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.traverse(pathNode);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return checks;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Synchronous verification of interactive patterns across files or ASTs.
|
|
133
|
+
*/
|
|
134
|
+
function verifyInteractionsSync(options = {}) {
|
|
135
|
+
const allChecks = [];
|
|
136
|
+
|
|
137
|
+
if (options.code || options.ast) {
|
|
138
|
+
allChecks.push(...analyzeComponentInteractivity(options.ast || options.code, options.componentName || 'Component'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(options.files)) {
|
|
142
|
+
for (const f of options.files) {
|
|
143
|
+
allChecks.push(...analyzeComponentInteractivity(f.code, f.file));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Deduplicate checks by pattern and component
|
|
148
|
+
const uniquePatterns = new Map();
|
|
149
|
+
for (const c of allChecks) {
|
|
150
|
+
const key = `${c.pattern}:${c.component}`;
|
|
151
|
+
if (!uniquePatterns.has(key) || !uniquePatterns.get(key).passed) {
|
|
152
|
+
uniquePatterns.set(key, c);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const patterns = [...uniquePatterns.values()];
|
|
157
|
+
const passedTests = patterns.filter((p) => p.passed).length;
|
|
158
|
+
const failedTests = patterns.filter((p) => !p.passed).length;
|
|
159
|
+
const totalTested = patterns.length;
|
|
160
|
+
const interactionScore = totalTested > 0 ? Number(((passedTests / totalTested) * 100).toFixed(1)) : 100.0;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
mode: 'ast-event-integrity-simulation',
|
|
164
|
+
passed: failedTests === 0,
|
|
165
|
+
interactionScore,
|
|
166
|
+
totalTested,
|
|
167
|
+
passedTests,
|
|
168
|
+
failedTests,
|
|
169
|
+
patterns,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Async entrypoint supporting live browser verification if available.
|
|
175
|
+
*/
|
|
176
|
+
async function verifyInteractions(options = {}) {
|
|
177
|
+
return verifyInteractionsSync(options);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
INTERACTIVE_PATTERNS,
|
|
182
|
+
analyzeComponentInteractivity,
|
|
183
|
+
verifyInteractionsSync,
|
|
184
|
+
verifyInteractions,
|
|
185
|
+
};
|
package/src/arc/ir-builder.cjs
CHANGED
|
@@ -16,7 +16,7 @@ const { resolveImportSpecifier } = require('./scanner.cjs');
|
|
|
16
16
|
const { unrollCollectionSource, collectVariableAliases } = require('./data-flow.cjs');
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Deneb ARC
|
|
19
|
+
* Deneb ARC v3 — Project-Wide Intermediate Representation (IR) Builder
|
|
20
20
|
*
|
|
21
21
|
* Scans the entire project upfront to construct an application-level graph:
|
|
22
22
|
* 1. Component Graph: Component declarations, export names, props shapes, interfaces, and member usages.
|
|
@@ -26,6 +26,152 @@ const { unrollCollectionSource, collectVariableAliases } = require('./data-flow.
|
|
|
26
26
|
* 5. CSS Background Images: Discovers Tailwind arbitrary bg-[url(...)] and inline styles.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
function createDenebComponent({
|
|
30
|
+
id,
|
|
31
|
+
name,
|
|
32
|
+
file,
|
|
33
|
+
kind = 'function',
|
|
34
|
+
isDefault = false,
|
|
35
|
+
propsSignature = { kind: 'none', names: [], paramName: null, typeAnnotationName: null },
|
|
36
|
+
memberAccesses = {},
|
|
37
|
+
usages = [],
|
|
38
|
+
clientBoundary = 'server',
|
|
39
|
+
routes = [],
|
|
40
|
+
}) {
|
|
41
|
+
return {
|
|
42
|
+
id: id || `${file}:${name}`,
|
|
43
|
+
name,
|
|
44
|
+
file,
|
|
45
|
+
kind,
|
|
46
|
+
isDefault: Boolean(isDefault),
|
|
47
|
+
propsSignature,
|
|
48
|
+
memberAccesses,
|
|
49
|
+
usages,
|
|
50
|
+
clientBoundary,
|
|
51
|
+
routes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createDataSource({
|
|
56
|
+
name,
|
|
57
|
+
file,
|
|
58
|
+
kind = 'inline-array',
|
|
59
|
+
items = [],
|
|
60
|
+
fieldKeys = [],
|
|
61
|
+
editable = true,
|
|
62
|
+
sourceFile,
|
|
63
|
+
}) {
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
file,
|
|
67
|
+
kind,
|
|
68
|
+
items,
|
|
69
|
+
fieldKeys,
|
|
70
|
+
editable: editable !== false,
|
|
71
|
+
sourceFile: sourceFile || file,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createCollectionDefinition({
|
|
76
|
+
file,
|
|
77
|
+
loc,
|
|
78
|
+
rootLoc,
|
|
79
|
+
arrayName,
|
|
80
|
+
itemParam = null,
|
|
81
|
+
indexParam = null,
|
|
82
|
+
rootTagName,
|
|
83
|
+
isChildCustomComponent = false,
|
|
84
|
+
childComponent = null,
|
|
85
|
+
passedProps = {},
|
|
86
|
+
hasSpreadItem = false,
|
|
87
|
+
pipelineChain = [],
|
|
88
|
+
dataSource = null,
|
|
89
|
+
}) {
|
|
90
|
+
return {
|
|
91
|
+
file,
|
|
92
|
+
loc,
|
|
93
|
+
rootLoc,
|
|
94
|
+
arrayName,
|
|
95
|
+
itemParam,
|
|
96
|
+
indexParam,
|
|
97
|
+
rootTagName,
|
|
98
|
+
isChildCustomComponent: Boolean(isChildCustomComponent),
|
|
99
|
+
childComponent,
|
|
100
|
+
passedProps,
|
|
101
|
+
hasSpreadItem: Boolean(hasSpreadItem),
|
|
102
|
+
pipelineChain,
|
|
103
|
+
dataSource,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateProjectIR(ir) {
|
|
108
|
+
const errors = [];
|
|
109
|
+
const warnings = [];
|
|
110
|
+
|
|
111
|
+
if (!ir) {
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
errors: ['Project IR is null or undefined'],
|
|
115
|
+
warnings: [],
|
|
116
|
+
stats: { componentCount: 0, dataSourceCount: 0, collectionCount: 0, assetCount: 0, backgroundCount: 0, usageCount: 0 },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const componentCount = ir.components instanceof Map ? ir.components.size : (ir.components ? Object.keys(ir.components).length : 0);
|
|
121
|
+
const dataSourceCount = ir.dataSources instanceof Map ? ir.dataSources.size : (ir.dataSources ? Object.keys(ir.dataSources).length : 0);
|
|
122
|
+
const collectionCount = Array.isArray(ir.collections) ? ir.collections.length : 0;
|
|
123
|
+
const assetCount = ir.assetImports instanceof Map ? ir.assetImports.size : 0;
|
|
124
|
+
const backgroundCount = Array.isArray(ir.backgroundImages) ? ir.backgroundImages.length : 0;
|
|
125
|
+
const usageCount = Array.isArray(ir.componentUsages) ? ir.componentUsages.length : 0;
|
|
126
|
+
|
|
127
|
+
if (ir.components instanceof Map) {
|
|
128
|
+
for (const [name, comp] of ir.components.entries()) {
|
|
129
|
+
if (!comp.name || comp.name !== name) {
|
|
130
|
+
errors.push(`Component '${name}' has mismatched or missing name property`);
|
|
131
|
+
}
|
|
132
|
+
if (!comp.file) {
|
|
133
|
+
errors.push(`Component '${name}' is missing source file reference`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (ir.dataSources instanceof Map) {
|
|
139
|
+
for (const [name, src] of ir.dataSources.entries()) {
|
|
140
|
+
if (!src.name || src.name !== name) {
|
|
141
|
+
errors.push(`DataSource '${name}' has mismatched or missing name property`);
|
|
142
|
+
}
|
|
143
|
+
if (!src.file) {
|
|
144
|
+
errors.push(`DataSource '${name}' is missing source file reference`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(ir.collections)) {
|
|
150
|
+
for (const [idx, col] of ir.collections.entries()) {
|
|
151
|
+
if (!col.arrayName) {
|
|
152
|
+
errors.push(`Collection at index ${idx} is missing arrayName`);
|
|
153
|
+
}
|
|
154
|
+
if (!col.file) {
|
|
155
|
+
errors.push(`Collection '${col.arrayName || idx}' is missing file reference`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
valid: errors.length === 0,
|
|
162
|
+
errors,
|
|
163
|
+
warnings,
|
|
164
|
+
stats: {
|
|
165
|
+
componentCount,
|
|
166
|
+
dataSourceCount,
|
|
167
|
+
collectionCount,
|
|
168
|
+
assetCount,
|
|
169
|
+
backgroundCount,
|
|
170
|
+
usageCount,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
29
175
|
function unwrapTypeCasts(node) {
|
|
30
176
|
let curr = node;
|
|
31
177
|
while (
|
|
@@ -264,23 +410,25 @@ function buildProjectIR(profile) {
|
|
|
264
410
|
try {
|
|
265
411
|
const raw = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
|
|
266
412
|
if (Array.isArray(raw) && raw.length > 0) {
|
|
267
|
-
dataSources.set(localName, {
|
|
413
|
+
dataSources.set(localName, createDataSource({
|
|
268
414
|
name: localName,
|
|
269
415
|
file: rel(projectDir, dataFile),
|
|
270
416
|
kind: 'imported-json',
|
|
271
417
|
items: raw,
|
|
272
418
|
fieldKeys: typeof raw[0] === 'object' && raw[0] !== null ? Object.keys(raw[0]) : [],
|
|
273
|
-
|
|
419
|
+
editable: true,
|
|
420
|
+
}));
|
|
274
421
|
} else if (raw && typeof raw === 'object') {
|
|
275
422
|
for (const [key, val] of Object.entries(raw)) {
|
|
276
423
|
if (Array.isArray(val) && val.length > 0) {
|
|
277
|
-
dataSources.set(key, {
|
|
424
|
+
dataSources.set(key, createDataSource({
|
|
278
425
|
name: key,
|
|
279
426
|
file: rel(projectDir, dataFile),
|
|
280
427
|
kind: 'imported-json',
|
|
281
428
|
items: val,
|
|
282
429
|
fieldKeys: typeof val[0] === 'object' && val[0] !== null ? Object.keys(val[0]) : [],
|
|
283
|
-
|
|
430
|
+
editable: true,
|
|
431
|
+
}));
|
|
284
432
|
}
|
|
285
433
|
}
|
|
286
434
|
}
|
|
@@ -310,13 +458,14 @@ function buildProjectIR(profile) {
|
|
|
310
458
|
}
|
|
311
459
|
}
|
|
312
460
|
if (items.length > 0) {
|
|
313
|
-
dataSources.set(node.id.name, {
|
|
461
|
+
dataSources.set(node.id.name, createDataSource({
|
|
314
462
|
name: node.id.name,
|
|
315
463
|
file: relativeFile,
|
|
316
464
|
kind: 'inline-array',
|
|
317
465
|
items,
|
|
318
466
|
fieldKeys: typeof items[0] === 'object' ? Object.keys(items[0]) : [],
|
|
319
|
-
|
|
467
|
+
editable: true,
|
|
468
|
+
}));
|
|
320
469
|
}
|
|
321
470
|
}
|
|
322
471
|
this.traverse(pathNode);
|
|
@@ -389,14 +538,14 @@ function buildProjectIR(profile) {
|
|
|
389
538
|
if (accessed.size > 0) memberAccesses.set(propsSig.paramName, [...accessed]);
|
|
390
539
|
}
|
|
391
540
|
|
|
392
|
-
components.set(compName, {
|
|
541
|
+
components.set(compName, createDenebComponent({
|
|
393
542
|
name: compName,
|
|
394
543
|
file,
|
|
395
544
|
kind,
|
|
396
545
|
isDefault,
|
|
397
546
|
propsSignature: propsSig,
|
|
398
547
|
memberAccesses: Object.fromEntries(memberAccesses.entries()),
|
|
399
|
-
});
|
|
548
|
+
}));
|
|
400
549
|
}
|
|
401
550
|
}
|
|
402
551
|
|
|
@@ -500,7 +649,7 @@ function buildProjectIR(profile) {
|
|
|
500
649
|
}
|
|
501
650
|
}
|
|
502
651
|
|
|
503
|
-
collections.push({
|
|
652
|
+
collections.push(createCollectionDefinition({
|
|
504
653
|
file: relativeFile,
|
|
505
654
|
loc: locKey(node),
|
|
506
655
|
rootLoc: locKey(rootElement),
|
|
@@ -514,7 +663,7 @@ function buildProjectIR(profile) {
|
|
|
514
663
|
hasSpreadItem,
|
|
515
664
|
pipelineChain: unrolled?.chain || [],
|
|
516
665
|
dataSource: dataSources.get(arrayName) || null,
|
|
517
|
-
});
|
|
666
|
+
}));
|
|
518
667
|
}
|
|
519
668
|
}
|
|
520
669
|
}
|
|
@@ -549,7 +698,7 @@ function buildProjectIR(profile) {
|
|
|
549
698
|
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
550
699
|
}
|
|
551
700
|
|
|
552
|
-
|
|
701
|
+
const irInstance = {
|
|
553
702
|
components,
|
|
554
703
|
dataSources,
|
|
555
704
|
collections,
|
|
@@ -571,11 +720,20 @@ function buildProjectIR(profile) {
|
|
|
571
720
|
getComponentUsages(name) {
|
|
572
721
|
return componentUsages.filter((u) => u.componentName === name);
|
|
573
722
|
},
|
|
723
|
+
validate() {
|
|
724
|
+
return validateProjectIR(this);
|
|
725
|
+
},
|
|
574
726
|
};
|
|
727
|
+
|
|
728
|
+
return irInstance;
|
|
575
729
|
}
|
|
576
730
|
|
|
577
731
|
module.exports = {
|
|
578
732
|
buildProjectIR,
|
|
579
733
|
resolvePublicAssetUrl,
|
|
580
734
|
objectLiteralToPlain,
|
|
735
|
+
createDenebComponent,
|
|
736
|
+
createDataSource,
|
|
737
|
+
createCollectionDefinition,
|
|
738
|
+
validateProjectIR,
|
|
581
739
|
};
|
package/src/arc/planner.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const { inferSection, inferFieldName, buildFieldPath, classifyFieldType, uniqueP
|
|
|
5
5
|
const { classifyHref } = require('./adapters.cjs');
|
|
6
6
|
const { loadFingerprintBoost } = require('./learning.cjs');
|
|
7
7
|
const { appendStyleBindTransforms } = require('./style-candidates.cjs');
|
|
8
|
+
const { classifyDataCandidate, isEditableClassification } = require('./data-classification.cjs');
|
|
8
9
|
|
|
9
10
|
function recipeBoost(candidate, recipe) {
|
|
10
11
|
if (!recipe) return 0;
|
|
@@ -47,7 +48,18 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
47
48
|
0.99,
|
|
48
49
|
(candidate.confidence || 0) + recipeBoost(candidate, recipe) + (fingerprintHint.boost || 0),
|
|
49
50
|
);
|
|
50
|
-
const
|
|
51
|
+
const dataClass = classifyDataCandidate(candidate, {
|
|
52
|
+
file: candidate.file,
|
|
53
|
+
componentName: candidate.componentName,
|
|
54
|
+
tag: candidate.tag,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let decision = decideThreshold(confidence, candidate);
|
|
58
|
+
if (!isEditableClassification(dataClass.classification)) {
|
|
59
|
+
decision = 'skip';
|
|
60
|
+
candidate.reason = candidate.reason || `non-editable-${dataClass.classification.toLowerCase()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
const section = inferSection({
|
|
52
64
|
componentName: candidate.componentName,
|
|
53
65
|
fileName: candidate.file,
|
|
@@ -84,11 +96,13 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
84
96
|
fallback: candidate.value,
|
|
85
97
|
labelFallback: candidate.label,
|
|
86
98
|
fieldType: classifyFieldType(candidate.kind, candidate.value || candidate.label),
|
|
99
|
+
dataClassification: dataClass.classification,
|
|
87
100
|
section,
|
|
88
101
|
scope,
|
|
89
102
|
explain: {
|
|
90
103
|
detected: `${candidate.tag} ${candidate.kind}`,
|
|
91
104
|
why: candidate.reason,
|
|
105
|
+
classification: dataClass.classification,
|
|
92
106
|
recipe: recipe?.name || null,
|
|
93
107
|
confidence,
|
|
94
108
|
fingerprintBoost: fingerprintHint.boost || 0,
|
|
@@ -103,6 +117,7 @@ function planTransformations({ profile, analyses, recipe, ir }) {
|
|
|
103
117
|
reason: candidate.reason || 'low-confidence',
|
|
104
118
|
confidence,
|
|
105
119
|
kind: candidate.kind,
|
|
120
|
+
classification: dataClass.classification,
|
|
106
121
|
});
|
|
107
122
|
continue;
|
|
108
123
|
}
|