@avocadostudio-ai/orchestrator-core 0.5.1 → 0.6.0
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.
|
@@ -45,13 +45,18 @@ export function buildBlockCatalog() {
|
|
|
45
45
|
const guide = BLOCK_VISUAL_GUIDE[type];
|
|
46
46
|
// Compact: Type — description | props | list fields
|
|
47
47
|
/*
|
|
48
|
+
* An `internal` field is dropped for a stronger reason than either: it is
|
|
49
|
+
* the CMS's own bookkeeping — a `_uid`, a revision stamp — and the site
|
|
50
|
+
* has said outright that it is not content. Naming it invites an edit to
|
|
51
|
+
* it.
|
|
52
|
+
*
|
|
48
53
|
* `reference` is dropped rather than annotated. It is a pointer the CMS
|
|
49
54
|
* owns — a story link, an entry link — and a model handed the prop name
|
|
50
55
|
* will eventually write a string into it, which replaces the pointer with a
|
|
51
56
|
* hard-coded URL that stops following renames. A prop that must never be
|
|
52
57
|
* written is not a prop worth naming.
|
|
53
58
|
*/
|
|
54
|
-
const writable = ([, fm]) => fm.kind !== "headingLevel" && fm.kind !== "reference";
|
|
59
|
+
const writable = ([, fm]) => fm.kind !== "headingLevel" && fm.kind !== "reference" && !fm.internal;
|
|
55
60
|
const fieldParts = Object.entries(meta.fields)
|
|
56
61
|
.filter(writable) // headingLevel is always optional noise
|
|
57
62
|
.map(([key, fm]) => {
|
|
@@ -232,6 +232,17 @@ export function findExplicitCtaTargetCoverageGap(args) {
|
|
|
232
232
|
function isTranslatableKind(kind) {
|
|
233
233
|
return kind === "text" || kind === "richtext" || kind === "imageAlt";
|
|
234
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Prose, and meant for a reader.
|
|
237
|
+
*
|
|
238
|
+
* A CMS's `_uid` is typed `text` and holds a string, so a translation
|
|
239
|
+
* checklist built on kind alone demands a French version of a UUID — and then
|
|
240
|
+
* reports the page as incompletely translated forever, because the planner
|
|
241
|
+
* quite rightly never produces one.
|
|
242
|
+
*/
|
|
243
|
+
function isTranslatableField(fm) {
|
|
244
|
+
return !fm.internal && isTranslatableKind(fm.kind);
|
|
245
|
+
}
|
|
235
246
|
/**
|
|
236
247
|
* The translatable surface of one block: which top-level props and which
|
|
237
248
|
* list-child fields carry prose. One definition, used both to tell the planner
|
|
@@ -239,13 +250,13 @@ function isTranslatableKind(kind) {
|
|
|
239
250
|
*/
|
|
240
251
|
function translatableFieldKeys(meta) {
|
|
241
252
|
const topLevel = Object.entries(meta?.fields ?? {})
|
|
242
|
-
.filter(([, fm]) =>
|
|
253
|
+
.filter(([, fm]) => isTranslatableField(fm))
|
|
243
254
|
.map(([key]) => key);
|
|
244
255
|
const lists = Object.entries(meta?.listFields ?? {})
|
|
245
256
|
.map(([listKey, listMeta]) => [
|
|
246
257
|
listKey,
|
|
247
258
|
Object.entries(listMeta.itemFields ?? {})
|
|
248
|
-
.filter(([, fm]) =>
|
|
259
|
+
.filter(([, fm]) => isTranslatableField(fm))
|
|
249
260
|
.map(([key]) => key)
|
|
250
261
|
])
|
|
251
262
|
.filter(([, itemFields]) => itemFields.length > 0);
|
|
@@ -46,6 +46,25 @@ function referencePropKeysForBlockType(blockType) {
|
|
|
46
46
|
}
|
|
47
47
|
return keys;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Props the site has declared as its own bookkeeping.
|
|
51
|
+
*
|
|
52
|
+
* A `_uid`, a revision stamp, a `__source` snapshot. They are in the schema —
|
|
53
|
+
* the publisher needs them — so every check above passes them, and the planner
|
|
54
|
+
* never sees them in the block summary. A plan that names one therefore did
|
|
55
|
+
* not read it anywhere; it invented a plausible key, which for identity fields
|
|
56
|
+
* is exactly the kind of key that is easy to invent. Writing it would corrupt
|
|
57
|
+
* the CMS's own link between the draft and the document it came from.
|
|
58
|
+
*/
|
|
59
|
+
function internalPropKeysForBlockType(blockType) {
|
|
60
|
+
const meta = getBlockMeta(blockType);
|
|
61
|
+
const keys = new Set();
|
|
62
|
+
for (const [key, field] of Object.entries(meta?.fields ?? {})) {
|
|
63
|
+
if (field.internal)
|
|
64
|
+
keys.add(key);
|
|
65
|
+
}
|
|
66
|
+
return keys;
|
|
67
|
+
}
|
|
49
68
|
/**
|
|
50
69
|
* Look up the current block by id across all pages in the draft. Preferring
|
|
51
70
|
* the targeted page when supplied avoids cross-page collisions on id reuse.
|
|
@@ -99,6 +118,7 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
99
118
|
const { plan, draft } = args;
|
|
100
119
|
const hallucinatedProps = [];
|
|
101
120
|
const referenceProps = [];
|
|
121
|
+
const internalProps = [];
|
|
102
122
|
for (const op of plan.ops) {
|
|
103
123
|
if (op.op !== "update_props")
|
|
104
124
|
continue;
|
|
@@ -115,12 +135,18 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
115
135
|
? rawPatch.props
|
|
116
136
|
: rawPatch;
|
|
117
137
|
const referenceKeys = referencePropKeysForBlockType(blockType);
|
|
138
|
+
const internalKeys = internalPropKeysForBlockType(blockType);
|
|
118
139
|
for (const key of Object.keys(patchCandidate)) {
|
|
119
140
|
if (referenceKeys.has(key)) {
|
|
120
141
|
delete patchCandidate[key];
|
|
121
142
|
referenceProps.push({ blockType, propName: key });
|
|
122
143
|
continue;
|
|
123
144
|
}
|
|
145
|
+
if (internalKeys.has(key)) {
|
|
146
|
+
delete patchCandidate[key];
|
|
147
|
+
internalProps.push({ blockType, propName: key });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
124
150
|
if (allowedKeys.has(key))
|
|
125
151
|
continue;
|
|
126
152
|
delete patchCandidate[key];
|
|
@@ -153,7 +179,28 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
153
179
|
}
|
|
154
180
|
}
|
|
155
181
|
}
|
|
156
|
-
|
|
182
|
+
/*
|
|
183
|
+
* An internal prop is the quietest of the three and gets the shortest
|
|
184
|
+
* sentence. The person did not ask for it — no request mentions a `_uid` —
|
|
185
|
+
* so the note exists to explain why one op did less than the plan said, not
|
|
186
|
+
* to teach them anything about the field.
|
|
187
|
+
*/
|
|
188
|
+
const internalNoteParts = [];
|
|
189
|
+
if (internalProps.length > 0) {
|
|
190
|
+
const byBlock = new Map();
|
|
191
|
+
for (const entry of internalProps) {
|
|
192
|
+
const bucket = byBlock.get(entry.blockType) ?? new Set();
|
|
193
|
+
bucket.add(entry.propName);
|
|
194
|
+
byBlock.set(entry.blockType, bucket);
|
|
195
|
+
}
|
|
196
|
+
for (const [blockType, props] of byBlock) {
|
|
197
|
+
const name = humanBlockName(blockType);
|
|
198
|
+
for (const prop of props) {
|
|
199
|
+
internalNoteParts.push(`“${prop}” on the ${name} block is managed by your site, not edited here, so it was left alone.`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (hallucinatedProps.length > 0 || referenceNoteParts.length > 0 || internalNoteParts.length > 0) {
|
|
157
204
|
/*
|
|
158
205
|
* Two different events used to share one sentence, and the wrong one was
|
|
159
206
|
* the default.
|
|
@@ -178,7 +225,7 @@ export function validateAndStripHallucinatedProps(args) {
|
|
|
178
225
|
bucket.add(entry.propName);
|
|
179
226
|
byBlockType.set(entry.blockType, bucket);
|
|
180
227
|
}
|
|
181
|
-
const noteParts = [...referenceNoteParts];
|
|
228
|
+
const noteParts = [...referenceNoteParts, ...internalNoteParts];
|
|
182
229
|
for (const [blockType, props] of byBlockType) {
|
|
183
230
|
const name = humanBlockName(blockType);
|
|
184
231
|
const visual = [...props].filter(isVisualPropName);
|
package/dist/cms/adapter.d.ts
CHANGED
|
@@ -72,16 +72,19 @@ export interface CmsPublishContext {
|
|
|
72
72
|
/**
|
|
73
73
|
* Result of an `onPublish` call. `void` means "success, nothing to report";
|
|
74
74
|
* an explicit `{ ok: false, error }` lets the adapter surface a per-publish
|
|
75
|
-
* error message that the orchestrator returns to the client
|
|
75
|
+
* error message that the orchestrator returns to the client, and `notes` lets
|
|
76
|
+
* a *successful* one say what it did.
|
|
76
77
|
*/
|
|
77
78
|
export type CmsPublishResult = void | {
|
|
78
79
|
ok: true;
|
|
79
80
|
written?: boolean;
|
|
80
81
|
unsupported?: string[];
|
|
82
|
+
notes?: string[];
|
|
81
83
|
} | {
|
|
82
84
|
ok: false;
|
|
83
85
|
error?: string;
|
|
84
86
|
unsupported?: string[];
|
|
87
|
+
notes?: string[];
|
|
85
88
|
};
|
|
86
89
|
/**
|
|
87
90
|
* One asset from a CMS's media library, in the shape the editor's image picker
|
|
@@ -344,6 +344,52 @@ function stripBasePath(pathname, basePath) {
|
|
|
344
344
|
return pathname.slice(basePath.length) || "/";
|
|
345
345
|
return pathname || "/";
|
|
346
346
|
}
|
|
347
|
+
/** One note may not be longer than this, and a publish may not carry more. */
|
|
348
|
+
const PUBLISH_NOTE_MAX_LENGTH = 500;
|
|
349
|
+
const PUBLISH_NOTE_MAX_COUNT = 20;
|
|
350
|
+
/**
|
|
351
|
+
* An adapter's notes are prose from another codebase, and they end up in a
|
|
352
|
+
* SQLite row and in the editor's chat transcript. Keep what is readable and
|
|
353
|
+
* drop the rest rather than trusting the shape.
|
|
354
|
+
*/
|
|
355
|
+
function sanitizePublishNotes(notes) {
|
|
356
|
+
const kept = [];
|
|
357
|
+
for (const note of notes) {
|
|
358
|
+
if (typeof note !== "string")
|
|
359
|
+
continue;
|
|
360
|
+
const trimmed = note.trim();
|
|
361
|
+
if (trimmed === "")
|
|
362
|
+
continue;
|
|
363
|
+
kept.push(trimmed.length > PUBLISH_NOTE_MAX_LENGTH ? `${trimmed.slice(0, PUBLISH_NOTE_MAX_LENGTH - 1)}…` : trimmed);
|
|
364
|
+
if (kept.length === PUBLISH_NOTE_MAX_COUNT)
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
return kept;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The fields the editor reads off a publish response.
|
|
371
|
+
*
|
|
372
|
+
* Library mode answered a successful publish with `{ ok: true, written, count }`
|
|
373
|
+
* and no `status`, and the editor's one check is
|
|
374
|
+
* `data.status !== "triggered" && data.status !== "ready"` — so every
|
|
375
|
+
* successful CMS publish was announced to the person who pressed the button as
|
|
376
|
+
* "Failed to trigger publish." The HTTP status was 200 and the CMS had the
|
|
377
|
+
* writes; only the sentence was wrong, which is the worst way for it to be
|
|
378
|
+
* wrong: it invites a second publish, and then a third.
|
|
379
|
+
*
|
|
380
|
+
* `ok`/`written`/`count` stay exactly as they were — an integration reading
|
|
381
|
+
* them is not disturbed by the additions. This mirrors the shape the
|
|
382
|
+
* site-contract target already returns, which is why the editor can read it.
|
|
383
|
+
*/
|
|
384
|
+
function publishEnvelope(session, slugs, message) {
|
|
385
|
+
return {
|
|
386
|
+
status: "ready",
|
|
387
|
+
...(session ? { session } : {}),
|
|
388
|
+
slugs,
|
|
389
|
+
vercelState: "READY",
|
|
390
|
+
message
|
|
391
|
+
};
|
|
392
|
+
}
|
|
347
393
|
/**
|
|
348
394
|
* Build a Web-standard request handler that wraps the orchestrator brain.
|
|
349
395
|
*
|
|
@@ -902,8 +948,15 @@ export function createOrchestrator(config = {}) {
|
|
|
902
948
|
}
|
|
903
949
|
};
|
|
904
950
|
if (!runtime.adapter?.onPublish) {
|
|
951
|
+
const reason = "adapter has no onPublish; publish is a no-op";
|
|
905
952
|
record(true, "Nothing written — the adapter has no onPublish");
|
|
906
|
-
return jsonResponse({
|
|
953
|
+
return jsonResponse({
|
|
954
|
+
...publishEnvelope(body.session, slugs, reason),
|
|
955
|
+
ok: true,
|
|
956
|
+
written: false,
|
|
957
|
+
count: pages.length,
|
|
958
|
+
reason
|
|
959
|
+
}, { status: 200, cors });
|
|
907
960
|
}
|
|
908
961
|
const config = selection.siteConfig;
|
|
909
962
|
/*
|
|
@@ -920,6 +973,7 @@ export function createOrchestrator(config = {}) {
|
|
|
920
973
|
}
|
|
921
974
|
: undefined;
|
|
922
975
|
let unsupported = [];
|
|
976
|
+
let notes = [];
|
|
923
977
|
// Default true: an adapter that does not mention `written` means what
|
|
924
978
|
// every adapter written before the field existed meant.
|
|
925
979
|
let written = true;
|
|
@@ -928,6 +982,9 @@ export function createOrchestrator(config = {}) {
|
|
|
928
982
|
if (result && typeof result === "object" && Array.isArray(result.unsupported)) {
|
|
929
983
|
unsupported = result.unsupported;
|
|
930
984
|
}
|
|
985
|
+
if (result && typeof result === "object" && Array.isArray(result.notes)) {
|
|
986
|
+
notes = sanitizePublishNotes(result.notes);
|
|
987
|
+
}
|
|
931
988
|
if (result && typeof result === "object" && result.ok === true && result.written === false) {
|
|
932
989
|
written = false;
|
|
933
990
|
}
|
|
@@ -936,11 +993,13 @@ export function createOrchestrator(config = {}) {
|
|
|
936
993
|
runtime.log.warn({ session: scopedSession, adapter: runtime.adapter.id, error: result.error }, "library-publish: adapter.onPublish() returned not-ok");
|
|
937
994
|
record(false, message, message);
|
|
938
995
|
return jsonResponse({
|
|
996
|
+
status: "failed",
|
|
939
997
|
ok: false,
|
|
940
998
|
written: false,
|
|
941
999
|
count: pages.length,
|
|
942
1000
|
error: message,
|
|
943
|
-
...(unsupported.length > 0 ? { unsupported } : {})
|
|
1001
|
+
...(unsupported.length > 0 ? { unsupported } : {}),
|
|
1002
|
+
...(notes.length > 0 ? { notes } : {})
|
|
944
1003
|
}, { status: 502, cors });
|
|
945
1004
|
}
|
|
946
1005
|
}
|
|
@@ -959,14 +1018,29 @@ export function createOrchestrator(config = {}) {
|
|
|
959
1018
|
const summary = written
|
|
960
1019
|
? buildPublishSummary({ changedSlugs: [], removedSlugs: [], totalPages: pages.length, hasDiff: false })
|
|
961
1020
|
: `Computed ${pages.length} ${pages.length === 1 ? "page" : "pages"} — nothing written`;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1021
|
+
/*
|
|
1022
|
+
* The adapter's own sentences go in the log row too. A publish log that
|
|
1023
|
+
* records "Published 12 pages" for a run that wrote nothing because the
|
|
1024
|
+
* queue was paused is worse than no log: it is a record of something
|
|
1025
|
+
* that did not happen.
|
|
1026
|
+
*/
|
|
1027
|
+
const logged = [
|
|
1028
|
+
summary,
|
|
1029
|
+
unsupported.length > 0
|
|
1030
|
+
? `${unsupported.length} change${unsupported.length === 1 ? "" : "s"} could not be published.`
|
|
1031
|
+
: "",
|
|
1032
|
+
...notes
|
|
1033
|
+
]
|
|
1034
|
+
.filter((part) => part !== "")
|
|
1035
|
+
.join(" ");
|
|
1036
|
+
record(true, logged);
|
|
965
1037
|
return jsonResponse({
|
|
1038
|
+
...publishEnvelope(body.session, slugs, logged),
|
|
966
1039
|
ok: true,
|
|
967
1040
|
written,
|
|
968
1041
|
count: pages.length,
|
|
969
|
-
...(unsupported.length > 0 ? { unsupported } : {})
|
|
1042
|
+
...(unsupported.length > 0 ? { unsupported } : {}),
|
|
1043
|
+
...(notes.length > 0 ? { notes } : {})
|
|
970
1044
|
}, { status: 200, cors });
|
|
971
1045
|
}
|
|
972
1046
|
// The editor polls this on boot to populate its model selector and the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -16,17 +16,29 @@
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
20
19
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
21
20
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
21
|
"better-sqlite3": "^12.9.0",
|
|
23
22
|
"openai": "^4.87.1",
|
|
24
23
|
"sharp": "^0.34.5",
|
|
25
24
|
"zod": "^4.3.6",
|
|
26
|
-
"@avocadostudio-ai/migration-sdk": "^0.
|
|
27
|
-
"@avocadostudio-ai/shared": "^0.
|
|
25
|
+
"@avocadostudio-ai/migration-sdk": "^0.6.0",
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.6.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"googleapis": "^171.4.0",
|
|
30
|
+
"@google/genai": "^1.46.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"googleapis": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"@google/genai": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
28
39
|
},
|
|
29
40
|
"devDependencies": {
|
|
41
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
|
30
42
|
"@google/genai": "^1.46.0",
|
|
31
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
32
44
|
"@types/node": "^22.13.10",
|
|
@@ -65,18 +77,6 @@
|
|
|
65
77
|
"url": "https://github.com/avocadostudio-ai/avocado.git",
|
|
66
78
|
"directory": "packages/orchestrator-core"
|
|
67
79
|
},
|
|
68
|
-
"peerDependencies": {
|
|
69
|
-
"googleapis": "^171.4.0",
|
|
70
|
-
"@google/genai": "^1.46.0"
|
|
71
|
-
},
|
|
72
|
-
"peerDependenciesMeta": {
|
|
73
|
-
"googleapis": {
|
|
74
|
-
"optional": true
|
|
75
|
-
},
|
|
76
|
-
"@google/genai": {
|
|
77
|
-
"optional": true
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
80
|
"scripts": {
|
|
81
81
|
"typecheck": "tsc --noEmit",
|
|
82
82
|
"build": "tsc -p tsconfig.build.json && node ./scripts/copy-assets.mjs",
|