@deneb-ui/cli 2.0.88 → 2.0.89

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 (54) 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
@@ -449,7 +449,29 @@ function applyResidualPass({ code, file, ownerScope, usedPaths, componentName, r
449
449
  };
450
450
  }
451
451
 
452
+ const RESIDUAL_REASONS = {
453
+ STATIC_INTENTIONAL: 'STATIC_INTENTIONAL',
454
+ STATIC_DECORATIVE: 'STATIC_DECORATIVE',
455
+ PLATFORM_CONTROLLED: 'PLATFORM_CONTROLLED',
456
+ RUNTIME_DATA: 'RUNTIME_DATA',
457
+ UNSUPPORTED_SAFE: 'UNSUPPORTED_SAFE',
458
+ CONVERSION_FAILED: 'CONVERSION_FAILED',
459
+ };
460
+
461
+ function normalizeResidualReason(rawReason) {
462
+ if (!rawReason) return RESIDUAL_REASONS.STATIC_DECORATIVE;
463
+ const lower = String(rawReason).toLowerCase();
464
+ if (lower.includes('fail') || lower.includes('error')) return RESIDUAL_REASONS.CONVERSION_FAILED;
465
+ if (lower.includes('platform') || lower.includes('control')) return RESIDUAL_REASONS.PLATFORM_CONTROLLED;
466
+ if (lower.includes('runtime') || lower.includes('session') || lower.includes('cart')) return RESIDUAL_REASONS.RUNTIME_DATA;
467
+ if (lower.includes('intentional') || lower.includes('brand')) return RESIDUAL_REASONS.STATIC_INTENTIONAL;
468
+ if (lower.includes('unsupported') || lower.includes('complex')) return RESIDUAL_REASONS.UNSUPPORTED_SAFE;
469
+ return RESIDUAL_REASONS.STATIC_DECORATIVE;
470
+ }
471
+
452
472
  module.exports = {
453
473
  applyResidualPass,
454
474
  isMeaningfulVisibleText,
475
+ RESIDUAL_REASONS,
476
+ normalizeResidualReason,
455
477
  };
@@ -4,8 +4,9 @@ const recast = require('recast');
4
4
  const b = recast.types.builders;
5
5
 
6
6
  /**
7
- * RSC (React Server Components) Boundary Intelligence for Next.js App Router.
8
- * Ensures proper placement of 'use client' directives and prevents metadata export conflicts.
7
+ * Deneb ARC v3 — Conservative RSC (React Server Components) Boundary Optimizer.
8
+ * Ensures proper placement of 'use client' directives, minimal boundary isolation,
9
+ * metadata export protection, and RSC metrics tracking.
9
10
  */
10
11
 
11
12
  /**
@@ -54,6 +55,37 @@ function hasMetadataExport(ast) {
54
55
  return hasMetadata;
55
56
  }
56
57
 
58
+ /**
59
+ * Detects if the AST defines an async component (async function Page).
60
+ * Async components in Next.js are strictly server-side and cannot have 'use client'.
61
+ */
62
+ function isAsyncServerComponent(ast) {
63
+ let isAsync = false;
64
+
65
+ recast.types.visit(ast, {
66
+ visitExportDefaultDeclaration(pathNode) {
67
+ const decl = pathNode.node.declaration;
68
+ if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
69
+ if (decl.async) {
70
+ isAsync = true;
71
+ return false;
72
+ }
73
+ }
74
+ this.traverse(pathNode);
75
+ },
76
+ visitFunctionDeclaration(pathNode) {
77
+ const name = pathNode.node.id?.name;
78
+ if (name && /^(Page|Layout|Template|[A-Z])/.test(name) && pathNode.node.async) {
79
+ isAsync = true;
80
+ return false;
81
+ }
82
+ this.traverse(pathNode);
83
+ },
84
+ });
85
+
86
+ return isAsync;
87
+ }
88
+
57
89
  /**
58
90
  * Detects if the AST has a 'use client' directive.
59
91
  */
