@avocadostudio-ai/orchestrator-core 0.3.2 → 0.3.3
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/anthropic-planner.d.ts +8 -0
- package/dist/chat/anthropic-planner.js +166 -12
- package/dist/chat/chat-pipeline-translation.d.ts +13 -0
- package/dist/chat/chat-pipeline-translation.js +109 -45
- package/dist/chat/chat-pipeline.d.ts +1 -1
- package/dist/chat/chat-pipeline.js +296 -53
- package/dist/chat/gemini-planner.d.ts +2 -0
- package/dist/chat/gemini-planner.js +2 -1
- package/dist/chat/planner-types.d.ts +15 -0
- package/dist/chat/planner-types.js +2 -2
- package/dist/chat/planner.d.ts +12 -0
- package/dist/chat/planner.js +16 -2
- package/dist/chat/translation-chunking.d.ts +124 -0
- package/dist/chat/translation-chunking.js +371 -0
- package/dist/checks/field-walk.d.ts +25 -0
- package/dist/checks/field-walk.js +152 -0
- package/dist/checks/index.d.ts +5 -0
- package/dist/checks/index.js +4 -0
- package/dist/checks/page-weight.d.ts +22 -0
- package/dist/checks/page-weight.js +200 -0
- package/dist/checks/rules-draft.d.ts +2 -0
- package/dist/checks/rules-draft.js +375 -0
- package/dist/checks/run-checks.d.ts +32 -0
- package/dist/checks/run-checks.js +152 -0
- package/dist/checks/session-runner.d.ts +19 -0
- package/dist/checks/session-runner.js +95 -0
- package/dist/checks/types.d.ts +65 -0
- package/dist/checks/types.js +1 -0
- package/dist/durable/durable-store-singleton.d.ts +37 -0
- package/dist/durable/durable-store-singleton.js +179 -0
- package/dist/durable/finding-impact.d.ts +30 -0
- package/dist/durable/finding-impact.js +53 -0
- package/dist/durable/in-memory-durable-store.d.ts +203 -0
- package/dist/durable/in-memory-durable-store.js +363 -0
- package/dist/durable/index.d.ts +5 -0
- package/dist/durable/index.js +4 -0
- package/dist/durable/pending-plan-store.d.ts +28 -0
- package/dist/durable/pending-plan-store.js +156 -0
- package/dist/durable/sqlite-durable-store.d.ts +71 -0
- package/dist/durable/sqlite-durable-store.js +631 -0
- package/dist/durable/types.d.ts +265 -0
- package/dist/durable/types.js +1 -0
- package/dist/handler/create-orchestrator.d.ts +4 -0
- package/dist/handler/create-orchestrator.js +67 -4
- package/dist/http/audio-actions.d.ts +1 -1
- package/dist/http/checks-actions.d.ts +39 -0
- package/dist/http/checks-actions.js +122 -0
- package/dist/http/history-actions.d.ts +1 -1
- package/dist/http/image-generate-actions.d.ts +2 -2
- package/dist/http/ops-actions.d.ts +2 -2
- package/dist/http/publish-actions.d.ts +4 -4
- package/dist/http/restore-actions.d.ts +3 -3
- package/dist/http/screenshot-actions.d.ts +2 -2
- package/dist/http/session-actions.d.ts +1 -1
- package/dist/http/telemetry-feedback-actions.d.ts +2 -2
- package/dist/http/unsplash-actions.d.ts +2 -2
- package/dist/http/variations-actions.d.ts +2 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -0
- package/dist/nlp/deterministic-planner-context.d.ts +16 -0
- package/dist/nlp/deterministic-planner-context.js +33 -7
- package/dist/nlp/plan-normalizer.js +54 -6
- package/dist/ops/destructive-action-gate.js +7 -2
- package/dist/ops/ops-engine.d.ts +12 -1
- package/dist/ops/ops-engine.js +41 -14
- package/dist/publish/publish-target-registry.js +1 -1
- package/dist/publish/publish-target.d.ts +1 -1
- package/dist/state/session-state.js +8 -1
- package/package.json +3 -3
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* wire bytes for callers that don't already have an SSE writer. Neither Fastify
|
|
18
18
|
* nor `Response` appears in this file.
|
|
19
19
|
*/
|
|
20
|
-
import { type VariationRequestBody, type VariationPipelineContext, type VariationResult, type VariationImageUpdate } from "../chat/variation-pipeline.
|
|
21
|
-
import type { ActionResult } from "./history-actions.
|
|
20
|
+
import { type VariationRequestBody, type VariationPipelineContext, type VariationResult, type VariationImageUpdate } from "../chat/variation-pipeline.ts";
|
|
21
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
22
22
|
export type { ActionResult };
|
|
23
23
|
export type { VariationRequestBody, VariationPipelineContext };
|
|
24
24
|
export type ParsedVariationRequest = {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,10 @@ export { createOrchestrator, type CreateOrchestratorConfig, type OrchestratorHan
|
|
|
2
2
|
export type { OrchestratorAuth, AuthContext } from "./handler/auth.ts";
|
|
3
3
|
export type { CmsAdapter, CmsCapabilities, CmsInlineAsset, CmsPublishContext, CmsPublishResult, CmsPerspective, CmsReadOptions, CmsMediaItem, CmsMediaPage, CmsMediaQuery, ResolvedCapabilities } from "./cms/adapter.ts";
|
|
4
4
|
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel, type JsonFileAdapterOptions, type EditorApiAdapterOptions, type CmsMediaSource, type CmsMediaSourceConfig } from "./cms/index.ts";
|
|
5
|
+
export { registerPublishTarget, selectPublishTarget, getPublishTarget, listPublishTargets } from "./publish/publish-target-registry.ts";
|
|
6
|
+
export type { PublishTarget, PublishContext, PublishOutcome, PublishStatus, PublishResult } from "./publish/publish-target.ts";
|
|
7
|
+
export { SqliteDurableStore, InMemoryDurableStore, getDurableStore, resetDurableStore, setDurableStore, durableStoreIsEphemeral, type SqliteDurableStoreOptions, type InMemoryDurableStoreOptions, type DurableStore, type FindingInput, type FindingRecord, type FindingQuery, type FindingSeverity, type FindingStatus, type FindingEvidence, type CheckRunInput, type CheckRunRecord, type CheckRunPatch, type CheckRunTrigger, type MemoryInput, type MemoryRecord, type MemoryQuery, type MemoryScope, type MemoryKind, type MemorySource, type MemoryStatus, type CorrectionInput, type CorrectionRecord, type CorrectionQuery, type CorrectionOutcome, type ProposalInput, type ProposalRecord, type ProposalQuery, type ProposalStatus } from "./durable/index.ts";
|
|
8
|
+
export { runDraftChecks, runChecksForSession, scheduleChecksAfterApply, scheduleChecksAfterPublish, cancelScheduledChecks, fingerprintFor, DRAFT_RULES, walkPageFields, fieldText, type RunChecksArgs, type CheckRule, type CheckContext, type RuleFinding, type FieldEntry, type SiteView } from "./checks/index.ts";
|
|
9
|
+
export { runChecksAction, listFindingsAction, listCheckRunsAction, updateFindingAction, type RunChecksParams, type ListFindingsParams, type UpdateFindingParams, type ChecksScope } from "./http/checks-actions.ts";
|
|
10
|
+
export { durableHealth, noteDurableFailure, isDiscardInFlight } from "./durable/durable-store-singleton.ts";
|
|
11
|
+
export { loadPendingPlan, savePendingPlan, clearPendingPlan, peekPendingPlan } from "./durable/pending-plan-store.ts";
|
package/dist/index.js
CHANGED
|
@@ -19,3 +19,30 @@
|
|
|
19
19
|
// The Fastify HTTP wrapper lives in apps/orchestrator and imports from here.
|
|
20
20
|
export { createOrchestrator } from "./handler/create-orchestrator.js";
|
|
21
21
|
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel } from "./cms/index.js";
|
|
22
|
+
// The publish-target plugin point. `docs-site/integration/publishing.mdx` has
|
|
23
|
+
// documented this as the way to publish somewhere we do not ship a target for
|
|
24
|
+
// since before the package had an export map, and told the reader to import it
|
|
25
|
+
// from `"./publish/publish-target-registry.js"` — a relative path into *this*
|
|
26
|
+
// repository, pasted into their file. It cannot resolve from anywhere, and
|
|
27
|
+
// there was no specifier that could: the symbol was not exported here, so a
|
|
28
|
+
// registry consumer had no reachable way to register a target at all.
|
|
29
|
+
//
|
|
30
|
+
// The interface was always meant to be public — `architecture.mdx` calls it the
|
|
31
|
+
// integration point for "S3, GitLab Pages, Netlify, a CMS API, a custom CI/CD
|
|
32
|
+
// pipeline". Only the export was missing.
|
|
33
|
+
export { registerPublishTarget, selectPublishTarget, getPublishTarget, listPublishTargets } from "./publish/publish-target-registry.js";
|
|
34
|
+
// The durable substrate for findings, memory, corrections and proposals. It is
|
|
35
|
+
// public because the implementation is meant to be replaceable: a library-mode
|
|
36
|
+
// host on a serverless platform has no persistent disk for a SQLite file, and
|
|
37
|
+
// every record here would otherwise die with the process. `DurableStore` is the
|
|
38
|
+
// seam a Neon- or Turso-backed store fills — which is only true if a consumer
|
|
39
|
+
// installing from the registry can reach the type.
|
|
40
|
+
export { SqliteDurableStore, InMemoryDurableStore, getDurableStore, resetDurableStore, setDurableStore, durableStoreIsEphemeral } from "./durable/index.js";
|
|
41
|
+
// The draft-tier checks: pure functions of a PageDoc and the block manifest,
|
|
42
|
+
// producing findings into the durable store. Public so an integrator can run
|
|
43
|
+
// them (or add a rule of their own) against a site we do not host.
|
|
44
|
+
export { runDraftChecks, runChecksForSession, scheduleChecksAfterApply, scheduleChecksAfterPublish, cancelScheduledChecks, fingerprintFor, DRAFT_RULES, walkPageFields, fieldText } from "./checks/index.js";
|
|
45
|
+
// The checks HTTP surface, mounted by both transports.
|
|
46
|
+
export { runChecksAction, listFindingsAction, listCheckRunsAction, updateFindingAction } from "./http/checks-actions.js";
|
|
47
|
+
export { durableHealth, noteDurableFailure, isDiscardInFlight } from "./durable/durable-store-singleton.js";
|
|
48
|
+
export { loadPendingPlan, savePendingPlan, clearPendingPlan, peekPendingPlan } from "./durable/pending-plan-store.js";
|
|
@@ -46,6 +46,22 @@ export declare function selectedBlockSnapshot(args: {
|
|
|
46
46
|
selectedEditablePath: string | null;
|
|
47
47
|
selectedEditableValue: {} | null;
|
|
48
48
|
} | null;
|
|
49
|
+
/**
|
|
50
|
+
* How an item in a list prop gets named to the planner, in precedence order.
|
|
51
|
+
*
|
|
52
|
+
* A list whose items match none of these reaches the planner as a bare count,
|
|
53
|
+
* and the planner then cannot answer "update the FAQ entry about freezing" —
|
|
54
|
+
* it knows there are three entries and nothing about any of them. It says so,
|
|
55
|
+
* correctly, and asks which one. Three of the twelve list fields in the block
|
|
56
|
+
* registry were in that state: `FAQAccordion.items` names its question `q`,
|
|
57
|
+
* `Testimonials.items` has `author`/`quote`, and `Gallery.images` has
|
|
58
|
+
* `caption`/`alt`. None of those were listed here.
|
|
59
|
+
*
|
|
60
|
+
* `arrayPropLabelKeys` is asserted against the block registry by a test, so a
|
|
61
|
+
* new block with a differently-named item field fails there rather than
|
|
62
|
+
* silently costing the planner a clarification round-trip.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ARRAY_PROP_LABEL_KEYS: readonly ["label", "title", "heading", "question", "q", "name", "author", "caption", "alt", "quote", "value", "text"];
|
|
49
65
|
export declare function arrayPropLengths(props: Record<string, unknown>): Record<string, {
|
|
50
66
|
length: number;
|
|
51
67
|
labels?: string[];
|
|
@@ -184,6 +184,35 @@ export function selectedBlockSnapshot(args) {
|
|
|
184
184
|
// ---------------------------------------------------------------------------
|
|
185
185
|
// Array prop metadata
|
|
186
186
|
// ---------------------------------------------------------------------------
|
|
187
|
+
/**
|
|
188
|
+
* How an item in a list prop gets named to the planner, in precedence order.
|
|
189
|
+
*
|
|
190
|
+
* A list whose items match none of these reaches the planner as a bare count,
|
|
191
|
+
* and the planner then cannot answer "update the FAQ entry about freezing" —
|
|
192
|
+
* it knows there are three entries and nothing about any of them. It says so,
|
|
193
|
+
* correctly, and asks which one. Three of the twelve list fields in the block
|
|
194
|
+
* registry were in that state: `FAQAccordion.items` names its question `q`,
|
|
195
|
+
* `Testimonials.items` has `author`/`quote`, and `Gallery.images` has
|
|
196
|
+
* `caption`/`alt`. None of those were listed here.
|
|
197
|
+
*
|
|
198
|
+
* `arrayPropLabelKeys` is asserted against the block registry by a test, so a
|
|
199
|
+
* new block with a differently-named item field fails there rather than
|
|
200
|
+
* silently costing the planner a clarification round-trip.
|
|
201
|
+
*/
|
|
202
|
+
export const ARRAY_PROP_LABEL_KEYS = [
|
|
203
|
+
"label",
|
|
204
|
+
"title",
|
|
205
|
+
"heading",
|
|
206
|
+
"question",
|
|
207
|
+
"q",
|
|
208
|
+
"name",
|
|
209
|
+
"author",
|
|
210
|
+
"caption",
|
|
211
|
+
"alt",
|
|
212
|
+
"quote",
|
|
213
|
+
"value",
|
|
214
|
+
"text"
|
|
215
|
+
];
|
|
187
216
|
export function arrayPropLengths(props) {
|
|
188
217
|
const out = {};
|
|
189
218
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -192,13 +221,10 @@ export function arrayPropLengths(props) {
|
|
|
192
221
|
const labels = [];
|
|
193
222
|
for (const item of value) {
|
|
194
223
|
if (typeof item === "object" && item !== null) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
item.name;
|
|
200
|
-
if (typeof labelValue === "string")
|
|
201
|
-
labels.push(labelValue);
|
|
224
|
+
const record = item;
|
|
225
|
+
const labelKey = ARRAY_PROP_LABEL_KEYS.find((candidate) => typeof record[candidate] === "string");
|
|
226
|
+
if (labelKey)
|
|
227
|
+
labels.push(record[labelKey]);
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
out[key] = labels.length > 0 ? { length: value.length, labels } : { length: value.length };
|
|
@@ -711,6 +711,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
711
711
|
return args.currentPage.blocks[idx - 1]?.id;
|
|
712
712
|
};
|
|
713
713
|
const usedBlockIds = new Set((args?.currentPage?.blocks ?? []).map((b) => b.id));
|
|
714
|
+
/**
|
|
715
|
+
* Block ids this pass renamed, old → new.
|
|
716
|
+
*
|
|
717
|
+
* Renaming an `add_block` id is only safe if the rest of the plan follows it.
|
|
718
|
+
* A plan that adds `b_hero_x` and then updates `b_hero_x` means the block it
|
|
719
|
+
* just added; leave the reference pointing at the colliding name and the
|
|
720
|
+
* update lands on the *existing* block instead — content written over the
|
|
721
|
+
* wrong block, which is the failure mode this repo keeps re-shipping. Ops are
|
|
722
|
+
* normalized in order, so only ops after the rename are rewritten: a
|
|
723
|
+
* reference that came *before* the add_block can only have meant the block
|
|
724
|
+
* that was already there.
|
|
725
|
+
*/
|
|
726
|
+
const renamedBlockIds = new Map();
|
|
714
727
|
let createdPageSlug;
|
|
715
728
|
let droppedPageLevelUpdate = false;
|
|
716
729
|
// itemIds already claimed by a remove_item freeze in THIS plan — used to spot
|
|
@@ -972,6 +985,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
972
985
|
raw.afterBlockId =
|
|
973
986
|
raw.after_block_id ?? raw.after ?? raw.insertAfterId ?? beforeToAfter(raw.beforeId ?? raw.insertBeforeId);
|
|
974
987
|
}
|
|
988
|
+
// Follow any add_block id an earlier op in this plan had to uniquify. Runs
|
|
989
|
+
// after the alias resolution above so `after_block_id` and friends are
|
|
990
|
+
// already folded into the canonical keys. See `renamedBlockIds`.
|
|
991
|
+
if (renamedBlockIds.size > 0) {
|
|
992
|
+
for (const key of ["blockId", "afterBlockId", "newBlockId"]) {
|
|
993
|
+
const value = raw[key];
|
|
994
|
+
if (typeof value === "string") {
|
|
995
|
+
const renamed = renamedBlockIds.get(value);
|
|
996
|
+
if (renamed)
|
|
997
|
+
raw[key] = renamed;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
975
1001
|
if (!raw.afterPageSlug) {
|
|
976
1002
|
raw.afterPageSlug =
|
|
977
1003
|
raw.afterPageSlug ??
|
|
@@ -1246,15 +1272,37 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1246
1272
|
block.props = {};
|
|
1247
1273
|
}
|
|
1248
1274
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1275
|
+
/*
|
|
1276
|
+
* The model authors this id. Nothing in the `add_block` prompt says how
|
|
1277
|
+
* to build one, so it free-associates from the page and the block type —
|
|
1278
|
+
* `b_featuregrid_wellness` for a FeatureGrid on /avocado-wellness. That is
|
|
1279
|
+
* a near-deterministic function of the prompt, so the same request asked
|
|
1280
|
+
* twice produces the same id twice, and the second one dies in the ops
|
|
1281
|
+
* engine on "Block id … already exists", taking the whole plan down with
|
|
1282
|
+
* it: a committing apply is all-or-nothing.
|
|
1283
|
+
*
|
|
1284
|
+
* `usedBlockIds` is seeded from the page's existing blocks and knew this
|
|
1285
|
+
* all along. It was consulted only when the model *omitted* an id, which
|
|
1286
|
+
* is the one case that cannot collide; a supplied id went through
|
|
1287
|
+
* untouched and was never even registered, so two ops in one plan could
|
|
1288
|
+
* pick the same name. Uniquify instead — a fourth FeatureGrid is a
|
|
1289
|
+
* legitimate reading of "populate this page", and only the label was
|
|
1290
|
+
* wrong. The engine's own check stays as the backstop and must never be
|
|
1291
|
+
* relaxed into an overwrite.
|
|
1292
|
+
*/
|
|
1293
|
+
const suppliedId = typeof block.id === "string" && block.id.length > 0 ? block.id : null;
|
|
1294
|
+
const idStem = suppliedId ?? (typeof block.type === "string" ? `b_${String(block.type).toLowerCase()}_${Date.now()}` : null);
|
|
1295
|
+
if (idStem) {
|
|
1296
|
+
let candidate = idStem;
|
|
1251
1297
|
let sfx = 0;
|
|
1252
|
-
while (usedBlockIds.has(
|
|
1298
|
+
while (usedBlockIds.has(candidate)) {
|
|
1253
1299
|
sfx++;
|
|
1254
|
-
|
|
1300
|
+
candidate = `${idStem}_${sfx}`;
|
|
1255
1301
|
}
|
|
1256
|
-
usedBlockIds.add(
|
|
1257
|
-
|
|
1302
|
+
usedBlockIds.add(candidate);
|
|
1303
|
+
if (suppliedId && candidate !== suppliedId)
|
|
1304
|
+
renamedBlockIds.set(suppliedId, candidate);
|
|
1305
|
+
block.id = candidate;
|
|
1258
1306
|
}
|
|
1259
1307
|
raw.block = block;
|
|
1260
1308
|
// Remap heading→title for non-Hero blocks (LLMs often confuse heading/title)
|
|
@@ -24,12 +24,17 @@ function slugsTouchedByOps(ops) {
|
|
|
24
24
|
set.add(op.toPageSlug);
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
|
+
// rename_page: one page, under two names. Counting both slugs made every
|
|
28
|
+
// rename look like a cross-page plan and held it for approval — the same
|
|
29
|
+
// miscount duplicate_page and duplicate_block are special-cased for above.
|
|
30
|
+
if (op.op === "rename_page") {
|
|
31
|
+
set.add(typeof op.newPageSlug === "string" ? op.newPageSlug : op.pageSlug);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
27
34
|
if ("pageSlug" in op && typeof op.pageSlug === "string")
|
|
28
35
|
set.add(op.pageSlug);
|
|
29
36
|
if (op.op === "duplicate_block" && typeof op.toPageSlug === "string")
|
|
30
37
|
set.add(op.toPageSlug);
|
|
31
|
-
if (op.op === "rename_page" && typeof op.newPageSlug === "string")
|
|
32
|
-
set.add(op.newPageSlug);
|
|
33
38
|
}
|
|
34
39
|
return Array.from(set);
|
|
35
40
|
}
|
package/dist/ops/ops-engine.d.ts
CHANGED
|
@@ -22,7 +22,18 @@ export declare function isNoEffectiveChangeError(reason: string): boolean;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function isAlreadyCurrentError(reason: string): boolean;
|
|
24
24
|
export declare function classifyGuardrailError(reason: string): GuardrailErrorCategory;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* `category` is the one the failure was actually thrown with, for callers that
|
|
27
|
+
* still hold it. Without it this falls back to reading the category back out of
|
|
28
|
+
* the message, which is a guess — see `isRepairEligibleCategory`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatValidationError(reason: string, category?: GuardrailErrorCategory): string;
|
|
31
|
+
/**
|
|
32
|
+
* The deterministic repair pass exists for plans that are structurally wrong
|
|
33
|
+
* but fixable — a bad prop name, an id that clashes. Anything else is either
|
|
34
|
+
* hopeless or not the planner's fault, and re-prompting it wastes a model call.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isRepairEligibleCategory(category: GuardrailErrorCategory): category is "schema_violation";
|
|
26
37
|
export declare function isDeterministicRepairEligible(reason: string): boolean;
|
|
27
38
|
/**
|
|
28
39
|
* Extracts structured fields from a planner schema_violation reason so the
|
package/dist/ops/ops-engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { blockSchemas, operationSchema, validateBlockProps, findManifestSchemaIssue, isChrome, isInBlockCatalogue, catalogueBlockTypes, mapSemanticThemeTokens, generateItemId, isRichTextDoc, fromMarkdown, mergeRichTextDoc } from "@avocadostudio-ai/shared";
|
|
2
|
+
import { blockSchemas, operationSchema, validateBlockProps, findManifestSchemaIssue, isChrome, getMediaFields, isInBlockCatalogue, catalogueBlockTypes, mapSemanticThemeTokens, generateItemId, isRichTextDoc, fromMarkdown, mergeRichTextDoc } from "@avocadostudio-ai/shared";
|
|
3
3
|
import { normalizeRouteCandidate } from "../nlp/intent-helpers.js";
|
|
4
4
|
import { pageIdFromSlug, pageTitleFromSlug } from "../nlp/plan-normalizer.js";
|
|
5
5
|
import { OperationError, toErrorDetail as _unifiedToErrorDetail } from "../errors.js";
|
|
@@ -88,7 +88,17 @@ function remapRouteReference(value, fromSlug, toSlug) {
|
|
|
88
88
|
}
|
|
89
89
|
return value;
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Rewrite every route reference under `input` from `fromSlug` to `toSlug`.
|
|
93
|
+
*
|
|
94
|
+
* The walk is permissive by design — it remaps any string that starts with the
|
|
95
|
+
* old slug, whatever prop holds it — because a custom block ships no field
|
|
96
|
+
* metadata and its links still have to survive a rename. `mediaKeys` is the one
|
|
97
|
+
* exception: props that address a *resource* rather than a page. Renaming
|
|
98
|
+
* `/pricing` to `/plans` does not move `/pricing/demo.mp4`, and before this the
|
|
99
|
+
* walker rewrote that path too, quietly breaking the asset.
|
|
100
|
+
*/
|
|
101
|
+
function rewriteRouteLinksInValue(input, fromSlug, toSlug, mediaKeys) {
|
|
92
102
|
if (typeof input === "string") {
|
|
93
103
|
const mapped = remapRouteReference(input, fromSlug, toSlug);
|
|
94
104
|
return { value: mapped, changed: mapped !== input };
|
|
@@ -96,7 +106,7 @@ function rewriteRouteLinksInValue(input, fromSlug, toSlug) {
|
|
|
96
106
|
if (Array.isArray(input)) {
|
|
97
107
|
let changed = false;
|
|
98
108
|
const next = input.map((item) => {
|
|
99
|
-
const mapped = rewriteRouteLinksInValue(item, fromSlug, toSlug);
|
|
109
|
+
const mapped = rewriteRouteLinksInValue(item, fromSlug, toSlug, mediaKeys);
|
|
100
110
|
if (mapped.changed)
|
|
101
111
|
changed = true;
|
|
102
112
|
return mapped.value;
|
|
@@ -109,11 +119,8 @@ function rewriteRouteLinksInValue(input, fromSlug, toSlug) {
|
|
|
109
119
|
let changed = false;
|
|
110
120
|
const next = {};
|
|
111
121
|
for (const [key, value] of Object.entries(source)) {
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
if (mapped !== value)
|
|
115
|
-
changed = true;
|
|
116
|
-
next[key] = mapped;
|
|
122
|
+
if (mediaKeys.has(key)) {
|
|
123
|
+
next[key] = value;
|
|
117
124
|
continue;
|
|
118
125
|
}
|
|
119
126
|
if (typeof value === "string" && key === "body") {
|
|
@@ -128,7 +135,7 @@ function rewriteRouteLinksInValue(input, fromSlug, toSlug) {
|
|
|
128
135
|
next[key] = rewritten;
|
|
129
136
|
continue;
|
|
130
137
|
}
|
|
131
|
-
const mapped = rewriteRouteLinksInValue(value, fromSlug, toSlug);
|
|
138
|
+
const mapped = rewriteRouteLinksInValue(value, fromSlug, toSlug, mediaKeys);
|
|
132
139
|
if (mapped.changed)
|
|
133
140
|
changed = true;
|
|
134
141
|
next[key] = mapped.value;
|
|
@@ -138,7 +145,7 @@ function rewriteRouteLinksInValue(input, fromSlug, toSlug) {
|
|
|
138
145
|
function rewriteLinksToRenamedPage(page, fromSlug, toSlug) {
|
|
139
146
|
let changed = false;
|
|
140
147
|
const nextBlocks = page.blocks.map((block) => {
|
|
141
|
-
const mapped = rewriteRouteLinksInValue(block.props, fromSlug, toSlug);
|
|
148
|
+
const mapped = rewriteRouteLinksInValue(block.props, fromSlug, toSlug, getMediaFields(block.type));
|
|
142
149
|
if (!mapped.changed)
|
|
143
150
|
return block;
|
|
144
151
|
changed = true;
|
|
@@ -208,16 +215,36 @@ export function classifyGuardrailError(reason) {
|
|
|
208
215
|
lower.includes("required") ||
|
|
209
216
|
lower.includes("unknown props") ||
|
|
210
217
|
lower.includes("out of range") ||
|
|
211
|
-
lower.includes("must be")
|
|
218
|
+
lower.includes("must be") ||
|
|
219
|
+
// Every "already exists" this engine raises — a duplicate block id, a page
|
|
220
|
+
// slug that is taken — it raises *as* a schema_violation. This list did not
|
|
221
|
+
// know the word, so a plan one renamed id away from applying classified as
|
|
222
|
+
// `internal_error`, which the repair pass refuses to touch. Callers holding
|
|
223
|
+
// the thrown error should prefer its `category` over this function; the
|
|
224
|
+
// keyword is here for the paths that only ever had the string.
|
|
225
|
+
lower.includes("already exists")) {
|
|
212
226
|
return "schema_violation";
|
|
213
227
|
}
|
|
214
228
|
return "internal_error";
|
|
215
229
|
}
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
/**
|
|
231
|
+
* `category` is the one the failure was actually thrown with, for callers that
|
|
232
|
+
* still hold it. Without it this falls back to reading the category back out of
|
|
233
|
+
* the message, which is a guess — see `isRepairEligibleCategory`.
|
|
234
|
+
*/
|
|
235
|
+
export function formatValidationError(reason, category) {
|
|
236
|
+
return `${category ?? classifyGuardrailError(reason)}: ${reason}`;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The deterministic repair pass exists for plans that are structurally wrong
|
|
240
|
+
* but fixable — a bad prop name, an id that clashes. Anything else is either
|
|
241
|
+
* hopeless or not the planner's fault, and re-prompting it wastes a model call.
|
|
242
|
+
*/
|
|
243
|
+
export function isRepairEligibleCategory(category) {
|
|
244
|
+
return category === "schema_violation";
|
|
218
245
|
}
|
|
219
246
|
export function isDeterministicRepairEligible(reason) {
|
|
220
|
-
return classifyGuardrailError(reason)
|
|
247
|
+
return isRepairEligibleCategory(classifyGuardrailError(reason));
|
|
221
248
|
}
|
|
222
249
|
/**
|
|
223
250
|
* Extracts structured fields from a planner schema_violation reason so the
|
|
@@ -10,7 +10,7 @@ import { DeployHookPublishTarget } from "./targets/deploy-hook.js";
|
|
|
10
10
|
* handling requests:
|
|
11
11
|
*
|
|
12
12
|
* ```ts
|
|
13
|
-
* import { registerPublishTarget } from "
|
|
13
|
+
* import { registerPublishTarget } from "@avocadostudio-ai/orchestrator-core"
|
|
14
14
|
* registerPublishTarget(new S3PublishTarget())
|
|
15
15
|
* ```
|
|
16
16
|
*
|
|
@@ -6,7 +6,7 @@ import type { PublishTracker } from "../state/session-state.ts";
|
|
|
6
6
|
* up writing content. Built-ins cover git + Vercel, a site-contract POST, and
|
|
7
7
|
* a raw Vercel deploy hook. Third parties (S3, GitLab Pages, custom CI, etc.)
|
|
8
8
|
* implement {@link PublishTarget} and register via
|
|
9
|
-
* {@link registerPublishTarget} from
|
|
9
|
+
* {@link registerPublishTarget} from `@avocadostudio-ai/orchestrator-core`.
|
|
10
10
|
*
|
|
11
11
|
* The route handler's job shrinks to: build {@link PublishContext}, call
|
|
12
12
|
* {@link selectPublishTarget}, invoke `target.publish(ctx)`, save the returned
|
|
@@ -3,6 +3,7 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { demoPublishedPages, demoSiteConfig, ensureItemIds, IMAGE_PLACEHOLDER } 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
|
+
import { discardPendingProposalsForSession, sweepExpiredProposals } from "../durable/durable-store-singleton.js";
|
|
6
7
|
// Re-exported so callers that already depend on this module for state can ask
|
|
7
8
|
// whether that state is actually being kept, without a second import path.
|
|
8
9
|
export { persistenceHealth, persistenceWarning } from "./sqlite-store-singleton.js";
|
|
@@ -173,6 +174,9 @@ export function forgetSession(sessionKey) {
|
|
|
173
174
|
pendingClarificationBySession.delete(sessionKey);
|
|
174
175
|
chatHistoryBySession.delete(sessionKey);
|
|
175
176
|
pendingApprovalPlanBySession.delete(sessionKey);
|
|
177
|
+
// Session ids are caller-supplied and routinely reused, so a pending proposal
|
|
178
|
+
// left behind here would rehydrate into the *next* session with this id.
|
|
179
|
+
discardPendingProposalsForSession(sessionKey);
|
|
176
180
|
imageSourcePreferenceBySession.delete(sessionKey);
|
|
177
181
|
publishStatusBySession.delete(sessionKey);
|
|
178
182
|
capabilitiesBySession.delete(sessionKey);
|
|
@@ -1091,12 +1095,15 @@ const EPHEMERAL_MAP_CAP = 500; // max entries for maps without timestamps
|
|
|
1091
1095
|
*/
|
|
1092
1096
|
export function evictStaleEphemeralMaps() {
|
|
1093
1097
|
const now = Date.now();
|
|
1094
|
-
// pendingApprovalPlanBySession: has createdAt — evict by age
|
|
1098
|
+
// pendingApprovalPlanBySession: has createdAt — evict by age. The durable
|
|
1099
|
+
// mirror carries the same TTL as `expiresAt`, so both halves age out together
|
|
1100
|
+
// rather than the Map forgetting a plan the store would hand straight back.
|
|
1095
1101
|
for (const [key, plan] of pendingApprovalPlanBySession) {
|
|
1096
1102
|
if (now - new Date(plan.createdAt).getTime() > APPROVAL_PLAN_TTL_MS) {
|
|
1097
1103
|
pendingApprovalPlanBySession.delete(key);
|
|
1098
1104
|
}
|
|
1099
1105
|
}
|
|
1106
|
+
sweepExpiredProposals();
|
|
1100
1107
|
// publishStatusBySession: has updatedAt — evict by age
|
|
1101
1108
|
for (const [key, tracker] of publishStatusBySession) {
|
|
1102
1109
|
if (now - new Date(tracker.updatedAt).getTime() > PUBLISH_STATUS_TTL_MS) {
|
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.3",
|
|
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/
|
|
27
|
-
"@avocadostudio-ai/
|
|
26
|
+
"@avocadostudio-ai/shared": "^0.3.3",
|
|
27
|
+
"@avocadostudio-ai/migration-sdk": "^0.3.3"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@google/genai": "^1.46.0",
|