@avocadostudio-ai/orchestrator-core 0.5.0 → 0.5.1
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/agent/sites-agent-context.js +10 -2
- package/dist/chat/anthropic-planner.js +13 -7
- package/dist/chat/hallucination-validator.js +50 -2
- package/dist/chat/planner.js +10 -1
- package/dist/cms/adapter.d.ts +25 -0
- package/dist/handler/create-orchestrator.js +11 -1
- package/dist/nlp/deterministic-planner-context.d.ts +19 -0
- package/dist/nlp/deterministic-planner-context.js +73 -3
- package/dist/state/session-state.d.ts +21 -0
- package/dist/state/session-state.js +24 -1
- package/package.json +3 -3
|
@@ -44,8 +44,16 @@ export function buildBlockCatalog() {
|
|
|
44
44
|
continue; // skip chrome blocks
|
|
45
45
|
const guide = BLOCK_VISUAL_GUIDE[type];
|
|
46
46
|
// Compact: Type — description | props | list fields
|
|
47
|
+
/*
|
|
48
|
+
* `reference` is dropped rather than annotated. It is a pointer the CMS
|
|
49
|
+
* owns — a story link, an entry link — and a model handed the prop name
|
|
50
|
+
* will eventually write a string into it, which replaces the pointer with a
|
|
51
|
+
* hard-coded URL that stops following renames. A prop that must never be
|
|
52
|
+
* written is not a prop worth naming.
|
|
53
|
+
*/
|
|
54
|
+
const writable = ([, fm]) => fm.kind !== "headingLevel" && fm.kind !== "reference";
|
|
47
55
|
const fieldParts = Object.entries(meta.fields)
|
|
48
|
-
.filter(
|
|
56
|
+
.filter(writable) // headingLevel is always optional noise
|
|
49
57
|
.map(([key, fm]) => {
|
|
50
58
|
const req = fm.required ? "" : "?";
|
|
51
59
|
const opts = fm.kind === "enum" && fm.options ? `(${fm.options.join("|")})` : "";
|
|
@@ -54,7 +62,7 @@ export function buildBlockCatalog() {
|
|
|
54
62
|
const listParts = meta.listFields
|
|
55
63
|
? Object.entries(meta.listFields).map(([listKey, listMeta]) => {
|
|
56
64
|
const items = Object.entries(listMeta.itemFields)
|
|
57
|
-
.filter(
|
|
65
|
+
.filter(writable)
|
|
58
66
|
.map(([k, fm]) => `${k}${fm.required ? "" : "?"}`);
|
|
59
67
|
return `${listKey}[]{${items.join(",")}}`;
|
|
60
68
|
})
|
|
@@ -336,13 +336,19 @@ export async function generatePlanWithAnthropic(args) {
|
|
|
336
336
|
const attachmentBlocks = resolvedAttachments.map((attachment) => attachment.kind === "pdf"
|
|
337
337
|
? { type: "document", source: { type: "base64", media_type: "application/pdf", data: attachment.base64 } }
|
|
338
338
|
: { type: "image", source: { type: "base64", media_type: attachment.mediaType, data: attachment.base64 } });
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
339
|
+
/*
|
|
340
|
+
* A URL source is only ever https. Anthropic rejects anything else with
|
|
341
|
+
* `400 … Only HTTPS URLs are supported`, and it rejects the *request*, not the
|
|
342
|
+
* block — so one http image on a selected block failed the whole turn, and the
|
|
343
|
+
* user saw a raw provider error in place of their answer. `fetchImageAsBase64`
|
|
344
|
+
* inlines the loopback case; an image we can neither inline nor link is simply
|
|
345
|
+
* not shown to the model, which costs this turn its vision and nothing else.
|
|
346
|
+
*/
|
|
347
|
+
const visionBlocks = imageBase64
|
|
348
|
+
? [{ type: "image", source: { type: "base64", media_type: imageBase64.mediaType, data: imageBase64.base64 } }]
|
|
349
|
+
: imageUrlForVision && /^https:\/\//i.test(imageUrlForVision)
|
|
350
|
+
? [{ type: "image", source: { type: "url", url: imageUrlForVision } }]
|
|
351
|
+
: [];
|
|
346
352
|
const userContent = visionBlocks.length > 0 || attachmentBlocks.length > 0
|
|
347
353
|
? [...visionBlocks, ...attachmentBlocks, { type: "text", text: JSON.stringify(user) }]
|
|
348
354
|
: JSON.stringify(user);
|
|
@@ -26,6 +26,26 @@ function allowedPropKeysForBlockType(blockType) {
|
|
|
26
26
|
return null;
|
|
27
27
|
return new Set(Object.keys(shape));
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Props that hold a pointer the CMS owns.
|
|
31
|
+
*
|
|
32
|
+
* These are in the schema and therefore survive every check above, which is the
|
|
33
|
+
* problem: a Storyblok story link is `{ linktype, id, cached_url }`, a model
|
|
34
|
+
* writes the rendered href it saw, and the CMS ends up holding a hard-coded URL
|
|
35
|
+
* where a reference was. It renders identically and silently stops following
|
|
36
|
+
* renames — the one thing the reference was for. There is no valid value a
|
|
37
|
+
* planner can produce here, because constructing one needs the CMS's own
|
|
38
|
+
* document ids, so the op is dropped rather than repaired.
|
|
39
|
+
*/
|
|
40
|
+
function referencePropKeysForBlockType(blockType) {
|
|
41
|
+
const meta = getBlockMeta(blockType);
|
|
42
|
+
const keys = new Set();
|
|
43
|
+
for (const [key, field] of Object.entries(meta?.fields ?? {})) {
|
|
44
|
+
if (field.kind === "reference")
|
|
45
|
+
keys.add(key);
|
|
46
|
+
}
|
|
47
|
+
return keys;
|
|
48
|
+
}
|
|
29
49
|
/**
|
|
30
50
|
* Look up the current block by id across all pages in the draft. Preferring
|
|
31
51
|
* the targeted page when supplied avoids cross-page collisions on id reuse.
|
|
@@ -78,6 +98,7 @@ function humanBlockName(blockType) {
|
|
|
78
98
|
export function validateAndStripHallucinatedProps(args) {
|
|
79
99
|
const { plan, draft } = args;
|
|
80
100
|
const hallucinatedProps = [];
|
|
101
|
+
const referenceProps = [];
|
|
81
102
|
for (const op of plan.ops) {
|
|
82
103
|
if (op.op !== "update_props")
|
|
83
104
|
continue;
|
|
@@ -93,7 +114,13 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
93
114
|
const patchCandidate = rawPatch.props && typeof rawPatch.props === "object" && !Array.isArray(rawPatch.props)
|
|
94
115
|
? rawPatch.props
|
|
95
116
|
: rawPatch;
|
|
117
|
+
const referenceKeys = referencePropKeysForBlockType(blockType);
|
|
96
118
|
for (const key of Object.keys(patchCandidate)) {
|
|
119
|
+
if (referenceKeys.has(key)) {
|
|
120
|
+
delete patchCandidate[key];
|
|
121
|
+
referenceProps.push({ blockType, propName: key });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
97
124
|
if (allowedKeys.has(key))
|
|
98
125
|
continue;
|
|
99
126
|
delete patchCandidate[key];
|
|
@@ -105,7 +132,28 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
105
132
|
});
|
|
106
133
|
}
|
|
107
134
|
}
|
|
108
|
-
|
|
135
|
+
/*
|
|
136
|
+
* Said separately from the hallucinated-prop note below, because it is a
|
|
137
|
+
* different fact about a different kind of field. "The block has no such
|
|
138
|
+
* field" is false here — the field exists, it is on the page, and the person
|
|
139
|
+
* can see what it points at. What they cannot do is change it from here.
|
|
140
|
+
*/
|
|
141
|
+
const referenceNoteParts = [];
|
|
142
|
+
if (referenceProps.length > 0) {
|
|
143
|
+
const byBlock = new Map();
|
|
144
|
+
for (const entry of referenceProps) {
|
|
145
|
+
const bucket = byBlock.get(entry.blockType) ?? new Set();
|
|
146
|
+
bucket.add(entry.propName);
|
|
147
|
+
byBlock.set(entry.blockType, bucket);
|
|
148
|
+
}
|
|
149
|
+
for (const [blockType, props] of byBlock) {
|
|
150
|
+
const name = humanBlockName(blockType);
|
|
151
|
+
for (const prop of props) {
|
|
152
|
+
referenceNoteParts.push(`“${prop}” on the ${name} block points at a document in your CMS, so it is changed there rather than here.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (hallucinatedProps.length > 0 || referenceNoteParts.length > 0) {
|
|
109
157
|
/*
|
|
110
158
|
* Two different events used to share one sentence, and the wrong one was
|
|
111
159
|
* the default.
|
|
@@ -130,7 +178,7 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
130
178
|
bucket.add(entry.propName);
|
|
131
179
|
byBlockType.set(entry.blockType, bucket);
|
|
132
180
|
}
|
|
133
|
-
const noteParts = [];
|
|
181
|
+
const noteParts = [...referenceNoteParts];
|
|
134
182
|
for (const [blockType, props] of byBlockType) {
|
|
135
183
|
const name = humanBlockName(blockType);
|
|
136
184
|
const visual = [...props].filter(isVisualPropName);
|
package/dist/chat/planner.js
CHANGED
|
@@ -1190,9 +1190,18 @@ export async function generatePlanWithOpenAI(args) {
|
|
|
1190
1190
|
? args.contextPack.selected.imageUrlForVision
|
|
1191
1191
|
: null;
|
|
1192
1192
|
const imageBase64 = imageUrlForVision ? await fetchImageAsBase64(imageUrlForVision) : null;
|
|
1193
|
+
/*
|
|
1194
|
+
* Same rule as the Anthropic path: a bare URL goes to the provider only when
|
|
1195
|
+
* it is https. A root-relative `/media/...` — which is what a site most often
|
|
1196
|
+
* writes in an image prop — is not a URL any provider can fetch, and sending
|
|
1197
|
+
* it fails the whole call rather than the image. Inline it when we can reach
|
|
1198
|
+
* it, drop it when we cannot.
|
|
1199
|
+
*/
|
|
1193
1200
|
const visionImageUrl = imageBase64
|
|
1194
1201
|
? `data:${imageBase64.mediaType};base64,${imageBase64.base64}`
|
|
1195
|
-
: imageUrlForVision
|
|
1202
|
+
: imageUrlForVision && /^https:\/\//i.test(imageUrlForVision)
|
|
1203
|
+
? imageUrlForVision
|
|
1204
|
+
: null;
|
|
1196
1205
|
// User-attached files: images as image_url parts, PDFs as `file` parts with
|
|
1197
1206
|
// inline base64 data. The `file` content part is only accepted by the gpt-4o /
|
|
1198
1207
|
// gpt-4.1 / gpt-5 families on chat.completions; sending it to other models
|
package/dist/cms/adapter.d.ts
CHANGED
|
@@ -35,14 +35,39 @@ export interface CmsPublishContext {
|
|
|
35
35
|
* destroys what it was projected from. A publisher has to diff, and it cannot
|
|
36
36
|
* diff against nothing.
|
|
37
37
|
*
|
|
38
|
+
* **This is the CMS's draft, not the live site**, and the distinction decides
|
|
39
|
+
* whether a publish is correct. `getPages({ perspective: "draft" })` is what
|
|
40
|
+
* seeded the session, and the baseline is that same list, so the diff
|
|
41
|
+
* compares like with like and reports what *this session* changed. Diff
|
|
42
|
+
* against the live site instead and the difference includes every
|
|
43
|
+
* unpublished edit anybody made in the CMS — an Avocado publish then ships
|
|
44
|
+
* all of it, from a button whose label says nothing about that. An adapter
|
|
45
|
+
* that does not declare `perspectives` ignores the argument and answers with
|
|
46
|
+
* whatever it has, in which case the two are the same thing anyway.
|
|
47
|
+
*
|
|
38
48
|
* Absent when the session was never bootstrapped from the adapter, so treat
|
|
39
49
|
* `undefined` as "no baseline available" and not as "the site was empty" —
|
|
40
50
|
* publishing every field on that assumption is the overwrite this exists to
|
|
41
51
|
* prevent. An integration that embeds its own source snapshot in block props
|
|
42
52
|
* (the more precise approach, since only it knows what it projected) can
|
|
43
53
|
* ignore this.
|
|
54
|
+
*
|
|
55
|
+
* Prefer `baseline`. This field holds the same array and is kept because
|
|
56
|
+
* adapters are written against it.
|
|
57
|
+
*
|
|
58
|
+
* @deprecated Misnamed — read `baseline`.
|
|
44
59
|
*/
|
|
45
60
|
published?: PageDoc[];
|
|
61
|
+
/**
|
|
62
|
+
* The same array as `published`, under the name that says what it holds.
|
|
63
|
+
*
|
|
64
|
+
* The old name taught the opposite of the truth: an integrator reading
|
|
65
|
+
* "published" reasons that the baseline is the live site, concludes the diff
|
|
66
|
+
* would carry colleagues' unpublished work, and designs around a problem the
|
|
67
|
+
* orchestrator does not have — or, worse, does a second read of the published
|
|
68
|
+
* perspective and creates it.
|
|
69
|
+
*/
|
|
70
|
+
baseline?: PageDoc[];
|
|
46
71
|
}
|
|
47
72
|
/**
|
|
48
73
|
* Result of an `onPublish` call. `void` means "success, nothing to report";
|
|
@@ -906,8 +906,18 @@ export function createOrchestrator(config = {}) {
|
|
|
906
906
|
return jsonResponse({ ok: true, written: false, count: pages.length, reason: "adapter has no onPublish; publish is a no-op" }, { status: 200, cors });
|
|
907
907
|
}
|
|
908
908
|
const config = selection.siteConfig;
|
|
909
|
+
/*
|
|
910
|
+
* `baseline` and `published` are the same array. The second name is what
|
|
911
|
+
* adapters were written against and the first is what it actually holds
|
|
912
|
+
* — the CMS's *draft*, read with `perspective: "draft"` when the adapter
|
|
913
|
+
* declares one, which is the only baseline that makes a publish mean
|
|
914
|
+
* "what this session changed".
|
|
915
|
+
*/
|
|
909
916
|
const context = body.assets || published
|
|
910
|
-
? {
|
|
917
|
+
? {
|
|
918
|
+
...(body.assets ? { assets: body.assets } : {}),
|
|
919
|
+
...(published ? { published, baseline: published } : {})
|
|
920
|
+
}
|
|
911
921
|
: undefined;
|
|
912
922
|
let unsupported = [];
|
|
913
923
|
// Default true: an adapter that does not mention `written` means what
|
|
@@ -11,6 +11,25 @@ export declare function readPathValue(root: unknown, path: string): unknown;
|
|
|
11
11
|
* - "items[0].image.alt" → props.items[0].image.src
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveImageUrlForAltField(blockProps: Record<string, unknown>, editablePath: string): string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Turn whatever the page wrote in an image prop into a URL something can
|
|
16
|
+
* actually fetch, or nothing at all.
|
|
17
|
+
*
|
|
18
|
+
* `resolveImageUrlForAltField` reads its answer straight out of block props, so
|
|
19
|
+
* it is whatever the site put there — and a site most often puts a root-relative
|
|
20
|
+
* path (`/media/pool.webp`), which names a file on the *site* and is not a URL
|
|
21
|
+
* any model, or this process, can resolve on its own. It went to the provider
|
|
22
|
+
* verbatim, and Anthropic answered `400 … Only HTTPS URLs are supported` for the
|
|
23
|
+
* whole request: selecting a hero image and asking an unrelated question failed
|
|
24
|
+
* the turn outright, over the least important thing it was carrying.
|
|
25
|
+
*
|
|
26
|
+
* `previewUrl` is the site's own origin — the same value `/preview/screenshot`
|
|
27
|
+
* resolves a page path against — so a relative path becomes an absolute one the
|
|
28
|
+
* loopback fetch below can turn into base64. A site that never declared one
|
|
29
|
+
* gets `undefined` and loses vision for that turn, which is the correct trade:
|
|
30
|
+
* the alt text is a nicety, the answer is the request.
|
|
31
|
+
*/
|
|
32
|
+
export declare function absolutizeVisionImageUrl(raw: string, previewUrl?: unknown): string | undefined;
|
|
14
33
|
/**
|
|
15
34
|
* Fetch an image URL and return base64 + media type.
|
|
16
35
|
* For localhost URLs the AI APIs can't reach, we fetch locally.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSessionDraft, getRecentEdits, getSiteConfig, orderSlugsHomeFirst } from "../state/session-state.js";
|
|
2
|
+
import { getLibraryMount } from "../handler/library-mount.js";
|
|
2
3
|
import { resolveReferencesFromMessage } from "./deterministic-planner-refs.js";
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Path traversal
|
|
@@ -58,6 +59,71 @@ export function resolveImageUrlForAltField(blockProps, editablePath) {
|
|
|
58
59
|
const value = readPathValue(blockProps, companionPath);
|
|
59
60
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Turn whatever the page wrote in an image prop into a URL something can
|
|
64
|
+
* actually fetch, or nothing at all.
|
|
65
|
+
*
|
|
66
|
+
* `resolveImageUrlForAltField` reads its answer straight out of block props, so
|
|
67
|
+
* it is whatever the site put there — and a site most often puts a root-relative
|
|
68
|
+
* path (`/media/pool.webp`), which names a file on the *site* and is not a URL
|
|
69
|
+
* any model, or this process, can resolve on its own. It went to the provider
|
|
70
|
+
* verbatim, and Anthropic answered `400 … Only HTTPS URLs are supported` for the
|
|
71
|
+
* whole request: selecting a hero image and asking an unrelated question failed
|
|
72
|
+
* the turn outright, over the least important thing it was carrying.
|
|
73
|
+
*
|
|
74
|
+
* `previewUrl` is the site's own origin — the same value `/preview/screenshot`
|
|
75
|
+
* resolves a page path against — so a relative path becomes an absolute one the
|
|
76
|
+
* loopback fetch below can turn into base64. A site that never declared one
|
|
77
|
+
* gets `undefined` and loses vision for that turn, which is the correct trade:
|
|
78
|
+
* the alt text is a nicety, the answer is the request.
|
|
79
|
+
*/
|
|
80
|
+
export function absolutizeVisionImageUrl(raw, previewUrl) {
|
|
81
|
+
if (!raw)
|
|
82
|
+
return undefined;
|
|
83
|
+
if (/^data:/i.test(raw))
|
|
84
|
+
return undefined;
|
|
85
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw))
|
|
86
|
+
return raw;
|
|
87
|
+
// Protocol-relative (`//host/x.png`) has an origin already; anything else
|
|
88
|
+
// relative needs one, and only the site can supply it.
|
|
89
|
+
if (raw.startsWith("//"))
|
|
90
|
+
return `https:${raw}`;
|
|
91
|
+
if (typeof previewUrl !== "string" || previewUrl.trim() === "")
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
return new URL(raw, previewUrl.replace(/\/+$/, "") + "/").toString();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The origin a relative image path belongs to: this site's own.
|
|
102
|
+
*
|
|
103
|
+
* Two sources, because neither covers both modes. `previewUrl` is what
|
|
104
|
+
* `/sites/register` writes and what `/preview/screenshot` resolves page paths
|
|
105
|
+
* against — but library mode passes it to `createOrchestrator()` rather than
|
|
106
|
+
* registering it, so it is absent from the config of the very sites whose
|
|
107
|
+
* content is most likely to be relative. The library mount is that site's own
|
|
108
|
+
* address, observed from a request it actually served, so its origin is the
|
|
109
|
+
* site: exactly the base a `/media/...` path is written against.
|
|
110
|
+
*/
|
|
111
|
+
function siteOriginForVision(session) {
|
|
112
|
+
// `previewUrl` is not in `siteConfigSchema` — `/sites/register` merges it in
|
|
113
|
+
// as an unvalidated extra, so it can only be read off the config as unknown.
|
|
114
|
+
const registered = getSiteConfig(session).previewUrl;
|
|
115
|
+
if (typeof registered === "string" && registered.trim() !== "")
|
|
116
|
+
return registered;
|
|
117
|
+
const mount = getLibraryMount();
|
|
118
|
+
if (!mount)
|
|
119
|
+
return undefined;
|
|
120
|
+
try {
|
|
121
|
+
return new URL(mount).origin;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
61
127
|
function isLocalUrl(url) {
|
|
62
128
|
try {
|
|
63
129
|
const parsed = new URL(url);
|
|
@@ -302,6 +368,12 @@ export function plannerContextPack(args) {
|
|
|
302
368
|
const { session, slug, message, currentPage, activeBlockId, activeBlockType, activeEditablePath } = args;
|
|
303
369
|
const pageRoutes = orderSlugsHomeFirst(Array.from(getSessionDraft(session).keys()));
|
|
304
370
|
const siteConfigSummary = summarizeSiteConfigForPlanner(getSiteConfig(session), pageRoutes);
|
|
371
|
+
const rawVisionImageUrl = activeBlockId && activeEditablePath
|
|
372
|
+
? resolveImageUrlForAltField((currentPage.blocks.find((b) => b.id === activeBlockId)?.props ?? {}), activeEditablePath)
|
|
373
|
+
: undefined;
|
|
374
|
+
const visionImageUrl = rawVisionImageUrl
|
|
375
|
+
? absolutizeVisionImageUrl(rawVisionImageUrl, siteOriginForVision(session))
|
|
376
|
+
: undefined;
|
|
305
377
|
const selectedIdx = activeBlockId ? currentPage.blocks.findIndex((b) => b.id === activeBlockId) : -1;
|
|
306
378
|
const neighbors = selectedIdx >= 0
|
|
307
379
|
? {
|
|
@@ -320,9 +392,7 @@ export function plannerContextPack(args) {
|
|
|
320
392
|
blockType: activeBlockType ?? null,
|
|
321
393
|
editablePath: activeEditablePath ?? null,
|
|
322
394
|
block: selectedBlockSnapshot({ currentPage, activeBlockId, activeEditablePath }),
|
|
323
|
-
imageUrlForVision:
|
|
324
|
-
? resolveImageUrlForAltField((currentPage.blocks.find((b) => b.id === activeBlockId)?.props ?? {}), activeEditablePath) ?? null
|
|
325
|
-
: null
|
|
395
|
+
imageUrlForVision: visionImageUrl ?? null
|
|
326
396
|
},
|
|
327
397
|
neighbors: {
|
|
328
398
|
previous: neighbors.previous ? { id: neighbors.previous.id, type: neighbors.previous.type } : null,
|
|
@@ -252,6 +252,27 @@ export declare function publishedPageCountGlobal(): number;
|
|
|
252
252
|
export declare function markRecentlyRestored(session: string): void;
|
|
253
253
|
export declare function consumeRecentlyRestored(session: string): boolean;
|
|
254
254
|
export declare const toErrorDetail: typeof _unifiedToErrorDetail;
|
|
255
|
+
/**
|
|
256
|
+
* Fill in the props Avocado's own Hero and TwoColumn need but a stored page may
|
|
257
|
+
* predate.
|
|
258
|
+
*
|
|
259
|
+
* **Only for blocks Avocado defines.** Both halves are keyed on a type *name*,
|
|
260
|
+
* and a name is all a site's own `Hero` shares with ours. Run them against one
|
|
261
|
+
* and they invent content: a `Hero` whose image lives in `backgroundAsset` is
|
|
262
|
+
* given an `imageUrl` pointing at our placeholder and an English alt string on
|
|
263
|
+
* a German page; a `TwoColumn` with its own `variant` has it rewritten to
|
|
264
|
+
* `"default"`, and its `heading`/`body` are copied into a `left`/`right` pair
|
|
265
|
+
* it does not render. Nothing is lost visibly, because the site renders its own
|
|
266
|
+
* props and ignores the additions — but they land in the draft, the publish
|
|
267
|
+
* baseline is the un-mutated page the adapter returned, so the diff attributes
|
|
268
|
+
* every one of them to the session, and a publish writes them into the CMS.
|
|
269
|
+
* This ran on the CMS seed path, which is where a page nobody edited picks them
|
|
270
|
+
* up.
|
|
271
|
+
*
|
|
272
|
+
* `isBuiltinBlock` is written at registration, so a site that registers over
|
|
273
|
+
* `Hero` turns both halves off for its own blocks and leaves them on for any
|
|
274
|
+
* built-in it still uses.
|
|
275
|
+
*/
|
|
255
276
|
export declare function ensureHeroImageProps(page: PageDoc): void;
|
|
256
277
|
export declare function orderSlugsHomeFirst(slugs: string[]): string[];
|
|
257
278
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { demoPublishedPages, demoSiteConfig, ensureItemIds, IMAGE_PLACEHOLDER } from "@avocadostudio-ai/shared";
|
|
3
|
+
import { demoPublishedPages, demoSiteConfig, ensureItemIds, IMAGE_PLACEHOLDER, isBuiltinBlock } from "@avocadostudio-ai/shared";
|
|
4
4
|
import { toErrorDetail as _unifiedToErrorDetail } from "../errors.js";
|
|
5
5
|
import { archiveMigratedJson, getStore, notePersistenceFailure, notePersistenceSuccess, readLegacyJson, resolveDbFile, resolveJsonMigrationTtlDays, sweepStaleMigrations, } from "./sqlite-store-singleton.js";
|
|
6
6
|
import { discardPendingProposalsForSession, sweepExpiredProposals } from "../durable/durable-store-singleton.js";
|
|
@@ -255,9 +255,32 @@ export const toErrorDetail = _unifiedToErrorDetail;
|
|
|
255
255
|
// ---------------------------------------------------------------------------
|
|
256
256
|
// Hero image prop guard (needed by getSessionDraft, setPage, applyPersistedState)
|
|
257
257
|
// ---------------------------------------------------------------------------
|
|
258
|
+
/**
|
|
259
|
+
* Fill in the props Avocado's own Hero and TwoColumn need but a stored page may
|
|
260
|
+
* predate.
|
|
261
|
+
*
|
|
262
|
+
* **Only for blocks Avocado defines.** Both halves are keyed on a type *name*,
|
|
263
|
+
* and a name is all a site's own `Hero` shares with ours. Run them against one
|
|
264
|
+
* and they invent content: a `Hero` whose image lives in `backgroundAsset` is
|
|
265
|
+
* given an `imageUrl` pointing at our placeholder and an English alt string on
|
|
266
|
+
* a German page; a `TwoColumn` with its own `variant` has it rewritten to
|
|
267
|
+
* `"default"`, and its `heading`/`body` are copied into a `left`/`right` pair
|
|
268
|
+
* it does not render. Nothing is lost visibly, because the site renders its own
|
|
269
|
+
* props and ignores the additions — but they land in the draft, the publish
|
|
270
|
+
* baseline is the un-mutated page the adapter returned, so the diff attributes
|
|
271
|
+
* every one of them to the session, and a publish writes them into the CMS.
|
|
272
|
+
* This ran on the CMS seed path, which is where a page nobody edited picks them
|
|
273
|
+
* up.
|
|
274
|
+
*
|
|
275
|
+
* `isBuiltinBlock` is written at registration, so a site that registers over
|
|
276
|
+
* `Hero` turns both halves off for its own blocks and leaves them on for any
|
|
277
|
+
* built-in it still uses.
|
|
278
|
+
*/
|
|
258
279
|
export function ensureHeroImageProps(page) {
|
|
259
280
|
for (const block of page.blocks) {
|
|
260
281
|
const props = block.props;
|
|
282
|
+
if (!isBuiltinBlock(block.type))
|
|
283
|
+
continue;
|
|
261
284
|
if (block.type === "Hero") {
|
|
262
285
|
// Skip imageUrl fallback if the block uses carouselImages instead
|
|
263
286
|
if (typeof props.imageUrl !== "string" || props.imageUrl.length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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.5.
|
|
27
|
-
"@avocadostudio-ai/shared": "^0.5.
|
|
26
|
+
"@avocadostudio-ai/migration-sdk": "^0.5.1",
|
|
27
|
+
"@avocadostudio-ai/shared": "^0.5.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@google/genai": "^1.46.0",
|