@avocadostudio-ai/orchestrator-core 0.3.1 → 0.3.2
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/dist/chat/chat-pipeline.js +1 -0
- package/dist/chat/hallucination-validator.d.ts +6 -0
- package/dist/chat/hallucination-validator.js +49 -8
- package/dist/cms/adapter.d.ts +1 -0
- package/dist/handler/create-orchestrator.js +18 -5
- package/dist/nlp/deterministic-planner-suggestions.js +1 -1
- package/dist/nlp/plan-normalizer.js +139 -50
- package/package.json +3 -3
|
@@ -1171,6 +1171,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
1171
1171
|
blockId: entry.blockId,
|
|
1172
1172
|
blockType: entry.blockType,
|
|
1173
1173
|
propName: entry.propName,
|
|
1174
|
+
allowedProps: entry.allowedProps,
|
|
1174
1175
|
plannerSource: source,
|
|
1175
1176
|
modelKey,
|
|
1176
1177
|
modelUsed
|
|
@@ -15,6 +15,12 @@ export type HallucinatedProp = {
|
|
|
15
15
|
blockId: string;
|
|
16
16
|
blockType: string;
|
|
17
17
|
propName: string;
|
|
18
|
+
/**
|
|
19
|
+
* What the block *does* accept. Present so the log line that records a strip
|
|
20
|
+
* carries the answer next to the question — the integrator who hit this spent
|
|
21
|
+
* nine minutes proving the block was fine, and this is the line they read.
|
|
22
|
+
*/
|
|
23
|
+
allowedProps: string[];
|
|
18
24
|
};
|
|
19
25
|
export type HallucinationValidationResult = {
|
|
20
26
|
plan: EditPlan;
|
|
@@ -45,6 +45,22 @@ function findBlockType(args) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
/*
|
|
49
|
+
* Does this key read as a visual/presentational one?
|
|
50
|
+
*
|
|
51
|
+
* Deliberately a small allow-list of stems rather than a clever rule: the
|
|
52
|
+
* default has to be "content", because the expensive mistake is telling
|
|
53
|
+
* somebody their content edit was a styling limitation, not the reverse.
|
|
54
|
+
*/
|
|
55
|
+
const VISUAL_PROP_STEMS = [
|
|
56
|
+
"color", "colour", "background", "gradient", "animation", "animate", "shadow",
|
|
57
|
+
"font", "size", "spacing", "padding", "margin", "border", "radius", "opacity",
|
|
58
|
+
"align", "theme", "style", "variant", "width", "height", "position", "layout"
|
|
59
|
+
];
|
|
60
|
+
function isVisualPropName(prop) {
|
|
61
|
+
const lower = prop.toLowerCase();
|
|
62
|
+
return VISUAL_PROP_STEMS.some((stem) => lower.includes(stem));
|
|
63
|
+
}
|
|
48
64
|
function humanBlockName(blockType) {
|
|
49
65
|
const meta = getBlockMeta(blockType);
|
|
50
66
|
return meta?.displayName ?? blockType;
|
|
@@ -81,15 +97,33 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
81
97
|
if (allowedKeys.has(key))
|
|
82
98
|
continue;
|
|
83
99
|
delete patchCandidate[key];
|
|
84
|
-
hallucinatedProps.push({
|
|
100
|
+
hallucinatedProps.push({
|
|
101
|
+
blockId: op.blockId,
|
|
102
|
+
blockType,
|
|
103
|
+
propName: key,
|
|
104
|
+
allowedProps: [...allowedKeys].sort()
|
|
105
|
+
});
|
|
85
106
|
}
|
|
86
107
|
}
|
|
87
108
|
if (hallucinatedProps.length > 0) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
/*
|
|
110
|
+
* Two different events used to share one sentence, and the wrong one was
|
|
111
|
+
* the default.
|
|
112
|
+
*
|
|
113
|
+
* "Some requested styling isn't available" is true when the planner
|
|
114
|
+
* promised a colour, a gradient or an animation the block has no field
|
|
115
|
+
* for. It is false — and actively misleading — when the planner simply
|
|
116
|
+
* used the wrong *name* for a field the block does have under another
|
|
117
|
+
* name: nothing about that is styling, and the sentence sends the reader
|
|
118
|
+
* to look at their design system. An integrator lost nine minutes to
|
|
119
|
+
* exactly that, on a block whose schema, registry and ops path were all
|
|
120
|
+
* correct.
|
|
121
|
+
*
|
|
122
|
+
* So classify the stripped key. A visual key keeps the original wording,
|
|
123
|
+
* because that is the case it was written for and the evals that check it
|
|
124
|
+
* are checking that case. A content key gets a sentence that names it,
|
|
125
|
+
* which is the one piece of information that ends the search.
|
|
126
|
+
*/
|
|
93
127
|
const byBlockType = new Map();
|
|
94
128
|
for (const entry of hallucinatedProps) {
|
|
95
129
|
const bucket = byBlockType.get(entry.blockType) ?? new Set();
|
|
@@ -97,9 +131,16 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
97
131
|
byBlockType.set(entry.blockType, bucket);
|
|
98
132
|
}
|
|
99
133
|
const noteParts = [];
|
|
100
|
-
for (const [blockType] of byBlockType) {
|
|
134
|
+
for (const [blockType, props] of byBlockType) {
|
|
101
135
|
const name = humanBlockName(blockType);
|
|
102
|
-
|
|
136
|
+
const visual = [...props].filter(isVisualPropName);
|
|
137
|
+
const content = [...props].filter((prop) => !isVisualPropName(prop));
|
|
138
|
+
if (visual.length > 0) {
|
|
139
|
+
noteParts.push(`Some requested styling isn't available on the ${name} block — applied the supported parts.`);
|
|
140
|
+
}
|
|
141
|
+
for (const prop of content) {
|
|
142
|
+
noteParts.push(`The ${name} block has no “${prop}” field, so that part wasn't applied.`);
|
|
143
|
+
}
|
|
103
144
|
}
|
|
104
145
|
const note = noteParts.join(" ");
|
|
105
146
|
const summary = plan.summary_for_user?.trimEnd() ?? "";
|
package/dist/cms/adapter.d.ts
CHANGED
|
@@ -771,20 +771,31 @@ export function createOrchestrator(config = {}) {
|
|
|
771
771
|
*
|
|
772
772
|
* This is the copy the bootstrap already took, not a fresh read — a
|
|
773
773
|
* second `getPages()` here is 45 sequential Sanity calls on the
|
|
774
|
-
* integration that motivated it. It
|
|
775
|
-
* that reloaded the draft from SQLite,
|
|
776
|
-
*
|
|
774
|
+
* integration that motivated it. It used to be absent for the rest of a
|
|
775
|
+
* process's life after a restart that reloaded the draft from SQLite,
|
|
776
|
+
* which quietly disabled publishing for any adapter that refuses without
|
|
777
|
+
* a baseline; `ensure` now recovers it on the first request instead.
|
|
778
|
+
*
|
|
779
|
+
* It can still be undefined — the adapter read can fail, and baselines
|
|
780
|
+
* are evicted FIFO — so the contract is unchanged: treat undefined as
|
|
781
|
+
* "no baseline available" and never as "the site was empty".
|
|
777
782
|
*/
|
|
778
783
|
const published = runtime.bootstrapCache.baselineFor(scopedSession) ?? undefined;
|
|
779
784
|
const context = body.assets || published
|
|
780
785
|
? { ...(body.assets ? { assets: body.assets } : {}), ...(published ? { published } : {}) }
|
|
781
786
|
: undefined;
|
|
782
787
|
let unsupported = [];
|
|
788
|
+
// Default true: an adapter that does not mention `written` means what
|
|
789
|
+
// every adapter written before the field existed meant.
|
|
790
|
+
let written = true;
|
|
783
791
|
try {
|
|
784
792
|
const result = await runtime.adapter.onPublish(pages, config, context);
|
|
785
793
|
if (result && typeof result === "object" && Array.isArray(result.unsupported)) {
|
|
786
794
|
unsupported = result.unsupported;
|
|
787
795
|
}
|
|
796
|
+
if (result && typeof result === "object" && result.ok === true && result.written === false) {
|
|
797
|
+
written = false;
|
|
798
|
+
}
|
|
788
799
|
if (result && typeof result === "object" && result.ok === false) {
|
|
789
800
|
const message = result.error ?? "adapter.onPublish returned not-ok";
|
|
790
801
|
runtime.log.warn({ session: scopedSession, adapter: runtime.adapter.id, error: result.error }, "library-publish: adapter.onPublish() returned not-ok");
|
|
@@ -810,13 +821,15 @@ export function createOrchestrator(config = {}) {
|
|
|
810
821
|
* image beside it — that is neither a plain success, which claims the
|
|
811
822
|
* whole edit shipped, nor an error, which claims none of it did.
|
|
812
823
|
*/
|
|
813
|
-
const summary =
|
|
824
|
+
const summary = written
|
|
825
|
+
? buildPublishSummary({ changedSlugs: [], removedSlugs: [], totalPages: pages.length, hasDiff: false })
|
|
826
|
+
: `Computed ${pages.length} ${pages.length === 1 ? "page" : "pages"} — nothing written`;
|
|
814
827
|
record(true, unsupported.length > 0
|
|
815
828
|
? `${summary} ${unsupported.length} change${unsupported.length === 1 ? "" : "s"} could not be published.`
|
|
816
829
|
: summary);
|
|
817
830
|
return jsonResponse({
|
|
818
831
|
ok: true,
|
|
819
|
-
written
|
|
832
|
+
written,
|
|
820
833
|
count: pages.length,
|
|
821
834
|
...(unsupported.length > 0 ? { unsupported } : {})
|
|
822
835
|
}, { status: 200, cors });
|
|
@@ -31,7 +31,7 @@ export function promptFromPropKey(propKey, blockType) {
|
|
|
31
31
|
return CLARIFICATION_LABELS[propKey];
|
|
32
32
|
const human = blockType ? getPropDisplayName(blockType, propKey) : propKey;
|
|
33
33
|
// Fall back to the registry-declared label so host-app overrides (e.g.
|
|
34
|
-
//
|
|
34
|
+
// a host site's TwoColumn adding `sectionId`) surface as "Edit Section id" rather
|
|
35
35
|
// than leaking the raw camelCase key into the UI.
|
|
36
36
|
return human === propKey ? `Edit ${propKey}` : `Edit ${human.toLowerCase()}`;
|
|
37
37
|
}
|
|
@@ -1,6 +1,135 @@
|
|
|
1
|
-
import { allowedBlockTypes, declaredDefaultPropsForType, defaultPropsForType as sharedDefaultPropsForType } from "@avocadostudio-ai/shared";
|
|
1
|
+
import { allowedBlockTypes, blockSchemas, declaredDefaultPropsForType, defaultPropsForType as sharedDefaultPropsForType, getBlockMeta } from "@avocadostudio-ai/shared";
|
|
2
2
|
import { extractRouteMentions, firstRouteMention, normalizeRouteCandidate, parseCreatePageRequest } from "./intent-helpers.js";
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
+
// Prop-name aliasing, asked of the registry rather than of a literal
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/*
|
|
7
|
+
* Models confuse Avocado's own prop names — `heading` for a section that calls
|
|
8
|
+
* it `title`, `question`/`answer` for FAQ items that call them `q`/`a`. The
|
|
9
|
+
* aliases below exist to repair that, and they are worth keeping.
|
|
10
|
+
*
|
|
11
|
+
* They were applied by comparing the block type against the literal string
|
|
12
|
+
* "Hero", which is a name only Avocado's own catalogue has. Every other block
|
|
13
|
+
* in the world is `!== "Hero"`, so a site that brings its own blocks and names
|
|
14
|
+
* a text prop `heading` had that prop deleted here and replaced with `title` —
|
|
15
|
+
* a prop its schema does not have. The ops engine then stripped `title` as a
|
|
16
|
+
* hallucinated prop, the edit vanished, and the user was told that "some
|
|
17
|
+
* requested styling isn't available" on their block. Nothing in the chain was
|
|
18
|
+
* wrong about its own job; the first link was answering a question about a name
|
|
19
|
+
* instead of about a schema.
|
|
20
|
+
*
|
|
21
|
+
* So ask the registry. A rename now requires positive evidence in both
|
|
22
|
+
* directions: the block cannot take the key the planner used, and can take the
|
|
23
|
+
* one we would rewrite it to. Every other case — unknown block type, a block
|
|
24
|
+
* that accepts both, a block that accepts neither — leaves the value alone,
|
|
25
|
+
* which is the answer that loses no data.
|
|
26
|
+
*/
|
|
27
|
+
function blockAcceptsProp(blockType, prop) {
|
|
28
|
+
if (!blockType)
|
|
29
|
+
return false;
|
|
30
|
+
const meta = getBlockMeta(blockType);
|
|
31
|
+
if (meta?.fields && prop in meta.fields)
|
|
32
|
+
return true;
|
|
33
|
+
// Manifest-registered blocks may carry a schema richer than their derived
|
|
34
|
+
// meta, so the schema gets the second look rather than the first refusal.
|
|
35
|
+
const shape = blockSchemas[blockType]?.shape;
|
|
36
|
+
return Boolean(shape && prop in shape);
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* Avocado's own `autoplay` / `loop` / `striped` are string enums ("true" /
|
|
40
|
+
* "false"), not booleans, so a model that emits a real boolean has to be
|
|
41
|
+
* coerced. That coercion ran on every block type, and a custom block whose
|
|
42
|
+
* `loop` is a genuine `z.boolean()` had the string `"true"` written into it —
|
|
43
|
+
* silently, and only on the chat path, so it looked like a CMS problem.
|
|
44
|
+
*
|
|
45
|
+
* Coerce only where the block actually declares the prop as a string enum.
|
|
46
|
+
*/
|
|
47
|
+
const BOOLEAN_ENUM_PROPS = ["autoplay", "loop", "striped"];
|
|
48
|
+
/**
|
|
49
|
+
* Ask the block's own schema, which is the only thing that actually knows:
|
|
50
|
+
* rewrite a boolean to `"true"`/`"false"` exactly when the schema rejects the
|
|
51
|
+
* boolean and accepts the string. A block that takes a real boolean keeps it,
|
|
52
|
+
* a block that takes either keeps what the planner sent, and an unregistered
|
|
53
|
+
* type is left alone.
|
|
54
|
+
*/
|
|
55
|
+
function coerceBooleanEnumProps(blockType, props) {
|
|
56
|
+
const shape = blockSchemas[blockType]?.shape;
|
|
57
|
+
if (!shape)
|
|
58
|
+
return;
|
|
59
|
+
for (const key of BOOLEAN_ENUM_PROPS) {
|
|
60
|
+
const value = props[key];
|
|
61
|
+
if (typeof value !== "boolean")
|
|
62
|
+
continue;
|
|
63
|
+
const field = shape[key];
|
|
64
|
+
if (typeof field?.safeParse !== "function")
|
|
65
|
+
continue;
|
|
66
|
+
const asString = value ? "true" : "false";
|
|
67
|
+
/*
|
|
68
|
+
* Round-trip, not merely "parses". Avocado's own `autoplay` is
|
|
69
|
+
* `z.enum(["true","false"]).default("false").catch("false")`, and `.catch`
|
|
70
|
+
* means a boolean parses *successfully* — into the wrong value. Accepting
|
|
71
|
+
* that as "the block wants a boolean" would leave the silent wrong answer
|
|
72
|
+
* in place. A schema genuinely wants a boolean only when it gives the
|
|
73
|
+
* boolean back unchanged.
|
|
74
|
+
*/
|
|
75
|
+
const asBool = field.safeParse(value);
|
|
76
|
+
if (asBool.success && asBool.data === value)
|
|
77
|
+
continue;
|
|
78
|
+
const asStr = field.safeParse(asString);
|
|
79
|
+
if (!asStr.success || asStr.data !== asString)
|
|
80
|
+
continue;
|
|
81
|
+
props[key] = asString;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Rename `from`→`to` only when the block demonstrably wants `to` and not `from`. */
|
|
85
|
+
function shouldAliasProp(blockType, from, to) {
|
|
86
|
+
return !blockAcceptsProp(blockType, from) && blockAcceptsProp(blockType, to);
|
|
87
|
+
}
|
|
88
|
+
/*
|
|
89
|
+
* The same question for a key inside a list item. `listFields[key].itemFields`
|
|
90
|
+
* is the declared shape; a block with no declared list metadata answers "no"
|
|
91
|
+
* to both halves and is therefore left alone.
|
|
92
|
+
*/
|
|
93
|
+
function listItemAcceptsKey(blockType, listKey, itemKey) {
|
|
94
|
+
if (!blockType)
|
|
95
|
+
return false;
|
|
96
|
+
const itemFields = getBlockMeta(blockType)?.listFields?.[listKey]?.itemFields;
|
|
97
|
+
return Boolean(itemFields && itemKey in itemFields);
|
|
98
|
+
}
|
|
99
|
+
function shouldAliasItemKey(blockType, listKey, from, to) {
|
|
100
|
+
return (!listItemAcceptsKey(blockType, listKey, from) && listItemAcceptsKey(blockType, listKey, to));
|
|
101
|
+
}
|
|
102
|
+
const ITEM_KEY_ALIASES = {
|
|
103
|
+
question: "q",
|
|
104
|
+
answer: "a",
|
|
105
|
+
testimonial: "quote",
|
|
106
|
+
review: "quote"
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Apply the list-item aliases to one array prop, block-type aware.
|
|
110
|
+
* Shared by the `update_props` and `add_block` paths so they cannot drift.
|
|
111
|
+
*/
|
|
112
|
+
function aliasListItems(blockType, listKey, value) {
|
|
113
|
+
return value.map((item) => {
|
|
114
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
115
|
+
return item;
|
|
116
|
+
const entry = item;
|
|
117
|
+
let changed = false;
|
|
118
|
+
const mapped = {};
|
|
119
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
120
|
+
const alias = ITEM_KEY_ALIASES[k.toLowerCase()];
|
|
121
|
+
if (alias && !(alias in entry) && shouldAliasItemKey(blockType, listKey, k.toLowerCase(), alias)) {
|
|
122
|
+
mapped[alias] = v;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
mapped[k] = v;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return changed ? mapped : entry;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
4
133
|
// Op reordering: create_page must precede ops targeting the same slug
|
|
5
134
|
// ---------------------------------------------------------------------------
|
|
6
135
|
function reorderCreatePageFirst(ops) {
|
|
@@ -886,38 +1015,17 @@ export function normalizePlanCandidate(input, args) {
|
|
|
886
1015
|
const patch = raw.patch;
|
|
887
1016
|
const targetBlock = args?.currentPage?.blocks.find((b) => b.id === raw.blockId);
|
|
888
1017
|
const blockType = targetBlock?.type ?? "";
|
|
889
|
-
if (
|
|
1018
|
+
if ("heading" in patch && !("title" in patch) && shouldAliasProp(blockType, "heading", "title")) {
|
|
890
1019
|
patch.title = patch.heading;
|
|
891
1020
|
delete patch.heading;
|
|
892
1021
|
}
|
|
893
|
-
const itemKeyAliases = { question: "q", answer: "a", testimonial: "quote", review: "quote" };
|
|
894
1022
|
for (const [propKey, propVal] of Object.entries(patch)) {
|
|
895
1023
|
if (!Array.isArray(propVal))
|
|
896
1024
|
continue;
|
|
897
|
-
patch[propKey] =
|
|
898
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
899
|
-
return item;
|
|
900
|
-
const entry = item;
|
|
901
|
-
let changed = false;
|
|
902
|
-
const mapped = {};
|
|
903
|
-
for (const [k, v] of Object.entries(entry)) {
|
|
904
|
-
const alias = itemKeyAliases[k.toLowerCase()];
|
|
905
|
-
if (alias && !(alias in entry)) {
|
|
906
|
-
mapped[alias] = v;
|
|
907
|
-
changed = true;
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
mapped[k] = v;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return changed ? mapped : entry;
|
|
914
|
-
});
|
|
1025
|
+
patch[propKey] = aliasListItems(blockType, propKey, propVal);
|
|
915
1026
|
}
|
|
916
1027
|
// Block-specific prop key remapping and type coercion (mirrors add_block path)
|
|
917
|
-
|
|
918
|
-
if (typeof patch[k] === "boolean")
|
|
919
|
-
patch[k] = patch[k] ? "true" : "false";
|
|
920
|
-
}
|
|
1028
|
+
coerceBooleanEnumProps(blockType, patch);
|
|
921
1029
|
if (blockType === "Carousel" && Array.isArray(patch.slides) && !patch.items) {
|
|
922
1030
|
patch.items = patch.slides;
|
|
923
1031
|
delete patch.slides;
|
|
@@ -1153,36 +1261,20 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1153
1261
|
if (block.props && typeof block.props === "object" && !Array.isArray(block.props)) {
|
|
1154
1262
|
const bProps = block.props;
|
|
1155
1263
|
const blockType = typeof block.type === "string" ? block.type : "";
|
|
1156
|
-
if (
|
|
1264
|
+
if ("heading" in bProps && !("title" in bProps) && shouldAliasProp(blockType, "heading", "title")) {
|
|
1157
1265
|
bProps.title = bProps.heading;
|
|
1158
1266
|
delete bProps.heading;
|
|
1159
1267
|
}
|
|
1160
1268
|
}
|
|
1161
|
-
// Remap list item keys inside add_block props (e.g., question→q, answer→a)
|
|
1269
|
+
// Remap list item keys inside add_block props (e.g., question→q, answer→a),
|
|
1270
|
+
// asking the block's own list metadata rather than rewriting every array.
|
|
1162
1271
|
if (block.props && typeof block.props === "object" && !Array.isArray(block.props)) {
|
|
1163
1272
|
const bProps = block.props;
|
|
1164
|
-
const
|
|
1273
|
+
const blockType = typeof block.type === "string" ? block.type : "";
|
|
1165
1274
|
for (const [propKey, propVal] of Object.entries(bProps)) {
|
|
1166
1275
|
if (!Array.isArray(propVal))
|
|
1167
1276
|
continue;
|
|
1168
|
-
bProps[propKey] =
|
|
1169
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
1170
|
-
return item;
|
|
1171
|
-
const entry = item;
|
|
1172
|
-
let changed = false;
|
|
1173
|
-
const mapped = {};
|
|
1174
|
-
for (const [k, v] of Object.entries(entry)) {
|
|
1175
|
-
const alias = itemKeyAliases[k.toLowerCase()];
|
|
1176
|
-
if (alias && !(alias in entry)) {
|
|
1177
|
-
mapped[alias] = v;
|
|
1178
|
-
changed = true;
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
mapped[k] = v;
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return changed ? mapped : entry;
|
|
1185
|
-
});
|
|
1277
|
+
bProps[propKey] = aliasListItems(blockType, propKey, propVal);
|
|
1186
1278
|
}
|
|
1187
1279
|
}
|
|
1188
1280
|
// Block-specific prop key remapping and type coercion
|
|
@@ -1190,10 +1282,7 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1190
1282
|
const cProps = block.props;
|
|
1191
1283
|
const bt = block.type;
|
|
1192
1284
|
// Coerce boolean→string for "true"/"false" enum props (Carousel, Video, Table)
|
|
1193
|
-
|
|
1194
|
-
if (typeof cProps[k] === "boolean")
|
|
1195
|
-
cProps[k] = cProps[k] ? "true" : "false";
|
|
1196
|
-
}
|
|
1285
|
+
coerceBooleanEnumProps(typeof bt === "string" ? bt : "", cProps);
|
|
1197
1286
|
// Carousel: slides→items
|
|
1198
1287
|
if (bt === "Carousel" && Array.isArray(cProps.slides) && !cProps.items) {
|
|
1199
1288
|
cProps.items = cProps.slides;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"openai": "^4.87.1",
|
|
24
24
|
"sharp": "^0.34.5",
|
|
25
25
|
"zod": "^4.3.6",
|
|
26
|
-
"@avocadostudio-ai/migration-sdk": "^0.3.
|
|
27
|
-
"@avocadostudio-ai/shared": "^0.3.
|
|
26
|
+
"@avocadostudio-ai/migration-sdk": "^0.3.2",
|
|
27
|
+
"@avocadostudio-ai/shared": "^0.3.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@google/genai": "^1.46.0",
|