@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.
Files changed (55) hide show
  1. package/bin/index.js +31 -5
  2. package/package.json +2 -2
  3. package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
  4. package/src/arc/__tests__/arc.test.cjs +922 -0
  5. package/src/arc/__tests__/canonical.test.cjs +57 -0
  6. package/src/arc/__tests__/classification.test.cjs +54 -0
  7. package/src/arc/__tests__/corpus.test.cjs +144 -0
  8. package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
  9. package/src/arc/__tests__/fuzz.test.cjs +106 -0
  10. package/src/arc/__tests__/interaction.test.cjs +85 -0
  11. package/src/arc/__tests__/visual-regression.test.cjs +99 -0
  12. package/src/arc/acceptance-gates.cjs +119 -0
  13. package/src/arc/canonical-paths.cjs +142 -23
  14. package/src/arc/corpus-verifier.cjs +295 -0
  15. package/src/arc/data-classification.cjs +182 -0
  16. package/src/arc/data-flow.cjs +62 -2
  17. package/src/arc/explain-blocked.cjs +388 -0
  18. package/src/arc/fuzz-engine.cjs +604 -0
  19. package/src/arc/index.cjs +148 -2
  20. package/src/arc/interaction-verifier.cjs +185 -0
  21. package/src/arc/ir-builder.cjs +170 -12
  22. package/src/arc/planner.cjs +16 -1
  23. package/src/arc/residual.cjs +22 -0
  24. package/src/arc/rsc-boundary.cjs +105 -4
  25. package/src/arc/runtime-validator.cjs +65 -0
  26. package/src/arc/semantic.cjs +12 -1
  27. package/src/arc/transformer.cjs +14 -2745
  28. package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
  29. package/src/arc/transforms/collections/list.cjs +474 -0
  30. package/src/arc/transforms/components/child-card.cjs +268 -0
  31. package/src/arc/transforms/components/compound-content.cjs +49 -0
  32. package/src/arc/transforms/components/reusable-component.cjs +173 -0
  33. package/src/arc/transforms/element-transform.cjs +192 -0
  34. package/src/arc/transforms/healing/conditionals.cjs +295 -0
  35. package/src/arc/transforms/healing/sanitizer.cjs +401 -0
  36. package/src/arc/transforms/index.cjs +195 -0
  37. package/src/arc/transforms/primitives/action.cjs +76 -0
  38. package/src/arc/transforms/primitives/text.cjs +65 -0
  39. package/src/arc/transforms/routing/page-key.cjs +84 -0
  40. package/src/arc/transforms/routing/shared-layout.cjs +294 -0
  41. package/src/arc/transforms/runtime/provider.cjs +230 -0
  42. package/src/arc/transforms/transform-context.cjs +118 -0
  43. package/src/arc/types/acceptance.d.ts +50 -0
  44. package/src/arc/types/canonical.d.ts +50 -0
  45. package/src/arc/types/classification.d.ts +42 -0
  46. package/src/arc/types/corpus.d.ts +52 -0
  47. package/src/arc/types/explain-blocked.d.ts +41 -0
  48. package/src/arc/types/fuzz.d.ts +74 -0
  49. package/src/arc/types/interaction.d.ts +43 -0
  50. package/src/arc/types/ir.d.ts +114 -0
  51. package/src/arc/types/rsc.d.ts +39 -0
  52. package/src/arc/types/visual-regression.d.ts +49 -0
  53. package/src/arc/visual-regression.cjs +195 -0
  54. package/src/arc/workspace.cjs +233 -0
  55. 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: Boolean(raw.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 ?? true };
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
+ };
@@ -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 v2 — Project-Wide Intermediate Representation (IR) Builder
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
- return {
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
  };
@@ -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 decision = decideThreshold(confidence, candidate);
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
  }