@deneb-ui/cli 2.0.87 → 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 (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/tools/deneb-template-validator.cjs +17 -17
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const contract = require('./fivora-contract.cjs');
6
+ const { validateRuntimeEditabilitySync, validateCollectionOperations } = require('./runtime-validator.cjs');
7
+ const { evaluateAcceptanceGates } = require('./acceptance-gates.cjs');
8
+ const { resolveSiteDataFile } = require('./corpus-verifier.cjs');
9
+
10
+ const IGNORED_SOURCE_DIRS = new Set([
11
+ 'node_modules',
12
+ 'out',
13
+ 'dist',
14
+ 'build',
15
+ '.next',
16
+ '.turbo',
17
+ '.cache',
18
+ ]);
19
+
20
+ function collectSourceFiles(dir) {
21
+ const sources = [];
22
+ function walk(current) {
23
+ if (!fs.existsSync(current)) return;
24
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
25
+ const lower = entry.name.toLowerCase();
26
+ if (IGNORED_SOURCE_DIRS.has(lower) || entry.name.startsWith('.deneb') || entry.name.startsWith('.')) continue;
27
+ const abs = path.join(current, entry.name);
28
+ if (entry.isDirectory()) {
29
+ walk(abs);
30
+ } else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
31
+ try {
32
+ sources.push({
33
+ abs,
34
+ rel: path.relative(dir, abs).replace(/\\/g, '/'),
35
+ code: fs.readFileSync(abs, 'utf8'),
36
+ });
37
+ } catch {}
38
+ }
39
+ }
40
+ }
41
+ walk(dir);
42
+ return sources;
43
+ }
44
+
45
+ function findLineInCode(code, searchText) {
46
+ if (!code || !searchText) return 1;
47
+ const lines = code.split('\n');
48
+ for (let i = 0; i < lines.length; i++) {
49
+ if (lines[i].includes(searchText)) {
50
+ return i + 1;
51
+ }
52
+ }
53
+ return 1;
54
+ }
55
+
56
+ function findBestSourceForPath(sources, fieldPath) {
57
+ if (!fieldPath) return null;
58
+ const parts = fieldPath.split('.');
59
+ const lastPart = parts[parts.length - 1];
60
+ const sectionPart = parts[1] || parts[0];
61
+
62
+ for (const s of sources) {
63
+ const baseName = path.basename(s.rel).toLowerCase();
64
+ if (baseName.includes(sectionPart.toLowerCase()) || (lastPart && baseName.includes(lastPart.toLowerCase()))) {
65
+ return s;
66
+ }
67
+ if (s.code.includes(lastPart)) {
68
+ return s;
69
+ }
70
+ }
71
+ return sources[0] || null;
72
+ }
73
+
74
+ /**
75
+ * Analyzes why a project conversion was blocked and compiles structured, actionable findings.
76
+ */
77
+ function analyzeBlockedProject(targetDir = process.cwd(), options = {}) {
78
+ const projectDir = path.resolve(targetDir);
79
+ const projectName = path.basename(projectDir);
80
+ const findings = [];
81
+
82
+ // 1. Inspect recent run artifacts in .deneb/runs/ if present
83
+ let runReasons = [];
84
+ const runsDir = path.join(projectDir, '.deneb', 'runs');
85
+ if (fs.existsSync(runsDir)) {
86
+ try {
87
+ const runs = fs.readdirSync(runsDir).sort();
88
+ const latestRun = runs[runs.length - 1];
89
+ if (latestRun) {
90
+ const rollbackFile = path.join(runsDir, latestRun, 'reports', 'rollback-reason.json');
91
+ if (fs.existsSync(rollbackFile)) {
92
+ const rollbackData = JSON.parse(fs.readFileSync(rollbackFile, 'utf8'));
93
+ if (Array.isArray(rollbackData.reasons)) {
94
+ runReasons = rollbackData.reasons;
95
+ }
96
+ }
97
+ }
98
+ } catch {}
99
+ }
100
+
101
+ // 2. Inspect fivora-template.json
102
+ const manifestPath = path.join(projectDir, 'fivora-template.json');
103
+ let manifest = null;
104
+ let manifestRaw = '';
105
+
106
+ if (!fs.existsSync(manifestPath)) {
107
+ findings.push({
108
+ ruleId: 'MISSING_MANIFEST',
109
+ category: 'FIVORA_CONTRACT',
110
+ severity: 'BLOCKING',
111
+ file: 'fivora-template.json',
112
+ line: 1,
113
+ message: 'fivora-template.json is missing in project root.',
114
+ remediation: "Run 'npx @deneb-ui/cli init' to generate the mandatory Fivora template manifest.",
115
+ });
116
+ } else {
117
+ try {
118
+ manifestRaw = fs.readFileSync(manifestPath, 'utf8');
119
+ manifest = JSON.parse(manifestRaw);
120
+ } catch (err) {
121
+ findings.push({
122
+ ruleId: 'CORRUPT_MANIFEST_JSON',
123
+ category: 'FIVORA_CONTRACT',
124
+ severity: 'BLOCKING',
125
+ file: 'fivora-template.json',
126
+ line: 1,
127
+ message: `Corrupted JSON in fivora-template.json: ${err.message}`,
128
+ remediation: 'Ensure fivora-template.json is valid JSON with proper quotes and commas.',
129
+ });
130
+ }
131
+ }
132
+
133
+ // 3. Inspect editorSchema uniqueness & types using authoritative Fivora contract
134
+ if (manifest && manifest.editorSchema) {
135
+ const schemaErrors = contract.auditSchemaUniqueness(manifest.editorSchema);
136
+ for (const sErr of schemaErrors) {
137
+ if (sErr.includes('declares unsupported type')) {
138
+ const typeMatch = sErr.match(/unsupported type "([^"]+)"/);
139
+ const badType = typeMatch ? typeMatch[1] : 'unknown';
140
+ const line = findLineInCode(manifestRaw, `"${badType}"`);
141
+ findings.push({
142
+ ruleId: 'SCHEMA_UNSUPPORTED_TYPE',
143
+ category: 'FIVORA_CONTRACT',
144
+ severity: 'BLOCKING',
145
+ file: 'fivora-template.json',
146
+ line,
147
+ snippet: `"type": "${badType}"`,
148
+ message: sErr,
149
+ remediation: `Fivora schema requires primitive types (text, textarea, email, tel, url, image, number, boolean, select). Replace type '${badType}' with 'number' or 'text'.`,
150
+ });
151
+ } else {
152
+ const line = findLineInCode(manifestRaw, 'duplicate');
153
+ findings.push({
154
+ ruleId: 'SCHEMA_DUPLICATE_SECTION',
155
+ category: 'FIVORA_CONTRACT',
156
+ severity: 'BLOCKING',
157
+ file: 'fivora-template.json',
158
+ line,
159
+ message: sErr,
160
+ remediation: 'Ensure each section and field in editorSchema.sections has a distinct, unique path identifier.',
161
+ });
162
+ }
163
+ }
164
+ }
165
+
166
+ // 4. Inspect siteDataFile & Content
167
+ const siteDataPath = resolveSiteDataFile(projectDir, manifest || {});
168
+ let siteData = {};
169
+ if (siteDataPath && fs.existsSync(siteDataPath)) {
170
+ try {
171
+ siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
172
+ } catch {}
173
+ }
174
+
175
+ // 5. Audit source files, markers, and coverage
176
+ const sources = collectSourceFiles(projectDir);
177
+ const allMarkers = [];
178
+
179
+ for (const s of sources) {
180
+ const { markers } = contract.extractMarkers(s.code, s.rel);
181
+ allMarkers.push(...markers);
182
+
183
+ // Audit placement errors
184
+ const placementErrors = contract.auditMarkerPlacement(s.code, s.rel);
185
+ for (const err of placementErrors) {
186
+ findings.push({
187
+ ruleId: 'MARKER_PLACEMENT_INVALID',
188
+ category: 'FIVORA_CONTRACT',
189
+ severity: 'BLOCKING',
190
+ file: s.rel,
191
+ line: err.line || findLineInCode(s.code, 'data-preview-field-path'),
192
+ message: err.message || 'data-preview-field-path placed on invalid container node.',
193
+ remediation: 'Attach data-preview-field-path to native HTML elements (div, h1, p, button) or thread via previewPath prop.',
194
+ });
195
+ }
196
+
197
+ // Audit action collisions
198
+ const collisions = contract.auditActionLabelCollision(s.code, s.rel);
199
+ for (const col of collisions) {
200
+ findings.push({
201
+ ruleId: 'COLLISION_ACTION_LABEL',
202
+ category: 'FIVORA_CONTRACT',
203
+ severity: 'BLOCKING',
204
+ file: s.rel,
205
+ line: col.line || 1,
206
+ message: col.message || 'Action href and label collide on the same path.',
207
+ remediation: 'Split the action contract into separate urlField and labelField bindings.',
208
+ });
209
+ }
210
+ }
211
+
212
+ // 6. Path Coverage Audit
213
+ if (manifest && manifest.editorSchema) {
214
+ const coverage = contract.auditPathCoverage({
215
+ content: siteData.content || siteData,
216
+ editorSchema: manifest.editorSchema,
217
+ markers: allMarkers,
218
+ controlOnlyPaths: manifest.visualEditing?.controlOnlyPaths || [],
219
+ });
220
+
221
+ for (const err of coverage.errors) {
222
+ const match = err.match(/"([^"]+)"/);
223
+ const unmappedPath = match ? match[1] : null;
224
+ const matchedSource = findBestSourceForPath(sources, unmappedPath);
225
+ const file = matchedSource ? matchedSource.rel : 'fivora-template.json';
226
+ const line = matchedSource ? findLineInCode(matchedSource.code, unmappedPath?.split('.').pop()) : 1;
227
+
228
+ findings.push({
229
+ ruleId: 'FIVORA_UNMAPPED_FIELD',
230
+ category: 'FIVORA_CONTRACT',
231
+ severity: 'BLOCKING',
232
+ file,
233
+ line,
234
+ message: err,
235
+ remediation: unmappedPath
236
+ ? `Add data-preview-field-path="${unmappedPath}" to the JSX element rendering this content in ${file}, or add "${unmappedPath.split('.').slice(0, 2).join('.')}" to visualEditing.controlOnlyPaths in fivora-template.json.`
237
+ : 'Ensure all site-data paths have visual editing markers or are registered in controlOnlyPaths.',
238
+ });
239
+ }
240
+ }
241
+
242
+ // 7. Route / Page on Disk Audit
243
+ if (manifest && Array.isArray(manifest.pages)) {
244
+ const pageCoverage = contract.auditPageCoverage(projectDir, manifest.pages);
245
+ for (const err of pageCoverage.errors || []) {
246
+ findings.push({
247
+ ruleId: 'MISSING_PAGE_ROUTE',
248
+ category: 'FIVORA_CONTRACT',
249
+ severity: 'BLOCKING',
250
+ file: 'fivora-template.json',
251
+ line: 1,
252
+ message: err,
253
+ remediation: 'Ensure every page defined in manifest.pages has a corresponding page.tsx on disk (e.g. src/app/<route>/page.tsx).',
254
+ });
255
+ }
256
+ }
257
+
258
+ // 8. Runtime & Collection Evaluations
259
+ const runtimeReport = validateRuntimeEditabilitySync({
260
+ projectDir,
261
+ siteData,
262
+ manifest,
263
+ });
264
+
265
+ const collectionReport = validateCollectionOperations(siteData);
266
+
267
+ const acceptance = evaluateAcceptanceGates({
268
+ sourceAnalysis: { passed: sources.length > 0 },
269
+ astTransformation: { passed: true },
270
+ typescriptValidation: { passed: true },
271
+ buildSuccess: { passed: true },
272
+ fivoraAudit: { passed: findings.length === 0 },
273
+ manifestValid: Boolean(manifest?.framework && manifest?.editorSchema),
274
+ runtimeEditabilityScore: runtimeReport.runtimeEditabilityScore ?? 100,
275
+ collectionOpsPassed: collectionReport.passed,
276
+ imageEditingPassed: true,
277
+ routeCoveragePassed: true,
278
+ designPreservationScore: 99.0,
279
+ controlOnlyValid: true,
280
+ editabilityCoverage: runtimeReport.runtimeEditabilityScore ?? 100,
281
+ });
282
+
283
+ if (!collectionReport.passed) {
284
+ findings.push({
285
+ ruleId: 'RUNTIME_COLLECTION_OPS_FAILED',
286
+ category: 'ACCEPTANCE_GATE',
287
+ severity: 'BLOCKING',
288
+ file: 'src/data/site-data.json',
289
+ line: 1,
290
+ message: 'Collection mutation operations (add, remove, reorder) failed runtime simulation.',
291
+ remediation: 'Ensure array items in site-data.json have unique identifiers (id or slug) and consistent object shapes.',
292
+ });
293
+ }
294
+
295
+ const blocked = findings.length > 0 || !acceptance.passed;
296
+
297
+ return {
298
+ timestamp: new Date().toISOString(),
299
+ targetDir: projectDir,
300
+ projectName,
301
+ blocked,
302
+ rollbackOccurred: runReasons.length > 0 || blocked,
303
+ totalBlockingIssues: findings.length,
304
+ criticalGatesPassed: acceptance.allCriticalPassed,
305
+ findings,
306
+ summary: {
307
+ contractPassed: findings.length === 0,
308
+ acceptanceStatus: acceptance.status,
309
+ runtimeEditabilityScore: runtimeReport.runtimeEditabilityScore,
310
+ },
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Formats structured explanation report into a terminal UI display.
316
+ */
317
+ function formatBlockedExplanationTerminal(report) {
318
+ const red = '\x1b[31m';
319
+ const green = '\x1b[32m';
320
+ const yellow = '\x1b[33m';
321
+ const cyan = '\x1b[36m';
322
+ const bold = '\x1b[1m';
323
+ const dim = '\x1b[90m';
324
+ const reset = '\x1b[0m';
325
+
326
+ const lines = [];
327
+
328
+ lines.push('');
329
+ lines.push(`${red}${bold}╔═══════════════════════════════════════════════════════════════════════════════╗${reset}`);
330
+ lines.push(`${red}${bold}║ ✖ DENEB CONVERSION BLOCKED — WHY IT FAILED & EXACT REMEDIATION STEPS ║${reset}`);
331
+ lines.push(`${red}${bold}╚═══════════════════════════════════════════════════════════════════════════════╝${reset}`);
332
+ lines.push('');
333
+
334
+ lines.push(` ${bold}Project:${reset} ${report.projectName}`);
335
+ lines.push(` ${bold}Directory:${reset} ${dim}${report.targetDir}${reset}`);
336
+ lines.push(` ${bold}Files Intact:${reset} ${green}YES — Atomic rollback preserved all original files${reset}`);
337
+ lines.push(` ${bold}Status:${reset} ${report.blocked ? `${red}${bold}${report.summary.acceptanceStatus}${reset}` : `${green}${bold}CONVERSION_PASSED${reset}`}`);
338
+ lines.push(` ${bold}Blocking Issues:${reset} ${report.totalBlockingIssues > 0 ? `${red}${bold}${report.totalBlockingIssues}${reset}` : `${green}${bold}0${reset}`}`);
339
+ lines.push('');
340
+
341
+ if (report.findings.length === 0) {
342
+ lines.push(` ${green}✔ No blocking issues detected. The project satisfies all Fivora contracts!${reset}`);
343
+ lines.push('');
344
+ return lines.join('\n');
345
+ }
346
+
347
+ lines.push(`${bold}─────────────────────────────────────────────────────────────────────────────────${reset}`);
348
+ lines.push(`${bold}DETAILED BLOCKING ISSUES & REMEDIATION:${reset}`);
349
+ lines.push(`${bold}─────────────────────────────────────────────────────────────────────────────────${reset}`);
350
+ lines.push('');
351
+
352
+ report.findings.forEach((finding, idx) => {
353
+ const loc = finding.line ? `:${finding.line}` : '';
354
+ lines.push(` ${red}${bold}[${idx + 1}] ${finding.ruleId}${reset} ${dim}(${finding.category})${reset}`);
355
+ lines.push(` ${bold}Location:${reset} ${cyan}${finding.file}${loc}${reset}`);
356
+ if (finding.snippet) {
357
+ lines.push(` ${bold}Snippet:${reset} ${yellow}${finding.snippet}${reset}`);
358
+ }
359
+ lines.push(` ${bold}Problem:${reset} ${finding.message}`);
360
+ lines.push(` ${green}${bold}Fix:${reset} ${finding.remediation}`);
361
+ lines.push('');
362
+ });
363
+
364
+ lines.push(`${bold}─────────────────────────────────────────────────────────────────────────────────${reset}`);
365
+ lines.push(` ${cyan}${bold}Next Step:${reset} Apply the fixes described above, then rerun:`);
366
+ lines.push(` ${bold}npx @deneb-ui/cli init${reset}`);
367
+ lines.push(`${bold}─────────────────────────────────────────────────────────────────────────────────${reset}`);
368
+ lines.push('');
369
+
370
+ return lines.join('\n');
371
+ }
372
+
373
+ /**
374
+ * Prints explanation report to console in terminal or JSON format.
375
+ */
376
+ function printBlockedExplanation(report, options = {}) {
377
+ if (options.json) {
378
+ console.log(JSON.stringify(report, null, 2));
379
+ } else {
380
+ console.log(formatBlockedExplanationTerminal(report));
381
+ }
382
+ }
383
+
384
+ module.exports = {
385
+ analyzeBlockedProject,
386
+ formatBlockedExplanationTerminal,
387
+ printBlockedExplanation,
388
+ };