@avocadostudio-ai/orchestrator-core 0.3.1 → 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 +297 -53
- package/dist/chat/gemini-planner.d.ts +2 -0
- package/dist/chat/gemini-planner.js +2 -1
- package/dist/chat/hallucination-validator.d.ts +6 -0
- package/dist/chat/hallucination-validator.js +49 -8
- 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/cms/adapter.d.ts +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 +85 -9
- 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/deterministic-planner-suggestions.js +1 -1
- package/dist/nlp/plan-normalizer.js +193 -56
- 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
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* page. So the path is declarable: per request, per registered site, or via
|
|
25
25
|
* `createOrchestrator({ draftPath })`.
|
|
26
26
|
*/
|
|
27
|
-
import type { Logger } from "../logger.
|
|
28
|
-
import type { ActionResult } from "./history-actions.
|
|
27
|
+
import type { Logger } from "../logger.ts";
|
|
28
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
29
29
|
export type { ActionResult };
|
|
30
30
|
export type ScreenshotParams = {
|
|
31
31
|
session?: string;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* embedding its own editor, and it would answer a question no embedded caller
|
|
18
18
|
* should be asking.
|
|
19
19
|
*/
|
|
20
|
-
import type { ActionResult } from "./history-actions.
|
|
20
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
21
21
|
export type { ActionResult };
|
|
22
22
|
/**
|
|
23
23
|
* The caller's bound session and a summary of its state.
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* status code and body to send; neither Fastify nor `Response` appears in this
|
|
16
16
|
* file.
|
|
17
17
|
*/
|
|
18
|
-
import type { FeedbackStore } from "../telemetry/feedback-store.
|
|
19
|
-
import type { ActionResult } from "./history-actions.
|
|
18
|
+
import type { FeedbackStore } from "../telemetry/feedback-store.ts";
|
|
19
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
20
20
|
export type { ActionResult };
|
|
21
21
|
/**
|
|
22
22
|
* The store is a parameter, not a module singleton.
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* orchestrator runtime is ready, and why this file needs no collaborators
|
|
16
16
|
* beyond an access key and a `fetch`.
|
|
17
17
|
*/
|
|
18
|
-
import type { Logger } from "../logger.
|
|
19
|
-
import type { ActionResult } from "./history-actions.
|
|
18
|
+
import type { Logger } from "../logger.ts";
|
|
19
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
20
20
|
export type { ActionResult };
|
|
21
21
|
/**
|
|
22
22
|
* Raw query values, straight off the wire.
|
|
@@ -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 };
|
|
@@ -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) {
|
|
@@ -582,6 +711,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
582
711
|
return args.currentPage.blocks[idx - 1]?.id;
|
|
583
712
|
};
|
|
584
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();
|
|
585
727
|
let createdPageSlug;
|
|
586
728
|
let droppedPageLevelUpdate = false;
|
|
587
729
|
// itemIds already claimed by a remove_item freeze in THIS plan — used to spot
|
|
@@ -843,6 +985,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
843
985
|
raw.afterBlockId =
|
|
844
986
|
raw.after_block_id ?? raw.after ?? raw.insertAfterId ?? beforeToAfter(raw.beforeId ?? raw.insertBeforeId);
|
|
845
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
|
+
}
|
|
846
1001
|
if (!raw.afterPageSlug) {
|
|
847
1002
|
raw.afterPageSlug =
|
|
848
1003
|
raw.afterPageSlug ??
|
|
@@ -886,38 +1041,17 @@ export function normalizePlanCandidate(input, args) {
|
|
|
886
1041
|
const patch = raw.patch;
|
|
887
1042
|
const targetBlock = args?.currentPage?.blocks.find((b) => b.id === raw.blockId);
|
|
888
1043
|
const blockType = targetBlock?.type ?? "";
|
|
889
|
-
if (
|
|
1044
|
+
if ("heading" in patch && !("title" in patch) && shouldAliasProp(blockType, "heading", "title")) {
|
|
890
1045
|
patch.title = patch.heading;
|
|
891
1046
|
delete patch.heading;
|
|
892
1047
|
}
|
|
893
|
-
const itemKeyAliases = { question: "q", answer: "a", testimonial: "quote", review: "quote" };
|
|
894
1048
|
for (const [propKey, propVal] of Object.entries(patch)) {
|
|
895
1049
|
if (!Array.isArray(propVal))
|
|
896
1050
|
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
|
-
});
|
|
1051
|
+
patch[propKey] = aliasListItems(blockType, propKey, propVal);
|
|
915
1052
|
}
|
|
916
1053
|
// 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
|
-
}
|
|
1054
|
+
coerceBooleanEnumProps(blockType, patch);
|
|
921
1055
|
if (blockType === "Carousel" && Array.isArray(patch.slides) && !patch.items) {
|
|
922
1056
|
patch.items = patch.slides;
|
|
923
1057
|
delete patch.slides;
|
|
@@ -1138,51 +1272,57 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1138
1272
|
block.props = {};
|
|
1139
1273
|
}
|
|
1140
1274
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
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;
|
|
1143
1297
|
let sfx = 0;
|
|
1144
|
-
while (usedBlockIds.has(
|
|
1298
|
+
while (usedBlockIds.has(candidate)) {
|
|
1145
1299
|
sfx++;
|
|
1146
|
-
|
|
1300
|
+
candidate = `${idStem}_${sfx}`;
|
|
1147
1301
|
}
|
|
1148
|
-
usedBlockIds.add(
|
|
1149
|
-
|
|
1302
|
+
usedBlockIds.add(candidate);
|
|
1303
|
+
if (suppliedId && candidate !== suppliedId)
|
|
1304
|
+
renamedBlockIds.set(suppliedId, candidate);
|
|
1305
|
+
block.id = candidate;
|
|
1150
1306
|
}
|
|
1151
1307
|
raw.block = block;
|
|
1152
1308
|
// Remap heading→title for non-Hero blocks (LLMs often confuse heading/title)
|
|
1153
1309
|
if (block.props && typeof block.props === "object" && !Array.isArray(block.props)) {
|
|
1154
1310
|
const bProps = block.props;
|
|
1155
1311
|
const blockType = typeof block.type === "string" ? block.type : "";
|
|
1156
|
-
if (
|
|
1312
|
+
if ("heading" in bProps && !("title" in bProps) && shouldAliasProp(blockType, "heading", "title")) {
|
|
1157
1313
|
bProps.title = bProps.heading;
|
|
1158
1314
|
delete bProps.heading;
|
|
1159
1315
|
}
|
|
1160
1316
|
}
|
|
1161
|
-
// Remap list item keys inside add_block props (e.g., question→q, answer→a)
|
|
1317
|
+
// Remap list item keys inside add_block props (e.g., question→q, answer→a),
|
|
1318
|
+
// asking the block's own list metadata rather than rewriting every array.
|
|
1162
1319
|
if (block.props && typeof block.props === "object" && !Array.isArray(block.props)) {
|
|
1163
1320
|
const bProps = block.props;
|
|
1164
|
-
const
|
|
1321
|
+
const blockType = typeof block.type === "string" ? block.type : "";
|
|
1165
1322
|
for (const [propKey, propVal] of Object.entries(bProps)) {
|
|
1166
1323
|
if (!Array.isArray(propVal))
|
|
1167
1324
|
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
|
-
});
|
|
1325
|
+
bProps[propKey] = aliasListItems(blockType, propKey, propVal);
|
|
1186
1326
|
}
|
|
1187
1327
|
}
|
|
1188
1328
|
// Block-specific prop key remapping and type coercion
|
|
@@ -1190,10 +1330,7 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1190
1330
|
const cProps = block.props;
|
|
1191
1331
|
const bt = block.type;
|
|
1192
1332
|
// 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
|
-
}
|
|
1333
|
+
coerceBooleanEnumProps(typeof bt === "string" ? bt : "", cProps);
|
|
1197
1334
|
// Carousel: slides→items
|
|
1198
1335
|
if (bt === "Carousel" && Array.isArray(cProps.slides) && !cProps.items) {
|
|
1199
1336
|
cProps.items = cProps.slides;
|
|
@@ -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
|