@@ -105,7 +137,7 @@ function needsClientDirective(ast, code = '') {
105
137
  * Determines whether 'use client' can be safely injected into the file.
106
138
  * Returns false if:
107
139
  * 1. File exports metadata or generateMetadata.
108
- * 2. File is a layout with metadata.
140
+ * 2. File is an async server component.
109
141
  */
110
142
  function canSafelyInjectClientDirective(ast, relativeFile = '', profile = {}) {
111
143
  const isAppRouter = profile.router === 'next-app' || (profile.appDir && relativeFile.startsWith(profile.appDir));
@@ -113,7 +145,7 @@ function canSafelyInjectClientDirective(ast, relativeFile = '', profile = {}) {
113
145
  return true;
114
146
  }
115
147
 
116
- if (hasMetadataExport(ast)) {
148
+ if (hasMetadataExport(ast) || isAsyncServerComponent(ast)) {
117
149
  return false;
118
150
  }
119
151
 
@@ -145,10 +177,79 @@ function ensureClientDirective(ast) {
145
177
  return true;
146
178
  }
147
179
 
180
+ /**
181
+ * Calculates the smallest client-editable boundary.
182
+ * Prevents turning entire server page trees into client components.
183
+ */
184
+ function calculateMinimalClientBoundary(componentInfo = {}) {
185
+ if (componentInfo.isAsync || componentInfo.hasMetadata) {
186
+ return 'server';
187
+ }
188
+ if (componentInfo.hasHooks || componentInfo.hasEvents) {
189
+ return 'client';
190
+ }
191
+ return componentInfo.isLeaf ? 'client' : 'server';
192
+ }
193
+
194
+ /**
195
+ * Computes RSC metrics across the compilation run.
196
+ */
197
+ function computeRscMetrics(files = []) {
198
+ let clientBoundariesBefore = 0;
199
+ let clientBoundariesAfter = 0;
200
+ let serverComponentsConverted = 0;
201
+ let serverComponentsPreserved = 0;
202
+
203
+ for (const f of files) {
204
+ if (f.hadClientDirective) clientBoundariesBefore++;
205
+ if (f.hasClientDirective) clientBoundariesAfter++;
206
+
207
+ if (!f.hadClientDirective && f.hasClientDirective) {
208
+ serverComponentsConverted++;
209
+ } else if (!f.hasClientDirective) {
210
+ serverComponentsPreserved++;
211
+ }
212
+ }
213
+
214
+ const total = files.length || 1;
215
+ const minimalBoundarySuccessRate = Number(((serverComponentsPreserved / total) * 100).toFixed(2));
216
+ const conversionExceededThreshold = serverComponentsConverted > 10;
217
+
218
+ return {
219
+ clientBoundariesBefore,
220
+ clientBoundariesAfter,
221
+ serverComponentsConverted,
222
+ serverComponentsPreserved,
223
+ minimalBoundarySuccessRate,
224
+ conversionExceededThreshold,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Validates RSC metrics against threshold limits.
230
+ */
231
+ function validateRscBoundaryIntegrity(metrics, maxAllowedConversions = 10) {
232
+ if (metrics.serverComponentsConverted > maxAllowedConversions) {
233
+ return {
234
+ valid: false,
235
+ error: `Excessive RSC conversions: ${metrics.serverComponentsConverted} server components converted to client (max allowed: ${maxAllowedConversions})`,
236
+ metrics,
237
+ };
238
+ }
239
+ return {
240
+ valid: true,
241
+ metrics,
242
+ };
243
+ }
244
+
148
245
  module.exports = {
149
246
  hasMetadataExport,
150
247
  hasClientDirective,
248
+ isAsyncServerComponent,
151
249
  needsClientDirective,
152
250
  canSafelyInjectClientDirective,
153
251
  ensureClientDirective,
252
+ calculateMinimalClientBoundary,
253
+ computeRscMetrics,
254
+ validateRscBoundaryIntegrity,
154
255
  };
@@ -269,6 +269,70 @@ class RuntimeValidator {
269
269
  }
270
270
  }
271
271
 
272
+ function validateCollectionOperations(siteData = {}) {
273
+ const content = siteData.content || siteData;
274
+ const results = [];
275
+ let allPassed = true;
276
+
277
+ function findCollections(obj, prefix = '') {
278
+ for (const [k, v] of Object.entries(obj || {})) {
279
+ const fullPath = prefix ? `${prefix}.${k}` : k;
280
+ if (Array.isArray(v)) {
281
+ // Test 1: Add Item
282
+ const cloned = JSON.parse(JSON.stringify(v));
283
+ const sampleItem = cloned[0] && typeof cloned[0] === 'object' ? { ...cloned[0], id: 'new-999' } : 'new-item';
284
+ cloned.push(sampleItem);
285
+ const addPassed = cloned.length === v.length + 1;
286
+
287
+ // Test 2: Remove Item
288
+ const removeCloned = JSON.parse(JSON.stringify(v));
289
+ removeCloned.pop();
290
+ const removePassed = removeCloned.length === Math.max(0, v.length - 1);
291
+
292
+ // Test 3: Reorder Items
293
+ const reorderCloned = JSON.parse(JSON.stringify(v));
294
+ reorderCloned.reverse();
295
+ const reorderPassed = reorderCloned.length === v.length;
296
+
297
+ // Test 4: Mutate Item Field
298
+ let mutatePassed = true;
299
+ if (v.length > 0 && typeof v[0] === 'object' && v[0] !== null) {
300
+ const firstKey = Object.keys(v[0])[0];
301
+ if (firstKey) {
302
+ const mutateCloned = JSON.parse(JSON.stringify(v));
303
+ mutateCloned[0][firstKey] = '__MUTATED__';
304
+ mutatePassed = mutateCloned[0][firstKey] === '__MUTATED__';
305
+ }
306
+ }
307
+
308
+ const colPassed = addPassed && removePassed && reorderPassed && mutatePassed;
309
+ if (!colPassed) allPassed = false;
310
+
311
+ results.push({
312
+ path: fullPath,
313
+ itemCount: v.length,
314
+ operations: {
315
+ addItem: addPassed,
316
+ removeItem: removePassed,
317
+ reorderItems: reorderPassed,
318
+ mutateItemField: mutatePassed,
319
+ },
320
+ passed: colPassed,
321
+ });
322
+ } else if (typeof v === 'object' && v !== null) {
323
+ findCollections(v, fullPath);
324
+ }
325
+ }
326
+ }
327
+
328
+ findCollections(content);
329
+ return {
330
+ passed: allPassed,
331
+ collections: results,
332
+ totalCollections: results.length,
333
+ };
334
+ }
335
+
272
336
  function validateRuntimeEditabilitySync(options) {
273
337
  const validator = new RuntimeValidator(options);
274
338
  return validator.runSync();
@@ -283,4 +347,5 @@ module.exports = {
283
347
  RuntimeValidator,
284
348
  validateRuntimeEditability,
285
349
  validateRuntimeEditabilitySync,
350
+ validateCollectionOperations,
286
351
  };
@@ -530,7 +530,16 @@ function collectLooseText(code) {
530
530
  return /<(h[1-6]|p|Button|span)[^>]*>\s*[A-Za-z]/.test(code);
531
531
  }
532
532
 
533
- function analyzeFile({ code, relativeFile, profile, graph, ownerScope, componentMeta, ir = profile?.ir || null }) {
533
+ function analyzeFile(optionsOrFile, codeArg, profileArg) {
534
+ let code, relativeFile, profile, graph, ownerScope, componentMeta, ir;
535
+ if (typeof optionsOrFile === 'string') {
536
+ relativeFile = optionsOrFile;
537
+ code = codeArg;
538
+ profile = profileArg;
539
+ ir = profile?.ir || null;
540
+ } else {
541
+ ({ code, relativeFile, profile, graph, ownerScope, componentMeta, ir = profile?.ir || null } = optionsOrFile || {});
542
+ }
534
543
  const adapters = activeAdapters(profile);
535
544
  const fileSkip = skipReasonForFile(relativeFile, code);
536
545
  if (fileSkip) {
@@ -1165,6 +1174,8 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
1165
1174
  alreadyEditable: fileAlreadyEditable(code),
1166
1175
  bindings: Object.fromEntries([...bindings.entries()].filter(([, v]) => typeof v === 'string')),
1167
1176
  imports,
1177
+ code,
1178
+ relativeFile,
1168
1179
  };
1169
1180
  }
1170
1181