@deneb-ui/cli 2.0.21 → 2.0.23
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/README.md +62 -114
- package/bin/index.js +101 -207
- package/package.json +20 -5
- package/src/arc/__fixtures__/next-app-basic/package.json +10 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/globals.css +3 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/layout.tsx +9 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/page.tsx +11 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/Header.tsx +13 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/Hero.tsx +12 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/PromoBanner.tsx +10 -0
- package/src/arc/__fixtures__/next-app-basic/tsconfig.json +12 -0
- package/src/arc/__fixtures__/next-app-storefront/components.json +14 -0
- package/src/arc/__fixtures__/next-app-storefront/package.json +22 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/about/page.tsx +13 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/globals.css +5 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/layout.tsx +19 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/page.tsx +13 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/Features.tsx +31 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/Hero.tsx +45 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/ProductGrid.tsx +49 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/SiteFooter.tsx +22 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/SiteHeader.tsx +21 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/ui/button.tsx +36 -0
- package/src/arc/__fixtures__/next-app-storefront/tsconfig.json +15 -0
- package/src/arc/__fixtures__/next-pages-basic/package.json +10 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/_app.jsx +5 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/contact.jsx +9 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/index.jsx +11 -0
- package/src/arc/__fixtures__/next-pages-basic/styles/globals.css +9 -0
- package/src/arc/__tests__/arc.test.cjs +458 -0
- package/src/arc/adapters.cjs +184 -0
- package/src/arc/ast.cjs +323 -0
- package/src/arc/field-paths.cjs +165 -0
- package/src/arc/fivora-contract.cjs +521 -0
- package/src/arc/fs-utils.cjs +170 -0
- package/src/arc/index.cjs +628 -0
- package/src/arc/learning.cjs +185 -0
- package/src/arc/manifest.cjs +421 -0
- package/src/arc/next-config.cjs +279 -0
- package/src/arc/planner.cjs +227 -0
- package/src/arc/printer.cjs +153 -0
- package/src/arc/recipes-v2.cjs +49 -0
- package/src/arc/scanner.cjs +613 -0
- package/src/arc/semantic.cjs +651 -0
- package/src/arc/transformer.cjs +646 -0
- package/src/arc/validator.cjs +173 -0
- package/src/arc/version.cjs +22 -0
- package/src/recipes/cosmetics-beauty-store.json +1097 -0
- package/src/recipes/electronics-gadgets-store.json +1080 -0
- package/src/recipes/fashion-apparel-store.json +1074 -0
- package/src/tools/deneb-doctor.cjs +646 -0
- package/src/tools/recipe-engine.cjs +30 -0
- package/src/tools/template-converter.cjs +19 -6
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — Adaptive Refactoring Compiler
|
|
5
|
+
*
|
|
6
|
+
* AST-driven adaptive UI refactoring, editable-contract compilation,
|
|
7
|
+
* validation, and self-evaluation engine.
|
|
8
|
+
*
|
|
9
|
+
* Default engine for `npx @deneb-ui/cli init`.
|
|
10
|
+
* Legacy regex converter remains available via `--legacy`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { ARC_NAME, ARC_VERSION, SCHEMA_VERSION, ENGINE_ID } = require('./version.cjs');
|
|
16
|
+
const { walkFiles, isJsxFile, rel, copyFilePreserve, writeJson, readJsonSafe, findFirstExisting } = require('./fs-utils.cjs');
|
|
17
|
+
const { scanProject, buildDependencyGraph, inferOwnerScope } = require('./scanner.cjs');
|
|
18
|
+
const { analyzeFile, collectDesignSnapshot } = require('./semantic.cjs');
|
|
19
|
+
const { planTransformations } = require('./planner.cjs');
|
|
20
|
+
const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, ensureJsonModule } = require('./transformer.cjs');
|
|
21
|
+
const { parseSource } = require('./ast.cjs');
|
|
22
|
+
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
|
|
23
|
+
const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
|
|
24
|
+
const { recordExperience, registryArchitecture } = require('./learning.cjs');
|
|
25
|
+
const { matchRecipeV2 } = require('./recipes-v2.cjs');
|
|
26
|
+
const { ensureStaticExportConfig, findNextConfig } = require('./next-config.cjs');
|
|
27
|
+
const {
|
|
28
|
+
extractMarkers,
|
|
29
|
+
canonicalizeMarkerPath,
|
|
30
|
+
auditMarkerPlacement,
|
|
31
|
+
auditActionLabelCollision,
|
|
32
|
+
auditPathCoverage,
|
|
33
|
+
auditSchemaUniqueness,
|
|
34
|
+
auditPageCoverage,
|
|
35
|
+
auditPreviewRuntime,
|
|
36
|
+
findUncoveredVisibleText,
|
|
37
|
+
} = require('./fivora-contract.cjs');
|
|
38
|
+
const printer = require('./printer.cjs');
|
|
39
|
+
|
|
40
|
+
function parseArcOptions(raw = {}) {
|
|
41
|
+
return {
|
|
42
|
+
dryRun: Boolean(raw.dryRun || raw.dryrun),
|
|
43
|
+
explain: Boolean(raw.explain),
|
|
44
|
+
recipeName: raw.recipeName || raw.recipe || null,
|
|
45
|
+
telemetry: raw.telemetry || 'off',
|
|
46
|
+
skipInstall: Boolean(raw.skipInstall),
|
|
47
|
+
detectedPages: raw.detectedPages || null,
|
|
48
|
+
json: Boolean(raw.json),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createRunId() {
|
|
53
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
54
|
+
return `arc-${stamp}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createBackup(projectDir, runId) {
|
|
58
|
+
const backupDir = path.join(projectDir, `.deneb-backup-${runId}`);
|
|
59
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
60
|
+
return backupDir;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function backupFile(projectDir, backupDir, absPath) {
|
|
64
|
+
if (!fs.existsSync(absPath)) return;
|
|
65
|
+
const dest = path.join(backupDir, rel(projectDir, absPath));
|
|
66
|
+
copyFilePreserve(absPath, dest);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function restoreBackup(projectDir, backupDir) {
|
|
70
|
+
if (!backupDir || !fs.existsSync(backupDir)) return;
|
|
71
|
+
const files = walkFiles(backupDir, { include: () => true });
|
|
72
|
+
for (const abs of files) {
|
|
73
|
+
const relative = path.relative(backupDir, abs);
|
|
74
|
+
copyFilePreserve(abs, path.join(projectDir, relative));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findLayoutFile(profile) {
|
|
79
|
+
const root = profile.root;
|
|
80
|
+
const dirs = [profile.appDir, profile.pagesDir].filter(Boolean).map((d) => path.join(root, d));
|
|
81
|
+
const names = ['layout.tsx', 'layout.jsx', 'layout.js', '_app.tsx', '_app.jsx', '_app.js'];
|
|
82
|
+
const candidates = [];
|
|
83
|
+
for (const dir of dirs) {
|
|
84
|
+
for (const name of names) candidates.push(path.join(dir, name));
|
|
85
|
+
}
|
|
86
|
+
return findFirstExisting(candidates);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reads every source file back off disk and inventories the Deneb markers it
|
|
91
|
+
* actually contains, along with which routes render each marker.
|
|
92
|
+
*/
|
|
93
|
+
function collectMarkerInventory(projectDir, profile, graph) {
|
|
94
|
+
const fieldPaths = new Set();
|
|
95
|
+
const listPaths = new Set();
|
|
96
|
+
const itemPaths = new Set();
|
|
97
|
+
const markerRoutes = {};
|
|
98
|
+
const pageKeysByFile = {};
|
|
99
|
+
const sources = [];
|
|
100
|
+
|
|
101
|
+
for (const relativeFile of profile.jsxFiles) {
|
|
102
|
+
const abs = path.join(projectDir, relativeFile);
|
|
103
|
+
let code = '';
|
|
104
|
+
try {
|
|
105
|
+
code = fs.readFileSync(abs, 'utf8');
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
sources.push({ rel: relativeFile, code });
|
|
110
|
+
const { markers } = extractMarkers(code, relativeFile);
|
|
111
|
+
const routes = graph.routesByFile?.[relativeFile] || [];
|
|
112
|
+
|
|
113
|
+
for (const marker of markers) {
|
|
114
|
+
if (marker.kind === 'page') {
|
|
115
|
+
pageKeysByFile[relativeFile] = pageKeysByFile[relativeFile] || [];
|
|
116
|
+
if (!pageKeysByFile[relativeFile].includes(marker.value)) {
|
|
117
|
+
pageKeysByFile[relativeFile].push(marker.value);
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const canonical = canonicalizeMarkerPath(marker.value);
|
|
122
|
+
if (!canonical) continue;
|
|
123
|
+
if (marker.kind === 'field') fieldPaths.add(canonical);
|
|
124
|
+
if (marker.kind === 'list') listPaths.add(canonical);
|
|
125
|
+
if (marker.kind === 'item') itemPaths.add(canonical);
|
|
126
|
+
markerRoutes[canonical] = [...new Set([...(markerRoutes[canonical] || []), ...routes])];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
fieldPaths: [...fieldPaths],
|
|
132
|
+
listPaths: [...listPaths],
|
|
133
|
+
itemPaths: [...itemPaths],
|
|
134
|
+
markerRoutes,
|
|
135
|
+
pageKeysByFile,
|
|
136
|
+
sources,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Runs the ported Fivora strict contract against the converted project so a
|
|
142
|
+
* rejection surfaces locally rather than at upload time.
|
|
143
|
+
*/
|
|
144
|
+
function auditFivoraContract({ profile, siteData, manifest, inventory }) {
|
|
145
|
+
const placement = [];
|
|
146
|
+
const collisions = [];
|
|
147
|
+
const uncoveredText = [];
|
|
148
|
+
const allMarkers = [];
|
|
149
|
+
|
|
150
|
+
for (const source of inventory.sources) {
|
|
151
|
+
const { markers } = extractMarkers(source.code, source.rel);
|
|
152
|
+
allMarkers.push(...markers);
|
|
153
|
+
placement.push(...auditMarkerPlacement(source.code, source.rel));
|
|
154
|
+
collisions.push(...auditActionLabelCollision(source.code, source.rel));
|
|
155
|
+
uncoveredText.push(...findUncoveredVisibleText(source.code, source.rel));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const coverage = auditPathCoverage({
|
|
159
|
+
content: siteData.content,
|
|
160
|
+
editorSchema: manifest.editorSchema,
|
|
161
|
+
markers: allMarkers,
|
|
162
|
+
controlOnlyPaths: manifest.visualEditing?.controlOnlyPaths || [],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const routeFiles = {};
|
|
166
|
+
for (const route of profile.routes || []) {
|
|
167
|
+
if (route.file) routeFiles[route.id] = route.file;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const errors = [
|
|
171
|
+
...coverage.errors,
|
|
172
|
+
...placement,
|
|
173
|
+
...collisions,
|
|
174
|
+
...auditSchemaUniqueness(manifest.editorSchema),
|
|
175
|
+
...auditPageCoverage({
|
|
176
|
+
pages: manifest.pages,
|
|
177
|
+
routeFiles,
|
|
178
|
+
pageMarkersByFile: inventory.pageKeysByFile,
|
|
179
|
+
}),
|
|
180
|
+
...auditPreviewRuntime(inventory.sources.map((source) => source.code)),
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
passed: errors.length === 0,
|
|
185
|
+
errors: [...new Set(errors)],
|
|
186
|
+
uncoveredVisibleText: uncoveredText,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function analyzeProjectFiles(profile, graph) {
|
|
191
|
+
const analyses = [];
|
|
192
|
+
for (const relativeFile of profile.jsxFiles) {
|
|
193
|
+
const abs = path.join(profile.root, relativeFile);
|
|
194
|
+
let code = '';
|
|
195
|
+
try {
|
|
196
|
+
code = fs.readFileSync(abs, 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const componentMeta = (profile.components || []).find((c) => c.file === relativeFile);
|
|
201
|
+
const ownerScope = inferOwnerScope(profile, graph, relativeFile);
|
|
202
|
+
let designSnapshot = { classNames: [], styles: [] };
|
|
203
|
+
try {
|
|
204
|
+
designSnapshot = collectDesignSnapshot(code);
|
|
205
|
+
} catch {
|
|
206
|
+
// snapshot is best-effort
|
|
207
|
+
}
|
|
208
|
+
const result = analyzeFile({
|
|
209
|
+
code,
|
|
210
|
+
relativeFile,
|
|
211
|
+
profile,
|
|
212
|
+
graph,
|
|
213
|
+
ownerScope,
|
|
214
|
+
componentMeta,
|
|
215
|
+
});
|
|
216
|
+
analyses.push({
|
|
217
|
+
...result,
|
|
218
|
+
relativeFile,
|
|
219
|
+
code,
|
|
220
|
+
designSnapshot,
|
|
221
|
+
ownerScope,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return analyses;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function runDenebArc(projectDir, projectName, options = {}) {
|
|
228
|
+
const opts = parseArcOptions(options);
|
|
229
|
+
const runId = createRunId();
|
|
230
|
+
const startedAt = new Date().toISOString();
|
|
231
|
+
|
|
232
|
+
printer.printBanner(opts.dryRun ? 'dry-run' : opts.explain ? 'explain' : 'run');
|
|
233
|
+
|
|
234
|
+
const profile = scanProject(projectDir);
|
|
235
|
+
if (opts.detectedPages && Array.isArray(opts.detectedPages) && opts.detectedPages.length) {
|
|
236
|
+
const scannedIds = new Set(profile.routes.map((r) => r.id));
|
|
237
|
+
for (const page of opts.detectedPages) {
|
|
238
|
+
if (page && page.id && !scannedIds.has(page.id)) {
|
|
239
|
+
profile.routes.push(page);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
printer.printProfile(profile);
|
|
245
|
+
const graph = buildDependencyGraph(profile);
|
|
246
|
+
const analyses = analyzeProjectFiles(profile, graph);
|
|
247
|
+
|
|
248
|
+
const candidateCount = analyses.reduce(
|
|
249
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.kind !== 'decoration' && c.kind !== 'already-editable' && !c.skip).length,
|
|
250
|
+
0
|
|
251
|
+
);
|
|
252
|
+
const actionCount = analyses.reduce(
|
|
253
|
+
(n, a) => n + (a.candidates || []).filter((c) => c.operation === 'split-action-contract' || c.kind === 'url').length,
|
|
254
|
+
0
|
|
255
|
+
);
|
|
256
|
+
printer.printScan(profile, graph, candidateCount, actionCount);
|
|
257
|
+
|
|
258
|
+
const sourceAbs = profile.jsxFiles.map((f) => path.join(projectDir, f));
|
|
259
|
+
const recipeMatch = matchRecipeV2(projectDir, profile, sourceAbs, opts.recipeName);
|
|
260
|
+
if (recipeMatch.recipe) {
|
|
261
|
+
printer.ok(`Matched recipe: ${recipeMatch.recipe.label} (${recipeMatch.recipe.name})`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const plan = planTransformations({
|
|
265
|
+
profile,
|
|
266
|
+
analyses,
|
|
267
|
+
recipe: recipeMatch.recipe,
|
|
268
|
+
});
|
|
269
|
+
printer.printPlan(plan);
|
|
270
|
+
|
|
271
|
+
if (opts.explain) printer.printExplain(plan);
|
|
272
|
+
if (opts.dryRun) {
|
|
273
|
+
printer.printDryRun(profile, plan);
|
|
274
|
+
const dryReport = buildReport({
|
|
275
|
+
runId, startedAt, projectDir, projectName, profile, graph, plan, recipeMatch, opts,
|
|
276
|
+
filesChanged: [],
|
|
277
|
+
validation: { syntaxPassed: true, contractPassed: true, dryRun: true },
|
|
278
|
+
coverage: coverageMetrics({
|
|
279
|
+
analyses,
|
|
280
|
+
plan,
|
|
281
|
+
appliedCount: 0,
|
|
282
|
+
skippedDynamic: plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length,
|
|
283
|
+
alreadyEditable: analyses.filter((a) => a.alreadyEditable).length,
|
|
284
|
+
}),
|
|
285
|
+
design: { score: 100 },
|
|
286
|
+
outcome: 'dry-run',
|
|
287
|
+
});
|
|
288
|
+
return dryReport;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const backupDir = createBackup(projectDir, runId);
|
|
292
|
+
const journalDir = path.join(projectDir, '.deneb', 'runs', runId);
|
|
293
|
+
fs.mkdirSync(journalDir, { recursive: true });
|
|
294
|
+
writeJson(path.join(journalDir, 'project-profile.json'), sanitizeProfile(profile));
|
|
295
|
+
writeJson(path.join(journalDir, 'transform-plan.json'), sanitizePlan(plan));
|
|
296
|
+
|
|
297
|
+
const existing = loadExistingData(projectDir, profile);
|
|
298
|
+
const changedFiles = [];
|
|
299
|
+
const afterFiles = {};
|
|
300
|
+
let appliedCount = 0;
|
|
301
|
+
let layoutUpdated = false;
|
|
302
|
+
const transformFailures = [];
|
|
303
|
+
|
|
304
|
+
const layoutFile = findLayoutFile(profile);
|
|
305
|
+
if (layoutFile) {
|
|
306
|
+
backupFile(projectDir, backupDir, layoutFile);
|
|
307
|
+
const layoutRel = rel(projectDir, layoutFile);
|
|
308
|
+
const siteDataImport = resolveSiteDataSpecifier(profile, layoutRel);
|
|
309
|
+
const original = fs.readFileSync(layoutFile, 'utf8');
|
|
310
|
+
const instrumented = instrumentLayoutSource(original, siteDataImport);
|
|
311
|
+
if (instrumented.updated && instrumented.code !== original) {
|
|
312
|
+
fs.writeFileSync(layoutFile, instrumented.code, 'utf8');
|
|
313
|
+
changedFiles.push(layoutRel);
|
|
314
|
+
layoutUpdated = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const filePlan of plan.files) {
|
|
319
|
+
if (!filePlan.transformations.length || filePlan.skippedFile) continue;
|
|
320
|
+
const abs = path.join(projectDir, filePlan.file);
|
|
321
|
+
backupFile(projectDir, backupDir, abs);
|
|
322
|
+
let result;
|
|
323
|
+
try {
|
|
324
|
+
result = applyFilePlan(filePlan, profile);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
transformFailures.push({ file: filePlan.file, reason: err.message, confidence: 0.4 });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!result.changed) continue;
|
|
330
|
+
try {
|
|
331
|
+
parseSource(result.code, filePlan.file);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
transformFailures.push({ file: filePlan.file, reason: `AST invalid after transform: ${err.message}`, confidence: 0.3 });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
fs.writeFileSync(abs, result.code, 'utf8');
|
|
337
|
+
changedFiles.push(filePlan.file);
|
|
338
|
+
afterFiles[filePlan.file] = result.code;
|
|
339
|
+
appliedCount += result.applied || 0;
|
|
340
|
+
if (result.failures?.length) transformFailures.push(...result.failures.map((f) => ({ file: filePlan.file, ...f })));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Every scanned route must carry its own data-preview-page-key, whichever
|
|
344
|
+
// router the project uses.
|
|
345
|
+
for (const route of profile.routes || []) {
|
|
346
|
+
if (!route.file) continue;
|
|
347
|
+
const abs = path.join(projectDir, route.file);
|
|
348
|
+
if (!fs.existsSync(abs)) continue;
|
|
349
|
+
const original = fs.readFileSync(abs, 'utf8');
|
|
350
|
+
const keyed = instrumentPageKey(original, route.file, route.id);
|
|
351
|
+
if (keyed.updated && keyed.code !== original) {
|
|
352
|
+
backupFile(projectDir, backupDir, abs);
|
|
353
|
+
fs.writeFileSync(abs, keyed.code, 'utf8');
|
|
354
|
+
if (!changedFiles.includes(route.file)) changedFiles.push(route.file);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (profile.framework === 'nextjs') {
|
|
359
|
+
const before = findNextConfig(projectDir);
|
|
360
|
+
if (before) backupFile(projectDir, backupDir, before.abs);
|
|
361
|
+
const nextConfigResult = ensureStaticExportConfig(projectDir, profile);
|
|
362
|
+
if (nextConfigResult.updated && nextConfigResult.file) {
|
|
363
|
+
changedFiles.push(nextConfigResult.file);
|
|
364
|
+
printer.ok(
|
|
365
|
+
nextConfigResult.created
|
|
366
|
+
? `Created ${nextConfigResult.file} with static export for Fivora hosting`
|
|
367
|
+
: `Configured static export in ${nextConfigResult.file}`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
for (const warning of nextConfigResult.warnings || []) {
|
|
371
|
+
printer.warn(warning);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (profile.tsconfigFile) {
|
|
376
|
+
const abs = path.join(projectDir, profile.tsconfigFile);
|
|
377
|
+
const current = readJsonSafe(abs);
|
|
378
|
+
const next = ensureJsonModule(current);
|
|
379
|
+
if (next.changed) {
|
|
380
|
+
backupFile(projectDir, backupDir, abs);
|
|
381
|
+
writeJson(abs, next.config);
|
|
382
|
+
changedFiles.push(profile.tsconfigFile);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printer.printApply({ filesUpdated: changedFiles.length, layoutUpdated });
|
|
387
|
+
|
|
388
|
+
// Marker inventory must be read back from what was actually written, not from
|
|
389
|
+
// the plan: a planned field that failed to transform must not be advertised
|
|
390
|
+
// as visually editable, and an unrendered field must be declared control-only.
|
|
391
|
+
const markerInventory = collectMarkerInventory(projectDir, profile, graph);
|
|
392
|
+
|
|
393
|
+
const dataBundle = buildSiteDataAndManifest({
|
|
394
|
+
projectDir,
|
|
395
|
+
projectName: projectName || profile.packageName,
|
|
396
|
+
profile,
|
|
397
|
+
plan,
|
|
398
|
+
recipe: recipeMatch.recipe,
|
|
399
|
+
existingSiteData: existing.siteData,
|
|
400
|
+
existingManifest: existing.manifest,
|
|
401
|
+
boundFieldPaths: markerInventory.fieldPaths,
|
|
402
|
+
markerRoutes: markerInventory.markerRoutes,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const siteAbs = path.join(projectDir, dataBundle.siteDataRel);
|
|
406
|
+
const manifestAbs = path.join(projectDir, 'fivora-template.json');
|
|
407
|
+
const createdDuringRun = [];
|
|
408
|
+
if (!fs.existsSync(siteAbs)) createdDuringRun.push(siteAbs);
|
|
409
|
+
if (!fs.existsSync(manifestAbs)) createdDuringRun.push(manifestAbs);
|
|
410
|
+
if (fs.existsSync(siteAbs)) backupFile(projectDir, backupDir, siteAbs);
|
|
411
|
+
if (fs.existsSync(manifestAbs)) backupFile(projectDir, backupDir, manifestAbs);
|
|
412
|
+
writeDataBank(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
413
|
+
|
|
414
|
+
const astResults = validateAstFiles(
|
|
415
|
+
changedFiles
|
|
416
|
+
.filter((f) => isJsxFile(f) || /\.(tsx|jsx|ts|js)$/.test(f))
|
|
417
|
+
.map((f) => ({ abs: path.join(projectDir, f), rel: f }))
|
|
418
|
+
);
|
|
419
|
+
const syntaxPassed = astResults.every((r) => r.passed);
|
|
420
|
+
const contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
421
|
+
const design = designPreservationScore(plan.files, afterFiles);
|
|
422
|
+
const alreadyEditable = analyses.filter((a) => a.alreadyEditable).length;
|
|
423
|
+
const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
|
|
424
|
+
const coverage = coverageMetrics({
|
|
425
|
+
analyses,
|
|
426
|
+
plan,
|
|
427
|
+
appliedCount,
|
|
428
|
+
skippedDynamic,
|
|
429
|
+
alreadyEditable,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const fivoraAudit = auditFivoraContract({
|
|
433
|
+
profile,
|
|
434
|
+
siteData: dataBundle.siteData,
|
|
435
|
+
manifest: dataBundle.manifest,
|
|
436
|
+
inventory: collectMarkerInventory(projectDir, profile, graph),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const validation = {
|
|
440
|
+
syntaxPassed,
|
|
441
|
+
fivoraContractPassed: fivoraAudit.passed,
|
|
442
|
+
fivoraContractErrors: fivoraAudit.errors,
|
|
443
|
+
uncoveredVisibleText: fivoraAudit.uncoveredVisibleText.length,
|
|
444
|
+
contractPassed: contracts.contractPassed,
|
|
445
|
+
orphans: contracts.orphans.length,
|
|
446
|
+
missingSchema: contracts.missingSchema.length,
|
|
447
|
+
actionCollisions: contracts.actionCollisions,
|
|
448
|
+
staticAncestorCollisions: contracts.staticAncestorCollisions,
|
|
449
|
+
astFailures: astResults.filter((r) => !r.passed),
|
|
450
|
+
transformFailures,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const criticalFailure = !syntaxPassed || contracts.actionCollisions > 0 && appliedCount === 0;
|
|
454
|
+
let outcome = 'success';
|
|
455
|
+
if (criticalFailure) {
|
|
456
|
+
restoreBackup(projectDir, backupDir);
|
|
457
|
+
for (const created of createdDuringRun) {
|
|
458
|
+
try {
|
|
459
|
+
if (fs.existsSync(created)) fs.rmSync(created, { force: true });
|
|
460
|
+
} catch {
|
|
461
|
+
// ignore
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
outcome = 'rolled-back';
|
|
465
|
+
printer.printRollback(validation.astFailures[0]?.error || 'Critical validation failed');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
printer.printValidation(validation, coverage, design);
|
|
469
|
+
if (!fivoraAudit.passed) {
|
|
470
|
+
console.log(`\n \x1b[33m⚠ Fivora strict contract: ${fivoraAudit.errors.length} finding(s)\x1b[0m`);
|
|
471
|
+
for (const error of fivoraAudit.errors.slice(0, 8)) {
|
|
472
|
+
console.log(` \x1b[90m- ${error}\x1b[0m`);
|
|
473
|
+
}
|
|
474
|
+
if (fivoraAudit.errors.length > 8) {
|
|
475
|
+
console.log(` \x1b[90m... ${fivoraAudit.errors.length - 8} more in .deneb/report.json\x1b[0m`);
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
console.log(' \x1b[32m✓\x1b[0m Fivora strict contract');
|
|
479
|
+
}
|
|
480
|
+
if (fivoraAudit.uncoveredVisibleText.length) {
|
|
481
|
+
console.log(
|
|
482
|
+
` \x1b[33m⚠\x1b[0m ${fivoraAudit.uncoveredVisibleText.length} visible text node(s) still uncovered — run \x1b[1mdeneb validate .\x1b[0m before packaging`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
coverage.actionLinkContracts = contracts.fieldPaths.filter((p) => /Url$/.test(p)).length;
|
|
486
|
+
coverage.contractCollisions = contracts.actionCollisions;
|
|
487
|
+
console.log(` Action/link contracts validated: ${coverage.actionLinkContracts}`);
|
|
488
|
+
console.log(` Contract collisions: ${coverage.contractCollisions}`);
|
|
489
|
+
|
|
490
|
+
if (transformFailures.length) {
|
|
491
|
+
for (const fail of transformFailures.slice(0, 5)) {
|
|
492
|
+
printer.printError(fail.file, fail.reason, fail.confidence);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const experienceRecords = recordExperience({
|
|
497
|
+
projectDir,
|
|
498
|
+
profile,
|
|
499
|
+
plan,
|
|
500
|
+
validation: {
|
|
501
|
+
syntaxPassed,
|
|
502
|
+
contractPassed: contracts.contractPassed,
|
|
503
|
+
visualPassed: design.score >= 95,
|
|
504
|
+
idempotencyPassed: true,
|
|
505
|
+
},
|
|
506
|
+
outcome,
|
|
507
|
+
telemetry: opts.telemetry,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const result = {
|
|
511
|
+
engine: ENGINE_ID,
|
|
512
|
+
arcVersion: ARC_VERSION,
|
|
513
|
+
schemaVersion: SCHEMA_VERSION,
|
|
514
|
+
runId,
|
|
515
|
+
backupDir,
|
|
516
|
+
detection: {
|
|
517
|
+
framework: profile.framework,
|
|
518
|
+
frameworkVersion: profile.frameworkVersion,
|
|
519
|
+
detected: [
|
|
520
|
+
profile.framework,
|
|
521
|
+
...(profile.cssSystems || []),
|
|
522
|
+
...(profile.componentLibraries || []),
|
|
523
|
+
],
|
|
524
|
+
hasShadcn: profile.shadcn,
|
|
525
|
+
hasHeroUi: profile.heroui,
|
|
526
|
+
hasTailwind: (profile.cssSystems || []).some((s) => s.startsWith('tailwind')),
|
|
527
|
+
appDir: profile.appDir,
|
|
528
|
+
isAppRouter: profile.router === 'next-app',
|
|
529
|
+
pkg: profile.pkg,
|
|
530
|
+
},
|
|
531
|
+
matchedRecipe: recipeMatch.recipe,
|
|
532
|
+
transformedFilesCount: changedFiles.length,
|
|
533
|
+
totalTransformedElements: appliedCount,
|
|
534
|
+
totalFields: countSchemaFields(dataBundle.manifest),
|
|
535
|
+
coverage,
|
|
536
|
+
designPreservation: design.score,
|
|
537
|
+
validation,
|
|
538
|
+
outcome,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const report = buildReport({
|
|
542
|
+
runId,
|
|
543
|
+
startedAt,
|
|
544
|
+
projectDir,
|
|
545
|
+
projectName: projectName || profile.packageName,
|
|
546
|
+
profile,
|
|
547
|
+
graph,
|
|
548
|
+
plan,
|
|
549
|
+
recipeMatch,
|
|
550
|
+
opts,
|
|
551
|
+
filesChanged: changedFiles,
|
|
552
|
+
validation,
|
|
553
|
+
coverage,
|
|
554
|
+
design,
|
|
555
|
+
outcome,
|
|
556
|
+
experienceRecords: experienceRecords.length,
|
|
557
|
+
result,
|
|
558
|
+
});
|
|
559
|
+
writeJson(path.join(journalDir, 'result.json'), result);
|
|
560
|
+
writeJson(path.join(journalDir, 'validation.json'), validation);
|
|
561
|
+
writeJson(path.join(projectDir, '.deneb', 'report.json'), report);
|
|
562
|
+
|
|
563
|
+
if (outcome === 'success') printer.printSuccess();
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function sanitizeProfile(profile) {
|
|
568
|
+
const copy = { ...profile };
|
|
569
|
+
delete copy.pkg;
|
|
570
|
+
delete copy.dependencies;
|
|
571
|
+
delete copy.aliasMap;
|
|
572
|
+
delete copy.tsconfig;
|
|
573
|
+
return copy;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function sanitizePlan(plan) {
|
|
577
|
+
return {
|
|
578
|
+
stats: plan.stats,
|
|
579
|
+
usedPaths: plan.usedPaths,
|
|
580
|
+
skipped: plan.skipped,
|
|
581
|
+
files: (plan.files || []).map((f) => ({
|
|
582
|
+
file: f.file,
|
|
583
|
+
skippedFile: f.skippedFile,
|
|
584
|
+
skipReason: f.skipReason,
|
|
585
|
+
transformations: f.transformations,
|
|
586
|
+
})),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function buildReport(args) {
|
|
591
|
+
return {
|
|
592
|
+
engine: ARC_NAME,
|
|
593
|
+
arcVersion: ARC_VERSION,
|
|
594
|
+
schemaVersion: SCHEMA_VERSION,
|
|
595
|
+
runId: args.runId,
|
|
596
|
+
startedAt: args.startedAt,
|
|
597
|
+
finishedAt: new Date().toISOString(),
|
|
598
|
+
project: args.projectName,
|
|
599
|
+
dryRun: Boolean(args.opts?.dryRun),
|
|
600
|
+
profile: sanitizeProfile(args.profile),
|
|
601
|
+
filesScanned: args.profile.jsxFiles?.length || 0,
|
|
602
|
+
filesChanged: args.filesChanged,
|
|
603
|
+
fieldsGenerated: args.plan?.usedPaths || [],
|
|
604
|
+
routesGenerated: args.profile.routes,
|
|
605
|
+
recipesMatched: args.recipeMatch?.recipe ? [args.recipeMatch.recipe.name] : [],
|
|
606
|
+
confidenceDistribution: {
|
|
607
|
+
auto: args.plan?.stats?.auto || 0,
|
|
608
|
+
validate: args.plan?.stats?.validate || 0,
|
|
609
|
+
skipped: args.plan?.stats?.skipped || 0,
|
|
610
|
+
},
|
|
611
|
+
skippedTransformations: args.plan?.skipped || [],
|
|
612
|
+
warnings: args.validation?.transformFailures || [],
|
|
613
|
+
validation: args.validation,
|
|
614
|
+
coverage: args.coverage,
|
|
615
|
+
designPreservation: args.design,
|
|
616
|
+
learningRecords: args.experienceRecords || 0,
|
|
617
|
+
registry: registryArchitecture(),
|
|
618
|
+
outcome: args.outcome,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
module.exports = {
|
|
623
|
+
runDenebArc,
|
|
624
|
+
parseArcOptions,
|
|
625
|
+
scanProject,
|
|
626
|
+
ENGINE_ID,
|
|
627
|
+
ARC_VERSION,
|
|
628
|
+
};
|