@deneb-ui/cli 2.0.91 → 2.0.93

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.
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
3
4
  const path = require('path');
4
5
  const { readJsonSafe, writeJson, deepMerge, isPlainObject } = require('./fs-utils.cjs');
5
6
  const { ARC_VERSION, SCHEMA_VERSION } = require('./version.cjs');
@@ -12,6 +13,7 @@ const {
12
13
  canonicalizeMarkerPath,
13
14
  wildcardPath,
14
15
  } = require('./fivora-contract.cjs');
16
+ const { sanitizeEditorSections } = require('./fivora-schema-authority.cjs');
15
17
 
16
18
  function pruneUnboundLeaves(content, isBound) {
17
19
  function walk(node, path) {
@@ -322,14 +324,24 @@ function collectStylesFromPlan(plan, existingStyles) {
322
324
  return styles;
323
325
  }
324
326
 
325
- function baseContent(projectName, routes) {
327
+ function baseContent(projectName, routes, projectDir) {
326
328
  const navLabels = {};
327
329
  for (const page of routes) navLabels[page.id] = page.label || page.id;
330
+ let logoUrl = '';
331
+ if (projectDir) {
332
+ const candidates = ['logo.svg', 'logo.png', 'logo-dark.svg', 'icon.svg', 'icon.png', 'fivora-logo.png'];
333
+ for (const cand of candidates) {
334
+ if (fs.existsSync(path.join(projectDir, 'public', cand))) {
335
+ logoUrl = `/${cand}`;
336
+ break;
337
+ }
338
+ }
339
+ }
328
340
  return {
329
341
  common: {
330
342
  websiteTitle: projectName,
331
343
  shortDescription: `A high-converting storefront built for the Fivora platform.`,
332
- logoUrl: '/fivora-logo.png',
344
+ logoUrl,
333
345
  headerCtaLabel: 'Contact Us',
334
346
  copyright: `© ${new Date().getFullYear()} ${projectName}. All rights reserved.`,
335
347
  navLabels,
@@ -519,7 +531,7 @@ function buildSiteDataAndManifest({
519
531
  ...(r.required ? { required: true } : {}),
520
532
  }));
521
533
 
522
- const content = baseContent(projectName, routes);
534
+ const content = baseContent(projectName, routes, projectDir);
523
535
 
524
536
  // Recipes may hint schema shape, but must not dump another storefront's content
525
537
  // into an unrelated project. Extracted values always win.
@@ -678,7 +690,7 @@ function buildSiteDataAndManifest({
678
690
  pages: routes,
679
691
  editorSchema: {
680
692
  version: 1,
681
- sections: editorSections,
693
+ sections: sanitizeEditorSections(editorSections),
682
694
  },
683
695
  };
684
696
 
@@ -6,6 +6,7 @@ const { classifyHref } = require('./adapters.cjs');
6
6
  const { loadFingerprintBoost } = require('./learning.cjs');
7
7
  const { appendStyleBindTransforms } = require('./style-candidates.cjs');
8
8
  const { classifyDataCandidate, isEditableClassification } = require('./data-classification.cjs');
9
+ const { recoverCandidateConfidence } = require('./confidence-recovery.cjs');
9
10
 
10
11
  function recipeBoost(candidate, recipe) {
11
12
  if (!recipe) return 0;
@@ -44,10 +45,21 @@ function planTransformations({ profile, analyses, recipe, ir }) {
44
45
  });
45
46
  continue;
46
47
  }
47
- const confidence = Math.min(
48
+ let confidence = Math.min(
48
49
  0.99,
49
50
  (candidate.confidence || 0) + recipeBoost(candidate, recipe) + (fingerprintHint.boost || 0),
50
51
  );
52
+
53
+ // Multi-stage recovery pass for low-confidence candidates (< 0.60)
54
+ let recovered = false;
55
+ if (confidence < CONFIDENCE.VALIDATE && !candidate.skip) {
56
+ const rec = recoverCandidateConfidence(candidate);
57
+ if (rec.recovered) {
58
+ confidence = rec.confidence;
59
+ recovered = true;
60
+ }
61
+ }
62
+
51
63
  const dataClass = classifyDataCandidate(candidate, {
52
64
  file: candidate.file,
53
65
  componentName: candidate.componentName,
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * TransformProof (P8, P9, P10)
5
+ *
6
+ * Formal evidence record for every AST mutation executed by ARC.
7
+ * Captures source provenance, target schema paths, preconditions,
8
+ * and multi-stage verification proofs.
9
+ */
10
+
11
+ class TransformProof {
12
+ constructor({
13
+ transformId,
14
+ type,
15
+ source = {},
16
+ target = {},
17
+ preconditions = [],
18
+ verification = {},
19
+ }) {
20
+ this.transformId = transformId || `tr_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
21
+ this.type = type; // 'TEXT_BIND' | 'ACTION_SPLIT' | 'COLLECTION_BIND' | 'IMAGE_BIND' | 'STYLE_BIND'
22
+ this.source = {
23
+ file: source.file || '',
24
+ loc: source.loc || '',
25
+ tag: source.tag || '',
26
+ originalText: source.originalText || '',
27
+ };
28
+ this.target = {
29
+ path: target.path || '',
30
+ type: target.type || 'text',
31
+ section: target.section || '',
32
+ };
33
+ this.preconditions = Array.isArray(preconditions) ? preconditions : [];
34
+ this.verification = {
35
+ astParsed: Boolean(verification.astParsed ?? true),
36
+ contractValid: Boolean(verification.contractValid ?? true),
37
+ runtimeTestable: Boolean(verification.runtimeTestable ?? true),
38
+ designPreserved: Boolean(verification.designPreserved ?? true),
39
+ };
40
+ this.timestamp = Date.now();
41
+ }
42
+
43
+ isVerified() {
44
+ return (
45
+ this.verification.astParsed &&
46
+ this.verification.contractValid &&
47
+ this.verification.designPreserved
48
+ );
49
+ }
50
+
51
+ toJSON() {
52
+ return {
53
+ transformId: this.transformId,
54
+ type: this.type,
55
+ source: this.source,
56
+ target: this.target,
57
+ preconditions: this.preconditions,
58
+ verification: this.verification,
59
+ timestamp: this.timestamp,
60
+ };
61
+ }
62
+ }
63
+
64
+ class ProofRegistry {
65
+ constructor() {
66
+ this.proofs = [];
67
+ }
68
+
69
+ record(proofData) {
70
+ const proof = new TransformProof(proofData);
71
+ this.proofs.push(proof);
72
+ return proof;
73
+ }
74
+
75
+ getAll() {
76
+ return [...this.proofs];
77
+ }
78
+
79
+ getVerified() {
80
+ return this.proofs.filter((p) => p.isVerified());
81
+ }
82
+
83
+ verifyPhaseInvariants({ plannedCount, boundCount, schemaPathCount }) {
84
+ const violations = [];
85
+
86
+ // Invariant 1: Every planned transformation must emit a proof
87
+ if (this.proofs.length < plannedCount) {
88
+ violations.push(`Phase Invariant Violation: Planned ${plannedCount} transforms, but only emitted ${this.proofs.length} proofs.`);
89
+ }
90
+
91
+ // Invariant 2: Every verified proof must have a non-empty target path
92
+ const emptyTargets = this.proofs.filter((p) => !p.target.path);
93
+ if (emptyTargets.length > 0) {
94
+ violations.push(`Phase Invariant Violation: Found ${emptyTargets.length} proofs with empty target paths.`);
95
+ }
96
+
97
+ return {
98
+ passed: violations.length === 0,
99
+ violations,
100
+ };
101
+ }
102
+
103
+ toJSON() {
104
+ return this.proofs.map((p) => (typeof p.toJSON === 'function' ? p.toJSON() : p));
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ TransformProof,
110
+ ProofRegistry,
111
+ };
@@ -12,6 +12,7 @@ const {
12
12
  } = require('../../ast.cjs');
13
13
  const { BROAD_CONTENT_CONTAINERS } = require('../../fivora-contract.cjs');
14
14
  const { injectSiteDataHook } = require('../runtime/provider.cjs');
15
+ const { ensureClientDirective } = require('../../rsc-boundary.cjs');
15
16
 
16
17
  function sanitizeContradictoryMarkers(ast) {
17
18
  let cleaned = 0;
@@ -378,11 +379,37 @@ function healDecorativeOverlays(ast) {
378
379
 
379
380
  function healMissingSiteDataHooks(code, filePath = 'file.tsx') {
380
381
  if (!code.includes('siteData')) return code;
382
+
383
+ // Never touch files mounting SiteDataProvider
384
+ if (code.includes('SiteDataProvider')) return code;
385
+
386
+ // Never touch Server Component layouts or metadata files
387
+ const isLayout = /(?:^|[/\\])layout\.(?:tsx|jsx|js|ts)$/.test(filePath);
388
+ const isClient = /['"]use client['"]/.test(code);
389
+ if (isLayout && !isClient) return code;
390
+
391
+ // Never touch plain utilities or config files that contain zero JSX
392
+ const hasJsx = /<[a-zA-Z][\s\S]*?>/.test(code);
393
+ const isHookFile = /(?:^|[/\\])use[A-Z]/.test(filePath);
394
+ if (!hasJsx && !isHookFile) return code;
395
+
381
396
  try {
382
397
  const ast = parseSource(code, filePath);
383
- const injected = injectSiteDataHook(ast);
398
+ const fileContext = {
399
+ filePath,
400
+ isServerComponent: !isClient,
401
+ isClientComponent: isClient,
402
+ mountsProvider: code.includes('SiteDataProvider'),
403
+ };
404
+ const injected = injectSiteDataHook(ast, filePath, fileContext);
384
405
  if (injected) {
385
- ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
406
+ if (!isClient) {
407
+ ensureClientDirective(ast);
408
+ }
409
+ const importSpecifier = code.includes('@/lib/siteDataContext')
410
+ ? '@/lib/siteDataContext'
411
+ : '@deneb-ui/ui';
412
+ ensureImport(ast, importSpecifier, ['useSiteData']);
386
413
  return printSource(ast, code);
387
414
  }
388
415
  } catch {}
@@ -7,9 +7,25 @@ const {
7
7
  } = require('../../ast.cjs');
8
8
 
9
9
  function replaceTextChildren(node, fieldPath, fallback, fieldType) {
10
+ const children = node.children || [];
11
+ const hasDynamicExpr = children.some(
12
+ (c) =>
13
+ c &&
14
+ c.type === 'JSXExpressionContainer' &&
15
+ c.expression &&
16
+ c.expression.type !== 'StringLiteral' &&
17
+ c.expression.type !== 'Literal'
18
+ );
19
+
20
+ // If there are dynamic runtime expressions, preserve them and wrap the static literal text
21
+ if (hasDynamicExpr) {
22
+ wrapLiteralTextChildren(node, fieldPath, fallback);
23
+ return;
24
+ }
25
+
10
26
  const nextChildren = [];
11
27
  let replaced = false;
12
- for (const child of node.children || []) {
28
+ for (const child of children) {
13
29
  if (!child) continue;
14
30
  if (child.type === 'JSXText' && child.value.replace(/\s+/g, '').length) {
15
31
  nextChildren.push(b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType)));
@@ -35,7 +51,7 @@ function replaceTextChildren(node, fieldPath, fallback, fieldType) {
35
51
 
36
52
  /**
37
53
  * Replaces literal text children with an editable <span>, leaving the container
38
- * element and every one of its classes untouched.
54
+ * element, icons, and dynamic expressions untouched.
39
55
  */
40
56
  function wrapLiteralTextChildren(node, fieldPath, fallback) {
41
57
  const nextChildren = [];
@@ -27,11 +27,9 @@ function inferPageKey(relativeFile) {
27
27
  * rather than being guessed from the filename — Pages Router uses
28
28
  * `pages/contact.jsx`, App Router uses `app/contact/page.tsx`.
29
29
  *
30
- * `<main>` is preferred, but any project shape is supported by falling back to
31
- * the outermost element the page component returns.
30
+ * <main> is preferred, then any host element (div/section), then any root component element.
32
31
  */
33
32
  function instrumentPageKey(code, relativeFile, pageKey) {
34
- if (code.includes('data-preview-page-key')) return { code, updated: false };
35
33
  const key = pageKey || inferPageKey(relativeFile);
36
34
 
37
35
  let ast;
@@ -41,40 +39,61 @@ function instrumentPageKey(code, relativeFile, pageKey) {
41
39
  return { code, updated: false };
42
40
  }
43
41
 
44
- const attribute = () =>
45
- b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key));
46
-
42
+ let existingAttr = null;
47
43
  let target = null;
44
+ let fallbackTarget = null;
45
+
48
46
  recast.types.visit(ast, {
49
47
  visitJSXOpeningElement(pathNode) {
48
+ const attrs = pathNode.node.attributes || [];
49
+ const pageKeyAttr = attrs.find(
50
+ (a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-page-key'
51
+ );
52
+ if (pageKeyAttr) {
53
+ existingAttr = pageKeyAttr;
54
+ return false;
55
+ }
56
+
50
57
  const name = pathNode.node.name && pathNode.node.name.name;
51
58
  if (!target && name === 'main') {
52
59
  target = pathNode.node;
53
- return false;
60
+ } else if (!fallbackTarget && typeof name === 'string' && /^[a-z]/.test(name)) {
61
+ fallbackTarget = pathNode.node;
62
+ } else if (!fallbackTarget) {
63
+ // Root custom component (e.g. <PlatformProductDetail />)
64
+ fallbackTarget = pathNode.node;
54
65
  }
66
+
55
67
  this.traverse(pathNode);
56
68
  },
57
69
  });
58
70
 
59
- if (!target) {
60
- // No <main>: use the first host element of the returned tree so the marker
61
- // still lands inside this route's exported HTML.
62
- recast.types.visit(ast, {
63
- visitJSXElement(pathNode) {
64
- if (target) return false;
65
- const name = getJsxName(pathNode.node);
66
- if (typeof name === 'string' && /^[a-z]/.test(name)) {
67
- target = pathNode.node.openingElement;
68
- return false;
69
- }
70
- this.traverse(pathNode);
71
- },
72
- });
71
+ // If already has data-preview-page-key, ensure its value matches the expected page key
72
+ if (existingAttr) {
73
+ let currentValue = null;
74
+ if (existingAttr.value) {
75
+ if (existingAttr.value.type === 'StringLiteral') {
76
+ currentValue = existingAttr.value.value;
77
+ } else if (existingAttr.value.type === 'JSXExpressionContainer' && existingAttr.value.expression.type === 'StringLiteral') {
78
+ currentValue = existingAttr.value.expression.value;
79
+ }
80
+ }
81
+ if (currentValue === key) {
82
+ return { code, updated: false };
83
+ }
84
+ // Update to correct key
85
+ existingAttr.value = b.stringLiteral(key);
86
+ return { code: printSource(ast, code), updated: true };
73
87
  }
74
88
 
75
- if (!target) return { code, updated: false };
76
- target.attributes = target.attributes || [];
77
- target.attributes.unshift(attribute());
89
+ const finalTarget = target || fallbackTarget;
90
+ if (!finalTarget) return { code, updated: false };
91
+
92
+ finalTarget.attributes = finalTarget.attributes || [];
93
+ finalTarget.attributes.unshift(
94
+ b.jsxAttribute(b.jsxIdentifier('data-preview-page-key'), b.stringLiteral(key))
95
+ );
96
+
78
97
  return { code: printSource(ast, code), updated: true };
79
98
  }
80
99
 
@@ -77,11 +77,24 @@ function functionReferencesSiteData(fn) {
77
77
  return found;
78
78
  }
79
79
 
80
- function injectSiteDataHook(ast) {
80
+ const {
81
+ SCOPE_STATUS,
82
+ classifySiteDataRequirement,
83
+ } = require('./site-data-scope.cjs');
84
+
85
+ function injectSiteDataHook(ast, filePath = 'file.tsx', options = {}) {
81
86
  let injected = false;
82
87
  let anyFunctionReferencedSiteData = false;
83
88
  let alreadyHasUseSiteData = false;
84
89
 
90
+ const fileContext = {
91
+ filePath,
92
+ isServerComponent: options.isServerComponent ?? false,
93
+ isClientComponent: options.isClientComponent ?? false,
94
+ mountsProvider: options.mountsProvider ?? false,
95
+ };
96
+
97
+ // Inspect file-level AST properties
85
98
  recast.types.visit(ast, {
86
99
  visitCallExpression(pathNode) {
87
100
  if (pathNode.node.callee && pathNode.node.callee.name === 'useSiteData') {
@@ -90,13 +103,35 @@ function injectSiteDataHook(ast) {
90
103
  }
91
104
  this.traverse(pathNode);
92
105
  },
106
+ visitJSXElement(pathNode) {
107
+ const opening = pathNode.node.openingElement;
108
+ if (opening && opening.name) {
109
+ const name = opening.name.name;
110
+ if (name === 'SiteDataProvider' || name === 'BaseSiteDataProvider' || name === 'DenebDataProvider') {
111
+ fileContext.mountsProvider = true;
112
+ }
113
+ }
114
+ this.traverse(pathNode);
115
+ },
93
116
  });
94
117
 
95
- function injectIntoFunction(fn) {
118
+ // If this file mounts SiteDataProvider, consumer hook injection is forbidden
119
+ if (fileContext.mountsProvider) {
120
+ return false;
121
+ }
122
+
123
+ function injectIntoFunction(fn, fnName) {
96
124
  if (!fn || !fn.body || fn.body.type !== 'BlockStatement') return false;
125
+
126
+ const requirement = classifySiteDataRequirement(fn, fnName, fileContext);
127
+ if (requirement !== SCOPE_STATUS.HOOK_SAFE) {
128
+ return false;
129
+ }
130
+
97
131
  const body = fn.body.body;
98
132
  const already = body.some((stmt) => recast.print(stmt).code.includes('useSiteData'));
99
133
  if (already) return false;
134
+
100
135
  const hook = b.variableDeclaration('const', [
101
136
  b.variableDeclarator(
102
137
  b.identifier('siteData'),
@@ -108,40 +143,45 @@ function injectSiteDataHook(ast) {
108
143
  return true;
109
144
  }
110
145
 
111
- // Pass 1: Inject useSiteData() into EVERY component function that references siteData
146
+ // Pass 1: Inject useSiteData() into component functions that reference siteData
112
147
  recast.types.visit(ast, {
113
148
  visitFunctionDeclaration(pathNode) {
149
+ const name = pathNode.node.id ? pathNode.node.id.name : null;
114
150
  if (functionReferencesSiteData(pathNode.node)) {
115
151
  anyFunctionReferencedSiteData = true;
116
- if (injectIntoFunction(pathNode.node)) injected = true;
152
+ if (injectIntoFunction(pathNode.node, name)) injected = true;
117
153
  }
118
154
  this.traverse(pathNode);
119
155
  },
120
156
  visitFunctionExpression(pathNode) {
157
+ const parent = pathNode.parent && pathNode.parent.node;
158
+ const name = parent && parent.type === 'VariableDeclarator' && parent.id ? parent.id.name : null;
121
159
  if (functionReferencesSiteData(pathNode.node)) {
122
160
  anyFunctionReferencedSiteData = true;
123
- if (injectIntoFunction(pathNode.node)) injected = true;
161
+ if (injectIntoFunction(pathNode.node, name)) injected = true;
124
162
  }
125
163
  this.traverse(pathNode);
126
164
  },
127
165
  visitArrowFunctionExpression(pathNode) {
166
+ const parent = pathNode.parent && pathNode.parent.node;
167
+ const name = parent && parent.type === 'VariableDeclarator' && parent.id ? parent.id.name : null;
128
168
  if (functionReferencesSiteData(pathNode.node)) {
129
169
  anyFunctionReferencedSiteData = true;
130
- if (injectIntoFunction(pathNode.node)) injected = true;
170
+ if (injectIntoFunction(pathNode.node, name)) injected = true;
131
171
  }
132
172
  this.traverse(pathNode);
133
173
  },
134
174
  });
135
175
 
136
- // Pass 2: If no component references siteData yet (e.g. before subsequent AST edits),
137
- // ensure the primary component receives useSiteData()
176
+ // Pass 2: If no component references siteData yet, ensure the primary component receives useSiteData()
138
177
  if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
139
178
  recast.types.visit(ast, {
140
179
  visitExportDefaultDeclaration(pathNode) {
141
180
  if (injected) return false;
142
181
  const decl = pathNode.node.declaration;
143
182
  if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
144
- injected = injectIntoFunction(decl);
183
+ const name = decl.id ? decl.id.name : 'DefaultExport';
184
+ injected = injectIntoFunction(decl, name);
145
185
  }
146
186
  this.traverse(pathNode);
147
187
  },
@@ -149,7 +189,7 @@ function injectSiteDataHook(ast) {
149
189
  if (injected) return false;
150
190
  const name = pathNode.node.id && pathNode.node.id.name;
151
191
  if (name && /^[A-Z]/.test(name)) {
152
- injected = injectIntoFunction(pathNode.node);
192
+ injected = injectIntoFunction(pathNode.node, name);
153
193
  return false;
154
194
  }
155
195
  this.traverse(pathNode);
@@ -159,7 +199,7 @@ function injectSiteDataHook(ast) {
159
199
  const id = pathNode.node.id;
160
200
  const init = pathNode.node.init;
161
201
  if (id && id.type === 'Identifier' && /^[A-Z]/.test(id.name) && init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')) {
162
- injected = injectIntoFunction(init);
202
+ injected = injectIntoFunction(init, id.name);
163
203
  return false;
164
204
  }
165
205
  this.traverse(pathNode);
@@ -167,16 +207,6 @@ function injectSiteDataHook(ast) {
167
207
  });
168
208
  }
169
209
 
170
- if (!injected && !anyFunctionReferencedSiteData && !alreadyHasUseSiteData) {
171
- recast.types.visit(ast, {
172
- visitFunctionDeclaration(pathNode) {
173
- if (injected) return false;
174
- injected = injectIntoFunction(pathNode.node);
175
- return false;
176
- },
177
- });
178
- }
179
-
180
210
  return injected;
181
211
  }
182
212