@fiodos/cli 0.1.32 → 0.1.34
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 +13 -1
- package/src/shopifyTheme.js +108 -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.34",
|
|
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');
|
|
@@ -539,6 +539,18 @@ async function main() {
|
|
|
539
539
|
`actions=${(proposed.actions || []).length} in ${(elapsedMs / 1000).toFixed(1)}s`);
|
|
540
540
|
logDev(`[ai] tokens in=${usage.prompt_tokens} out=${usage.completion_tokens} cost=$${costUSD.toFixed(4)}`);
|
|
541
541
|
|
|
542
|
+
// ── 3b. Shopify platform guarantee (BEFORE verification) ────────────────
|
|
543
|
+
// The five universal commerce actions carry platform-constant execution
|
|
544
|
+
// templates; never trust the AI to have reproduced them. Enforcing them
|
|
545
|
+
// here means verification's structural check always passes for the
|
|
546
|
+
// guaranteed set and a Shopify manifest can never publish inert.
|
|
547
|
+
if (platform === 'shopify') {
|
|
548
|
+
const repairedIntents = enforceShopifyGuaranteedExecutions(proposed);
|
|
549
|
+
if (repairedIntents.length) {
|
|
550
|
+
logDev(`[shopify] canonical execution enforced on: ${repairedIntents.join(', ')}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
542
554
|
// ── 4. Mechanical verification of the AI's claims ───────────────────────
|
|
543
555
|
// Actions are always verified against real code. Routes are verified only
|
|
544
556
|
// 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) {
|
|
@@ -108,6 +211,10 @@ function universalSurfaceDoc(storeDomain) {
|
|
|
108
211
|
universalRoutes: ['/', '/collections/all', '/search', '/cart'],
|
|
109
212
|
universalActions: [
|
|
110
213
|
{ intent: 'search_products', handler: 'shopifySearchProducts', api: '{baseUrl}/search/suggest.json' },
|
|
214
|
+
// The search→cart bridge: suggest.json returns product ids/handles but
|
|
215
|
+
// NEVER variant ids, and cart/add.js only accepts variant ids. This
|
|
216
|
+
// endpoint (universal on every store) turns a handle into its variants.
|
|
217
|
+
{ intent: 'get_product_details', handler: 'shopifyGetProductDetails', api: '{baseUrl}/products/{handle}.js' },
|
|
111
218
|
{ intent: 'add_to_cart', handler: 'shopifyAddToCart', api: '{baseUrl}/cart/add.js' },
|
|
112
219
|
{ intent: 'get_cart', handler: 'shopifyGetCart', api: '{baseUrl}/cart.js' },
|
|
113
220
|
{ intent: 'update_cart_item', handler: 'shopifyUpdateCartItem', api: '{baseUrl}/cart/change.js' },
|
|
@@ -354,6 +461,7 @@ function reportShopifyWire(result, colors = {}, { quiet = false } = {}) {
|
|
|
354
461
|
}
|
|
355
462
|
|
|
356
463
|
module.exports = {
|
|
464
|
+
enforceShopifyGuaranteedExecutions,
|
|
357
465
|
isShopifyThemeRoot,
|
|
358
466
|
readThemeMeta,
|
|
359
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,
|