@fiodos/cli 0.1.33 → 0.1.35
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/package.json +1 -1
- package/src/index.js +59 -4
- package/src/shopifyTheme.js +104 -0
- package/src/verify.js +12 -0
- package/src/wireHandlers.js +93 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiodos/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "Fiodos CLI — analyzes your app's source code and generates the in-app voice-agent manifest, then wires the orb. Powers `npx @fiodos/cli analyze`.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
package/src/index.js
CHANGED
|
@@ -102,7 +102,7 @@ const {
|
|
|
102
102
|
const { wireReactNativeOrb, reportReactNativeWire, connectRnSession } = require('./wireReactNative');
|
|
103
103
|
const {
|
|
104
104
|
isShopifyThemeRoot, fetchStorefrontSnapshot, wireShopifyTheme, reportShopifyWire,
|
|
105
|
-
normalizeStoreDomain,
|
|
105
|
+
normalizeStoreDomain, enforceShopifyGuaranteedExecutions,
|
|
106
106
|
} = require('./shopifyTheme');
|
|
107
107
|
const { wireFlutterOrb, writeFlutterEnv, reportFlutterWire } = require('./wireFlutter');
|
|
108
108
|
const { wireHandlers, reportHandlerResult } = require('./wireHandlers');
|
|
@@ -220,11 +220,46 @@ function wiringSpinnerLabel(platform) {
|
|
|
220
220
|
return `Wiring actions to your ${surfaceNoun(platform)}`;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Fit a colored line into ONE terminal row. The in-place refresh
|
|
225
|
+
* (`\r` + erase-line) only works if the rendered line never wraps: on a
|
|
226
|
+
* narrow terminal (IDE panes) a wrapped spinner line leaves one orphan row
|
|
227
|
+
* BEHIND per tick — hundreds of stacked "Analyzing your project…" lines
|
|
228
|
+
* instead of a single live counter. ANSI escape sequences are copied without
|
|
229
|
+
* counting toward the visible width, so colors survive the truncation.
|
|
230
|
+
*/
|
|
231
|
+
function fitLineToWidth(line, columns) {
|
|
232
|
+
const cols = Number(columns);
|
|
233
|
+
if (!Number.isFinite(cols) || cols <= 0) return line;
|
|
234
|
+
const max = cols - 1; // last cell stays free: writing it can auto-wrap
|
|
235
|
+
let out = '';
|
|
236
|
+
let visible = 0;
|
|
237
|
+
for (let i = 0; i < line.length; ) {
|
|
238
|
+
if (line[i] === '\x1b') {
|
|
239
|
+
const m = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
240
|
+
if (m) {
|
|
241
|
+
out += m[0];
|
|
242
|
+
i += m[0].length;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (visible >= max) break;
|
|
247
|
+
out += line[i];
|
|
248
|
+
visible += 1;
|
|
249
|
+
i += 1;
|
|
250
|
+
}
|
|
251
|
+
// Close any open color so the truncated tail never bleeds styles.
|
|
252
|
+
return visible >= max ? `${out}\x1b[0m` : out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Branded terminal spinner (Fiodos orb + colors when stderr is a TTY).
|
|
256
|
+
* Without a TTY (piped/CI output) there is no in-place line refresh, so the
|
|
257
|
+
* animation would stack one line per tick — print the label ONCE instead. */
|
|
224
258
|
async function withSpinner(label, work) {
|
|
225
259
|
const orbFrames = ['◴', '◷', '◶', '◵'];
|
|
226
260
|
const brailleFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
227
261
|
const { blue, cyan, dim, reset } = fyodosTermColors();
|
|
262
|
+
const isTTY = Boolean(process.stderr.isTTY);
|
|
228
263
|
let currentLabel = label;
|
|
229
264
|
let frame = 0;
|
|
230
265
|
let id = null;
|
|
@@ -236,13 +271,18 @@ async function withSpinner(label, work) {
|
|
|
236
271
|
const tail = brailleFrames[frame % brailleFrames.length];
|
|
237
272
|
const line =
|
|
238
273
|
`${cyan}${orb}${reset} ${blue}Fiodos${reset} ${dim}${tail}${reset} ${currentLabel}… ${dim}${sec}s${reset}`;
|
|
239
|
-
process.stderr.write(`\r\x1b[K${line}`);
|
|
274
|
+
process.stderr.write(`\r\x1b[K${fitLineToWidth(line, process.stderr.columns)}`);
|
|
240
275
|
frame += 1;
|
|
241
276
|
};
|
|
242
277
|
|
|
243
278
|
const clearLine = () => process.stderr.write('\r\x1b[K');
|
|
244
279
|
|
|
245
280
|
const startSpinner = () => {
|
|
281
|
+
if (!isTTY) {
|
|
282
|
+
// One static line; no timer (nothing can be refreshed without a TTY).
|
|
283
|
+
process.stderr.write(`${cyan}◉${reset} ${blue}Fiodos${reset} ${currentLabel}…\n`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
246
286
|
render();
|
|
247
287
|
id = setInterval(render, 140);
|
|
248
288
|
};
|
|
@@ -256,11 +296,14 @@ async function withSpinner(label, work) {
|
|
|
256
296
|
};
|
|
257
297
|
|
|
258
298
|
const resume = () => {
|
|
259
|
-
if (!id) startSpinner();
|
|
299
|
+
if (!id && isTTY) startSpinner();
|
|
260
300
|
};
|
|
261
301
|
|
|
262
302
|
const setLabel = (next) => {
|
|
263
303
|
currentLabel = next;
|
|
304
|
+
// Without a TTY the label is only ever printed once — announce the change
|
|
305
|
+
// on its own line so progress is still visible in piped/CI output.
|
|
306
|
+
if (!isTTY) process.stderr.write(`${cyan}◉${reset} ${blue}Fiodos${reset} ${next}…\n`);
|
|
264
307
|
};
|
|
265
308
|
|
|
266
309
|
startSpinner();
|
|
@@ -539,6 +582,18 @@ async function main() {
|
|
|
539
582
|
`actions=${(proposed.actions || []).length} in ${(elapsedMs / 1000).toFixed(1)}s`);
|
|
540
583
|
logDev(`[ai] tokens in=${usage.prompt_tokens} out=${usage.completion_tokens} cost=$${costUSD.toFixed(4)}`);
|
|
541
584
|
|
|
585
|
+
// ── 3b. Shopify platform guarantee (BEFORE verification) ────────────────
|
|
586
|
+
// The five universal commerce actions carry platform-constant execution
|
|
587
|
+
// templates; never trust the AI to have reproduced them. Enforcing them
|
|
588
|
+
// here means verification's structural check always passes for the
|
|
589
|
+
// guaranteed set and a Shopify manifest can never publish inert.
|
|
590
|
+
if (platform === 'shopify') {
|
|
591
|
+
const repairedIntents = enforceShopifyGuaranteedExecutions(proposed);
|
|
592
|
+
if (repairedIntents.length) {
|
|
593
|
+
logDev(`[shopify] canonical execution enforced on: ${repairedIntents.join(', ')}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
542
597
|
// ── 4. Mechanical verification of the AI's claims ───────────────────────
|
|
543
598
|
// Actions are always verified against real code. Routes are verified only
|
|
544
599
|
// when there is filesystem ground truth (mobile); for web they are kept as
|
package/src/shopifyTheme.js
CHANGED
|
@@ -35,6 +35,109 @@ const ORB_END = '<!-- FYODOS:ORB:END -->';
|
|
|
35
35
|
/** Repo-relative folder of the synthetic storefront snapshot files. */
|
|
36
36
|
const SNAPSHOT_DIR = '_fyodos/storefront';
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Canonical mechanics of the five guaranteed Shopify commerce actions.
|
|
40
|
+
*
|
|
41
|
+
* These execution templates are PLATFORM CONSTANTS (identical on every
|
|
42
|
+
* Shopify store), yet the analysis prompt asks the AI to reproduce them
|
|
43
|
+
* verbatim — and models sometimes omit or mangle the `execution` block. A
|
|
44
|
+
* manifest published without it is silently dead: the embed registers zero
|
|
45
|
+
* handlers and the orb promises cart actions it can never run (observed in
|
|
46
|
+
* production). `enforceShopifyGuaranteedExecutions` overwrites the MECHANICS
|
|
47
|
+
* (execution/parameters) of any action matching intent+handler while leaving
|
|
48
|
+
* the AI's personalized prose (labels, descriptions, examples) untouched.
|
|
49
|
+
* The backend applies the same guarantee at publish/serve time
|
|
50
|
+
* (backend/clients/shopify_guarantees.py) — keep both in sync.
|
|
51
|
+
*/
|
|
52
|
+
const SHOPIFY_GUARANTEED_ACTIONS = {
|
|
53
|
+
search_products: {
|
|
54
|
+
handler: 'shopifySearchProducts',
|
|
55
|
+
parameters: { query: { type: 'string', required: true } },
|
|
56
|
+
execution: {
|
|
57
|
+
kind: 'http',
|
|
58
|
+
method: 'GET',
|
|
59
|
+
url: '{baseUrl}/search/suggest.json?q={query}&resources[type]=product&resources[limit]=8',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
get_product_details: {
|
|
63
|
+
handler: 'shopifyGetProductDetails',
|
|
64
|
+
parameters: { handle: { type: 'string', required: true } },
|
|
65
|
+
execution: { kind: 'http', method: 'GET', url: '{baseUrl}/products/{handle}.js' },
|
|
66
|
+
},
|
|
67
|
+
add_to_cart: {
|
|
68
|
+
handler: 'shopifyAddToCart',
|
|
69
|
+
parameters: {
|
|
70
|
+
variantId: { type: 'number', required: true },
|
|
71
|
+
quantity: { type: 'number', required: true },
|
|
72
|
+
},
|
|
73
|
+
execution: {
|
|
74
|
+
kind: 'http',
|
|
75
|
+
method: 'POST',
|
|
76
|
+
url: '{baseUrl}/cart/add.js',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: { id: '{variantId}', quantity: '{quantity}' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
get_cart: {
|
|
82
|
+
handler: 'shopifyGetCart',
|
|
83
|
+
parameters: {},
|
|
84
|
+
execution: { kind: 'http', method: 'GET', url: '{baseUrl}/cart.js' },
|
|
85
|
+
},
|
|
86
|
+
// variantId-based on purpose (cart/change.js accepts a variant id as `id`):
|
|
87
|
+
// the theme snippet's cart screen context hands the agent variantIds, so
|
|
88
|
+
// the action contract must accept exactly what the agent can see.
|
|
89
|
+
update_cart_item: {
|
|
90
|
+
handler: 'shopifyUpdateCartItem',
|
|
91
|
+
parameters: {
|
|
92
|
+
variantId: { type: 'number', required: true },
|
|
93
|
+
quantity: { type: 'number', required: true },
|
|
94
|
+
},
|
|
95
|
+
execution: {
|
|
96
|
+
kind: 'http',
|
|
97
|
+
method: 'POST',
|
|
98
|
+
url: '{baseUrl}/cart/change.js',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: { id: '{variantId}', quantity: '{quantity}' },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Enforce the canonical execution/parameters on every guaranteed commerce
|
|
107
|
+
* action of a Shopify manifest, in place. Prose fields are never touched;
|
|
108
|
+
* per-parameter descriptions written by the AI survive when the parameter
|
|
109
|
+
* survives. Returns the intents that were repaired (for logging).
|
|
110
|
+
*/
|
|
111
|
+
function enforceShopifyGuaranteedExecutions(manifest) {
|
|
112
|
+
const repaired = [];
|
|
113
|
+
if (!manifest || !Array.isArray(manifest.actions)) return repaired;
|
|
114
|
+
for (const action of manifest.actions) {
|
|
115
|
+
if (!action || typeof action !== 'object' || action.kind === 'link') continue;
|
|
116
|
+
const spec = SHOPIFY_GUARANTEED_ACTIONS[String(action.intent || '').trim()];
|
|
117
|
+
if (!spec || String(action.handler || '').trim() !== spec.handler) continue;
|
|
118
|
+
|
|
119
|
+
const params = {};
|
|
120
|
+
const prior = (action.parameters && typeof action.parameters === 'object') ? action.parameters : {};
|
|
121
|
+
for (const [name, shape] of Object.entries(spec.parameters)) {
|
|
122
|
+
params[name] = { ...shape };
|
|
123
|
+
const priorShape = prior[name];
|
|
124
|
+
if (priorShape && typeof priorShape.description === 'string') {
|
|
125
|
+
params[name].description = priorShape.description;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const already =
|
|
130
|
+
JSON.stringify(action.execution || null) === JSON.stringify(spec.execution) &&
|
|
131
|
+
JSON.stringify(Object.keys(prior).sort()) === JSON.stringify(Object.keys(spec.parameters).sort());
|
|
132
|
+
if (already) continue;
|
|
133
|
+
|
|
134
|
+
action.execution = JSON.parse(JSON.stringify(spec.execution));
|
|
135
|
+
action.parameters = params;
|
|
136
|
+
repaired.push(action.intent);
|
|
137
|
+
}
|
|
138
|
+
return repaired;
|
|
139
|
+
}
|
|
140
|
+
|
|
38
141
|
/** A Shopify theme repo is unmistakable: layout/theme.liquid is the layout
|
|
39
142
|
* every Online Store 2.0 theme must have, plus templates/ or sections/. */
|
|
40
143
|
function isShopifyThemeRoot(appRoot) {
|
|
@@ -358,6 +461,7 @@ function reportShopifyWire(result, colors = {}, { quiet = false } = {}) {
|
|
|
358
461
|
}
|
|
359
462
|
|
|
360
463
|
module.exports = {
|
|
464
|
+
enforceShopifyGuaranteedExecutions,
|
|
361
465
|
isShopifyThemeRoot,
|
|
362
466
|
readThemeMeta,
|
|
363
467
|
fetchStorefrontSnapshot,
|
package/src/verify.js
CHANGED
|
@@ -298,6 +298,18 @@ function verifyManifest(manifest, evidence, sentFiles, staticRoutes, opts = {})
|
|
|
298
298
|
});
|
|
299
299
|
continue;
|
|
300
300
|
}
|
|
301
|
+
// Shopify function action WITHOUT an execution block: nothing can ever run
|
|
302
|
+
// it (storefronts have no wireable code; the embed builds handlers ONLY
|
|
303
|
+
// from execution specs). Publishing it would make the orb promise an
|
|
304
|
+
// action that silently dies — drop it honestly. The guaranteed commerce
|
|
305
|
+
// set never lands here: its canonical execution is enforced pre-verify.
|
|
306
|
+
if (isShopify && action.kind !== 'link') {
|
|
307
|
+
provenance.actions.push({
|
|
308
|
+
intent: action.intent, included: false,
|
|
309
|
+
reason: 'shopify function action without a declarative execution block — nothing could ever run it — DROPPED',
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
301
313
|
if (isShopify && action.kind === 'link' && String(action.navTarget || '').trim() === '/checkout') {
|
|
302
314
|
action.handler = '';
|
|
303
315
|
actions.push(action);
|
package/src/wireHandlers.js
CHANGED
|
@@ -445,6 +445,41 @@ function relImport(fromDirRel, toFileRel) {
|
|
|
445
445
|
return rel;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
+
/**
|
|
449
|
+
* PARAMETER CONTRACT check shared by both wiring strategies: every
|
|
450
|
+
* `<receiver>.<name>` / `<receiver>['<name>']` the expression reads must be a
|
|
451
|
+
* parameter the manifest declares for this action. The executor builds that
|
|
452
|
+
* object exclusively from the manifest parameters, so any other name is
|
|
453
|
+
* ALWAYS undefined at runtime — the action then "succeeds" while silently
|
|
454
|
+
* doing the wrong thing (e.g. an invoke reading `args.voiceMode` when the
|
|
455
|
+
* manifest declares 'mode': every call fell back to a default value). It
|
|
456
|
+
* compiles clean, so only this check can catch it.
|
|
457
|
+
*/
|
|
458
|
+
function checkParamContract(expr, receiver, manifestParams) {
|
|
459
|
+
const declared = new Set((manifestParams || []).map((p) => p.name));
|
|
460
|
+
const refs = new Set();
|
|
461
|
+
let m;
|
|
462
|
+
const dotRe = new RegExp(`\\b${receiver}\\.([A-Za-z_$][\\w$]*)`, 'g');
|
|
463
|
+
while ((m = dotRe.exec(expr))) refs.add(m[1]);
|
|
464
|
+
const bracketRe = new RegExp(`\\b${receiver}\\[\\s*['"]([^'"]+)['"]\\s*\\]`, 'g');
|
|
465
|
+
while ((m = bracketRe.exec(expr))) refs.add(m[1]);
|
|
466
|
+
for (const ref of refs) {
|
|
467
|
+
if (!declared.has(ref)) {
|
|
468
|
+
const hint = declared.size
|
|
469
|
+
? `the manifest declares: ${[...declared].map((n) => `'${n}'`).join(', ')}`
|
|
470
|
+
: 'the manifest declares NO parameters for this action';
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
reason:
|
|
474
|
+
`the wiring reads '${receiver}.${ref}', but ${hint} — '${receiver}' only ever carries the ` +
|
|
475
|
+
'declared manifest parameters, so that value would always be undefined at runtime. ' +
|
|
476
|
+
'Use the declared parameter name(s) exactly.',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { ok: true };
|
|
481
|
+
}
|
|
482
|
+
|
|
448
483
|
/**
|
|
449
484
|
* Turn the AI's proposed wiring for ONE action into a verified `auto` entry, or
|
|
450
485
|
* return { reason } when the proposal cannot be safely trusted. This is the
|
|
@@ -518,6 +553,14 @@ function buildAiEntry({ appRoot, fyodosDirRel, base, aiW, manifestParams }) {
|
|
|
518
553
|
};
|
|
519
554
|
}
|
|
520
555
|
|
|
556
|
+
// 5) PARAMETER CONTRACT (same guarantee as the bridge strategy): every
|
|
557
|
+
// `params.<name>` / `params['<name>']` the call reads must be a parameter
|
|
558
|
+
// the manifest declares for this action — anything else is ALWAYS
|
|
559
|
+
// undefined at runtime and produces a silent wrong-value execution that
|
|
560
|
+
// compiles clean and "succeeds".
|
|
561
|
+
const paramContract = checkParamContract(call, 'params', manifestParams);
|
|
562
|
+
if (!paramContract.ok) return { reason: paramContract.reason };
|
|
563
|
+
|
|
521
564
|
return {
|
|
522
565
|
...base,
|
|
523
566
|
confidence: 'auto',
|
|
@@ -703,6 +746,11 @@ function buildAiBridgeEntry({ appRoot, fyodosDirRel, base, aiW }) {
|
|
|
703
746
|
};
|
|
704
747
|
}
|
|
705
748
|
|
|
749
|
+
// PARAMETER CONTRACT: every `args.<name>` the invoke reads must be a
|
|
750
|
+
// parameter the manifest declares for THIS action (see checkParamContract).
|
|
751
|
+
const paramContract = checkParamContract(invoke, 'args', base.manifestParams);
|
|
752
|
+
if (!paramContract.ok) return { reason: paramContract.reason };
|
|
753
|
+
|
|
706
754
|
// Verify extra imports the invoke needs.
|
|
707
755
|
const imports = [];
|
|
708
756
|
const importedNames = new Set();
|
|
@@ -1345,6 +1393,44 @@ function stripFiodosBlocks(content) {
|
|
|
1345
1393
|
);
|
|
1346
1394
|
}
|
|
1347
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Sweep GENERATED bridge blocks left behind in files this run does NOT wire.
|
|
1398
|
+
*
|
|
1399
|
+
* applyComponentEdits strips old blocks only in the files it edits THIS run,
|
|
1400
|
+
* so when a re-analysis moves an action's wiring to a different component the
|
|
1401
|
+
* previous component keeps a stale registerFiodosBridge() block: a duplicate
|
|
1402
|
+
* registration for the same key whose argument contract may no longer match —
|
|
1403
|
+
* whichever component mounts last silently wins (the select_voice_mode
|
|
1404
|
+
* production bug: two components registered the key, one reading args.mode
|
|
1405
|
+
* and the other args.voiceMode, and the action "ran" without doing anything).
|
|
1406
|
+
*
|
|
1407
|
+
* Only blocks carrying the generated header are removed; developer-authored
|
|
1408
|
+
* FYODOS:BRIDGE blocks are preserved verbatim (same rule as
|
|
1409
|
+
* stripFiodosBlocks). Returns backups compatible with revertAll so a combined
|
|
1410
|
+
* build regression restores the swept files too.
|
|
1411
|
+
*/
|
|
1412
|
+
function sweepStaleGeneratedBlocks(appRoot, files, editedFiles) {
|
|
1413
|
+
const edited = new Set((editedFiles || []).map((f) => normRel(f)));
|
|
1414
|
+
const backups = [];
|
|
1415
|
+
for (const f of files || []) {
|
|
1416
|
+
const rel = normRel(f && f.rel);
|
|
1417
|
+
if (!rel || edited.has(rel)) continue;
|
|
1418
|
+
const abs = path.join(appRoot, rel);
|
|
1419
|
+
let original;
|
|
1420
|
+
try {
|
|
1421
|
+
original = fs.readFileSync(abs, 'utf8');
|
|
1422
|
+
} catch {
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (!original.includes(EDIT_START_PREFIX)) continue;
|
|
1426
|
+
const swept = stripFiodosBlocks(original);
|
|
1427
|
+
if (swept === original) continue;
|
|
1428
|
+
backups.push({ file: rel, abs, original });
|
|
1429
|
+
fs.writeFileSync(abs, swept);
|
|
1430
|
+
}
|
|
1431
|
+
return backups;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1348
1434
|
/** Insert an import block right after the file's last top-level import statement.
|
|
1349
1435
|
* Matches the WHOLE statement through its module string — a per-line regex put
|
|
1350
1436
|
* the block INSIDE multi-line imports (`import {\n a,\n} from 'x';`), corrupting
|
|
@@ -1926,6 +2012,7 @@ async function wireHandlers(appRoot, opts = {}) {
|
|
|
1926
2012
|
}
|
|
1927
2013
|
fs.writeFileSync(registryAbs, buildRegistryModule(finalPlan, { ts, esm }));
|
|
1928
2014
|
({ backups } = applyComponentEdits(appRoot, finalEdits));
|
|
2015
|
+
backups.push(...sweepStaleGeneratedBlocks(appRoot, files, finalEdits.map((e) => e.file)));
|
|
1929
2016
|
} catch (err) {
|
|
1930
2017
|
revertAll(backups, generatedFiles);
|
|
1931
2018
|
return { status: 'failed', docPath: docAbs, error: err, plan: finalPlan, verification: loop.attemptsLog };
|
|
@@ -1972,6 +2059,9 @@ async function wireHandlers(appRoot, opts = {}) {
|
|
|
1972
2059
|
}
|
|
1973
2060
|
fs.writeFileSync(registryAbs, buildRegistryModule(survivorPlan, { ts, esm }));
|
|
1974
2061
|
({ backups: survivorBackups } = applyComponentEdits(appRoot, survivorEdits));
|
|
2062
|
+
survivorBackups.push(
|
|
2063
|
+
...sweepStaleGeneratedBlocks(appRoot, files, survivorEdits.map((e) => e.file)),
|
|
2064
|
+
);
|
|
1975
2065
|
} catch (err) {
|
|
1976
2066
|
revertAll(survivorBackups, survivorGenerated);
|
|
1977
2067
|
return { status: 'failed', docPath: docAbs, error: err, plan: finalPlan, verification: loop.attemptsLog };
|
|
@@ -2041,6 +2131,7 @@ async function wireHandlers(appRoot, opts = {}) {
|
|
|
2041
2131
|
throw new Error('post-write verification failed');
|
|
2042
2132
|
}
|
|
2043
2133
|
({ backups } = applyComponentEdits(appRoot, componentEdits));
|
|
2134
|
+
backups.push(...sweepStaleGeneratedBlocks(appRoot, files, componentEdits.map((e) => e.file)));
|
|
2044
2135
|
} catch (err) {
|
|
2045
2136
|
revertAll(backups, generatedFiles);
|
|
2046
2137
|
return { status: 'failed', docPath: docAbs, error: err, plan };
|
|
@@ -2138,6 +2229,8 @@ module.exports = {
|
|
|
2138
2229
|
planComponentEdits,
|
|
2139
2230
|
applyComponentEdits,
|
|
2140
2231
|
stripFiodosBlocks,
|
|
2232
|
+
sweepStaleGeneratedBlocks,
|
|
2233
|
+
checkParamContract,
|
|
2141
2234
|
revertAll,
|
|
2142
2235
|
runAutoCorrectionLoop,
|
|
2143
2236
|
writeIsolated,
|