@avocadostudio-ai/orchestrator-core 0.3.2 → 0.4.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.
- 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 +312 -54
- 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/prompts.d.ts +5 -0
- package/dist/chat/prompts.js +92 -9
- package/dist/chat/translation-chunking.d.ts +124 -0
- package/dist/chat/translation-chunking.js +371 -0
- package/dist/checks/field-walk.d.ts +42 -0
- package/dist/checks/field-walk.js +198 -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 +439 -0
- package/dist/checks/run-checks.d.ts +42 -0
- package/dist/checks/run-checks.js +159 -0
- package/dist/checks/session-runner.d.ts +19 -0
- package/dist/checks/session-runner.js +99 -0
- package/dist/checks/types.d.ts +109 -0
- package/dist/checks/types.js +1 -0
- package/dist/cms/adapter.d.ts +74 -1
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/index.d.ts +1 -1
- package/dist/cms/index.js +1 -1
- package/dist/cms/media-sources.d.ts +29 -1
- package/dist/cms/media-sources.js +188 -7
- 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 +283 -32
- 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 +44 -1
- package/dist/http/history-actions.js +122 -0
- 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 +15 -4
- package/dist/http/publish-actions.js +3 -3
- 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 +9 -2
- package/dist/index.js +28 -1
- package/dist/nlp/deterministic-planner-context.d.ts +16 -0
- package/dist/nlp/deterministic-planner-context.js +33 -7
- package/dist/nlp/intent-detection.d.ts +16 -0
- package/dist/nlp/intent-detection.js +15 -1
- package/dist/nlp/plan-normalizer.js +66 -32
- 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-helpers.d.ts +12 -2
- package/dist/publish/publish-helpers.js +10 -3
- package/dist/publish/publish-selection.d.ts +84 -0
- package/dist/publish/publish-selection.js +113 -0
- package/dist/publish/publish-target-registry.js +1 -1
- package/dist/publish/publish-target.d.ts +1 -1
- package/dist/publish/targets/git.js +2 -2
- package/dist/state/session-state.js +8 -1
- package/dist/state/site-assets.d.ts +41 -0
- package/dist/state/site-assets.js +40 -0
- package/package.json +3 -3
|
@@ -167,3 +167,125 @@ export function historyRestoreAction(body, log) {
|
|
|
167
167
|
}
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Throw away selected changes by rolling each affected page back to the state
|
|
172
|
+
* it held immediately before the earliest change selected on it.
|
|
173
|
+
*
|
|
174
|
+
* The version log is a per-page timeline, not a stack of independent patches:
|
|
175
|
+
* an entry records the page *after* the change, so "undo this one change and
|
|
176
|
+
* keep the later ones" is not a question the stored data can answer. What it
|
|
177
|
+
* can answer is "put this page back the way it was before change N", which is
|
|
178
|
+
* what discarding means here — and it is why the response names every later
|
|
179
|
+
* entry that goes with it. The caller shows that list before asking the user
|
|
180
|
+
* to confirm; this function reports it again for whatever actually happened.
|
|
181
|
+
*
|
|
182
|
+
* Selecting several entries on one page collapses to its earliest. Selecting
|
|
183
|
+
* entries across pages rolls each page back independently — untouched pages
|
|
184
|
+
* keep their edits.
|
|
185
|
+
*
|
|
186
|
+
* Like a restore, a discard is itself an edit: the current page goes onto the
|
|
187
|
+
* undo stack first, so a discard can be undone.
|
|
188
|
+
*/
|
|
189
|
+
export function historyDiscardAction(body, log) {
|
|
190
|
+
if (!body.session)
|
|
191
|
+
return badRequest("session is required");
|
|
192
|
+
const requested = Array.isArray(body.versions)
|
|
193
|
+
? Array.from(new Set(body.versions.filter((v) => typeof v === "number" && Number.isFinite(v))))
|
|
194
|
+
: [];
|
|
195
|
+
if (requested.length === 0)
|
|
196
|
+
return badRequest("versions must be a non-empty array of version numbers");
|
|
197
|
+
const session = scopedSessionKey(body.session, body.siteId);
|
|
198
|
+
const entries = versionLog.get(session) ?? [];
|
|
199
|
+
const byVersion = new Map(entries.map((entry) => [entry.version, entry]));
|
|
200
|
+
const skipped = [];
|
|
201
|
+
/*
|
|
202
|
+
* Collapse to one rollback per page. A selection of three entries on the
|
|
203
|
+
* same page is one rollback to before the earliest of them; running three
|
|
204
|
+
* would have the second and third restore snapshots the first already
|
|
205
|
+
* invalidated.
|
|
206
|
+
*/
|
|
207
|
+
const earliestBySlug = new Map();
|
|
208
|
+
for (const version of requested) {
|
|
209
|
+
const entry = byVersion.get(version);
|
|
210
|
+
if (!entry) {
|
|
211
|
+
skipped.push({ version, reason: "not in this session's version log" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const current = earliestBySlug.get(entry.slug);
|
|
215
|
+
if (current === undefined || version < current)
|
|
216
|
+
earliestBySlug.set(entry.slug, version);
|
|
217
|
+
}
|
|
218
|
+
const discarded = [];
|
|
219
|
+
let previewVersion = versions.get(session) ?? 0;
|
|
220
|
+
let navigateToSlug;
|
|
221
|
+
for (const [slug, fromVersion] of earliestBySlug) {
|
|
222
|
+
/*
|
|
223
|
+
* The state before `fromVersion` is the snapshot of the newest earlier
|
|
224
|
+
* entry on the same page. An entry with no snapshot is a legacy row that
|
|
225
|
+
* cannot restore anything, so it does not count as the prior state.
|
|
226
|
+
*/
|
|
227
|
+
const prior = entries
|
|
228
|
+
.filter((entry) => entry.slug === slug && entry.version < fromVersion && entry.snapshot !== undefined)
|
|
229
|
+
.at(-1);
|
|
230
|
+
if (!prior) {
|
|
231
|
+
/*
|
|
232
|
+
* Nothing recorded before this change — the log was capped, or this is
|
|
233
|
+
* the page's first entry. Guessing would mean inventing a state the
|
|
234
|
+
* page never had, so the entry stays and the caller is told why.
|
|
235
|
+
*/
|
|
236
|
+
skipped.push({
|
|
237
|
+
version: fromVersion,
|
|
238
|
+
reason: "no recorded page state before this change"
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const alsoDiscarded = entries
|
|
243
|
+
.filter((entry) => entry.slug === slug && entry.version >= fromVersion)
|
|
244
|
+
.map((entry) => entry.version)
|
|
245
|
+
.filter((version) => version !== fromVersion);
|
|
246
|
+
// Filtered above, so this only maps the impossible `undefined` onto the
|
|
247
|
+
// `null` that means "the page did not exist at this version".
|
|
248
|
+
const snapshot = prior.snapshot ?? null;
|
|
249
|
+
// pushUndo also clears redo, which is right: a discard starts a new branch.
|
|
250
|
+
pushUndo(session, slug, getPage(session, slug));
|
|
251
|
+
if (snapshot === null)
|
|
252
|
+
removePage(session, slug);
|
|
253
|
+
else
|
|
254
|
+
setPage(session, structuredClone(snapshot));
|
|
255
|
+
previewVersion = bumpVersion(session);
|
|
256
|
+
const count = alsoDiscarded.length + 1;
|
|
257
|
+
pushVersionEntry(session, {
|
|
258
|
+
version: previewVersion,
|
|
259
|
+
slug,
|
|
260
|
+
summary: `Discarded ${count} change${count === 1 ? "" : "s"} — back to v${prior.version}`,
|
|
261
|
+
opTypes: [],
|
|
262
|
+
opCount: 0,
|
|
263
|
+
source: "restore",
|
|
264
|
+
snapshot: snapshot === null ? null : structuredClone(snapshot)
|
|
265
|
+
});
|
|
266
|
+
discarded.push({ slug, fromVersion, toVersion: snapshot === null ? null : prior.version, alsoDiscarded });
|
|
267
|
+
navigateToSlug = slug;
|
|
268
|
+
}
|
|
269
|
+
if (discarded.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
code: 400,
|
|
272
|
+
body: { error: "nothing could be discarded", skipped }
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
schedulePersistState(log);
|
|
276
|
+
const touched = discarded[discarded.length - 1].slug;
|
|
277
|
+
const undoList = getHistoryMap(historyUndo, session).get(touched) ?? [];
|
|
278
|
+
const redoList = getHistoryMap(historyRedo, session).get(touched) ?? [];
|
|
279
|
+
return {
|
|
280
|
+
code: 200,
|
|
281
|
+
body: {
|
|
282
|
+
status: "applied",
|
|
283
|
+
previewVersion,
|
|
284
|
+
discarded,
|
|
285
|
+
skipped,
|
|
286
|
+
...(navigateToSlug ? { navigateToSlug } : {}),
|
|
287
|
+
canUndo: undoList.length > 0,
|
|
288
|
+
canRedo: redoList.length > 0
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* Each function returns the status code and body to send, or emits frames;
|
|
24
24
|
* neither Fastify nor `Response` appears in this file.
|
|
25
25
|
*/
|
|
26
|
-
import type { Logger } from "../logger.
|
|
27
|
-
import type { ActionResult } from "./history-actions.
|
|
26
|
+
import type { Logger } from "../logger.ts";
|
|
27
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
28
28
|
export type { ActionResult };
|
|
29
29
|
/**
|
|
30
30
|
* The one seam that decides where a generated image is written and what URL
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* own again.
|
|
18
18
|
*/
|
|
19
19
|
import type { BlockManifest, Operation } from "@avocadostudio-ai/shared";
|
|
20
|
-
import { type SkippedOperation } from "../ops/ops-engine.
|
|
21
|
-
import type { ActionResult } from "./history-actions.
|
|
20
|
+
import { type SkippedOperation } from "../ops/ops-engine.ts";
|
|
21
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
22
22
|
export type { ActionResult };
|
|
23
23
|
/**
|
|
24
24
|
* Never mutates state, so it skips undo snapshotting, the version bump and the
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
* neither Fastify nor `Response` appears in this file.
|
|
17
17
|
*/
|
|
18
18
|
import type { PageDoc, SiteConfig } from "@avocadostudio-ai/shared";
|
|
19
|
-
import type { PublishLogStatus } from "../state/session-state.
|
|
20
|
-
import { refreshPublishStatusFromVercel } from "../publish/publish-helpers.
|
|
21
|
-
import type { Logger } from "../logger.
|
|
22
|
-
import type { ActionResult } from "./history-actions.
|
|
19
|
+
import type { PublishLogStatus } from "../state/session-state.ts";
|
|
20
|
+
import { refreshPublishStatusFromVercel } from "../publish/publish-helpers.ts";
|
|
21
|
+
import type { Logger } from "../logger.ts";
|
|
22
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
23
23
|
export type { ActionResult };
|
|
24
24
|
/**
|
|
25
25
|
* What "the site as it is currently live" resolves to.
|
|
@@ -35,6 +35,16 @@ export type PublishedContent = {
|
|
|
35
35
|
pages: PageDoc[];
|
|
36
36
|
siteConfig?: SiteConfig | null;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Where the published side came from, in descending order of trust.
|
|
40
|
+
*
|
|
41
|
+
* A diff can be computed from any of them — a wrong diff only misreports. A
|
|
42
|
+
* *partial* publish cannot: it merges the unselected pages back out of this
|
|
43
|
+
* baseline and ships them, so a baseline that is really the startup demo seed
|
|
44
|
+
* would overwrite the live site with demo content. `"memory"` therefore
|
|
45
|
+
* disqualifies a subset publish; see `publish-selection.ts`.
|
|
46
|
+
*/
|
|
47
|
+
export type PublishedPagesSource = "site" | "file" | "memory";
|
|
38
48
|
/**
|
|
39
49
|
* Where the published side comes from. The monorepo answers it from the site
|
|
40
50
|
* app; a library-mode consumer answers it from its CMS adapter, whose
|
|
@@ -65,6 +75,7 @@ export declare function loadPublishedForDiff(opts: {
|
|
|
65
75
|
}): Promise<{
|
|
66
76
|
pages: PageDoc[];
|
|
67
77
|
siteConfig: SiteConfig | null;
|
|
78
|
+
source: PublishedPagesSource;
|
|
68
79
|
}>;
|
|
69
80
|
/**
|
|
70
81
|
* The default source: the four-step monorepo chain above, unchanged, so the
|
|
@@ -91,7 +91,7 @@ export async function loadPublishedForDiff(opts) {
|
|
|
91
91
|
}
|
|
92
92
|
// Hot path: remote returned both pages and siteConfig.
|
|
93
93
|
if (remotePages && remoteSiteConfig) {
|
|
94
|
-
return { pages: remotePages, siteConfig: remoteSiteConfig };
|
|
94
|
+
return { pages: remotePages, siteConfig: remoteSiteConfig, source: "site" };
|
|
95
95
|
}
|
|
96
96
|
// Either the remote didn't run, didn't include siteConfig (older SDK or
|
|
97
97
|
// site dev not yet restarted), or didn't include pages. Read the JSON file
|
|
@@ -101,9 +101,9 @@ export async function loadPublishedForDiff(opts) {
|
|
|
101
101
|
const pages = remotePages ?? fromFile.pages;
|
|
102
102
|
const siteConfig = remoteSiteConfig ?? fromFile.siteConfig;
|
|
103
103
|
if (pages)
|
|
104
|
-
return { pages, siteConfig };
|
|
104
|
+
return { pages, siteConfig, source: remotePages ? "site" : "file" };
|
|
105
105
|
logger.warn("publish/diff: falling back to in-memory publishedPages — diff may be inaccurate");
|
|
106
|
-
return { pages: Array.from(publishedPages.values()), siteConfig };
|
|
106
|
+
return { pages: Array.from(publishedPages.values()), siteConfig, source: "memory" };
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
109
|
* The default source: the four-step monorepo chain above, unchanged, so the
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* function returns the status code and body to send; neither Fastify nor
|
|
15
15
|
* `Response` appears in this file.
|
|
16
16
|
*/
|
|
17
|
-
import { listRestoreSnapshots, loadPublishedSnapshotFromCommit, deletePublishSnapshot } from "../publish/publish-helpers.
|
|
18
|
-
import type { Logger } from "../logger.
|
|
19
|
-
import type { ActionResult } from "./history-actions.
|
|
17
|
+
import { listRestoreSnapshots, loadPublishedSnapshotFromCommit, deletePublishSnapshot } from "../publish/publish-helpers.ts";
|
|
18
|
+
import type { Logger } from "../logger.ts";
|
|
19
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
20
20
|
export type { ActionResult };
|
|
21
21
|
/**
|
|
22
22
|
* The three git-backed helpers, injectable so the actions are testable.
|
|
@@ -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
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export { createOrchestrator, type CreateOrchestratorConfig, type OrchestratorHandler } from "./handler/create-orchestrator.ts";
|
|
2
2
|
export type { OrchestratorAuth, AuthContext } from "./handler/auth.ts";
|
|
3
|
-
export type { CmsAdapter, CmsCapabilities, CmsInlineAsset, CmsPublishContext, CmsPublishResult, CmsPerspective, CmsReadOptions, CmsMediaItem, CmsMediaPage, CmsMediaQuery, ResolvedCapabilities } from "./cms/adapter.ts";
|
|
4
|
-
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel, type JsonFileAdapterOptions, type EditorApiAdapterOptions, type CmsMediaSource, type CmsMediaSourceConfig } from "./cms/index.ts";
|
|
3
|
+
export type { CmsAdapter, CmsCapabilities, CmsInlineAsset, CmsPublishContext, CmsPublishResult, CmsPerspective, CmsReadOptions, CmsMediaItem, CmsMediaPage, CmsMediaQuery, CmsMediaUpload, ResolvedCapabilities } from "./cms/adapter.ts";
|
|
4
|
+
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, cmsMediaLabel, type JsonFileAdapterOptions, type EditorApiAdapterOptions, type CmsMediaSource, type CmsMediaUploader, 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
|
@@ -18,4 +18,31 @@
|
|
|
18
18
|
//
|
|
19
19
|
// The Fastify HTTP wrapper lives in apps/orchestrator and imports from here.
|
|
20
20
|
export { createOrchestrator } from "./handler/create-orchestrator.js";
|
|
21
|
-
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaLabel } from "./cms/index.js";
|
|
21
|
+
export { jsonFileAdapter, editorApiAdapter, resolveCapabilities, cmsMediaSource, cmsMediaUploader, 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 };
|
|
@@ -282,12 +282,28 @@ export declare function adviceResponse(args: {
|
|
|
282
282
|
};
|
|
283
283
|
export declare function plannerMessageWithPendingContext(session: string, message: string): string;
|
|
284
284
|
/** Build the site context lines without wrapping in a message. Returns null if empty. */
|
|
285
|
+
/** How many documents ride along in a planner request. */
|
|
286
|
+
export declare const SITE_CONTEXT_DOCUMENT_CAP = 60;
|
|
285
287
|
export declare function buildSiteContextBlock(args?: {
|
|
286
288
|
sitePurpose?: string;
|
|
287
289
|
siteHosting?: string;
|
|
288
290
|
businessContext?: ChatRequestBody["businessContext"];
|
|
289
291
|
siteContext?: ChatRequestBody["siteContext"];
|
|
290
292
|
pageDirectory?: string;
|
|
293
|
+
/**
|
|
294
|
+
* The site's downloadable documents, so the planner can link one.
|
|
295
|
+
*
|
|
296
|
+
* Without it, "link the winter menu" is unanswerable: the model has the
|
|
297
|
+
* page list and the block schemas and no idea the site holds fifteen PDFs,
|
|
298
|
+
* so it invents a plausible path — which is exactly how a link to a
|
|
299
|
+
* misspelled filename gets written in the first place. Listed by path and
|
|
300
|
+
* name, because the path is what goes in the link and the name is what the
|
|
301
|
+
* user said.
|
|
302
|
+
*/
|
|
303
|
+
documents?: Array<{
|
|
304
|
+
path: string;
|
|
305
|
+
name?: string;
|
|
306
|
+
}>;
|
|
291
307
|
}): string | null;
|
|
292
308
|
export declare function withSiteContext(message: string, args?: {
|
|
293
309
|
sitePurpose?: string;
|
|
@@ -492,6 +492,8 @@ function normalizeConstraintList(value) {
|
|
|
492
492
|
return [];
|
|
493
493
|
}
|
|
494
494
|
/** Build the site context lines without wrapping in a message. Returns null if empty. */
|
|
495
|
+
/** How many documents ride along in a planner request. */
|
|
496
|
+
export const SITE_CONTEXT_DOCUMENT_CAP = 60;
|
|
495
497
|
export function buildSiteContextBlock(args) {
|
|
496
498
|
const businessContext = parseJsonObjectMaybe(args?.businessContext);
|
|
497
499
|
const siteContext = parseJsonObjectMaybe(args?.siteContext);
|
|
@@ -516,7 +518,19 @@ export function buildSiteContextBlock(args) {
|
|
|
516
518
|
constraints.length > 0 ? `Constraints: ${constraints.join("; ")}` : null,
|
|
517
519
|
siteName ? `Site name: ${siteName}` : null,
|
|
518
520
|
pageTemplates.length > 0 ? `Page templates:\n${pageTemplates.join("\n")}` : null,
|
|
519
|
-
args?.pageDirectory ? `Pages:\n${args.pageDirectory}` : null
|
|
521
|
+
args?.pageDirectory ? `Pages:\n${args.pageDirectory}` : null,
|
|
522
|
+
/*
|
|
523
|
+
* Capped. A media library can hold hundreds of files and this rides in
|
|
524
|
+
* every planner request; past the cap the model sees a partial list, which
|
|
525
|
+
* makes it worse at finding an obscure document but never wrong about the
|
|
526
|
+
* ones it does see.
|
|
527
|
+
*/
|
|
528
|
+
args?.documents && args.documents.length > 0
|
|
529
|
+
? `Documents the site hosts (link by path, never invent one):\n${args.documents
|
|
530
|
+
.slice(0, SITE_CONTEXT_DOCUMENT_CAP)
|
|
531
|
+
.map((d) => (d.name ? `- ${d.name} — ${d.path}` : `- ${d.path}`))
|
|
532
|
+
.join("\n")}`
|
|
533
|
+
: null
|
|
520
534
|
].filter((line) => Boolean(line));
|
|
521
535
|
if (lines.length === 0)
|
|
522
536
|
return null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allowedBlockTypes, blockSchemas, declaredDefaultPropsForType, defaultPropsForType as sharedDefaultPropsForType
|
|
1
|
+
import { allowedBlockTypes, blockAcceptsProp, blockListItemAcceptsKey, blockSchemas, declaredDefaultPropsForType, defaultPropsForType as sharedDefaultPropsForType } from "@avocadostudio-ai/shared";
|
|
2
2
|
import { extractRouteMentions, firstRouteMention, normalizeRouteCandidate, parseCreatePageRequest } from "./intent-helpers.js";
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Prop-name aliasing, asked of the registry rather than of a literal
|
|
@@ -18,23 +18,14 @@ import { extractRouteMentions, firstRouteMention, normalizeRouteCandidate, parse
|
|
|
18
18
|
* wrong about its own job; the first link was answering a question about a name
|
|
19
19
|
* instead of about a schema.
|
|
20
20
|
*
|
|
21
|
-
* So ask the registry
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* So ask the registry — `blockAcceptsProp`, which lives in `shared` because the
|
|
22
|
+
* planner prompt needs the same answer before it tells the model a prop name is
|
|
23
|
+
* wrong. A rename requires positive evidence in both directions: the block
|
|
24
|
+
* cannot take the key the planner used, and can take the one we would rewrite
|
|
25
|
+
* it to. Every other case — unknown block type, a block that accepts both, a
|
|
26
|
+
* block that accepts neither — leaves the value alone, which is the answer that
|
|
27
|
+
* loses no data.
|
|
26
28
|
*/
|
|
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
29
|
/*
|
|
39
30
|
* Avocado's own `autoplay` / `loop` / `striped` are string enums ("true" /
|
|
40
31
|
* "false"), not booleans, so a model that emits a real boolean has to be
|
|
@@ -86,16 +77,11 @@ function shouldAliasProp(blockType, from, to) {
|
|
|
86
77
|
return !blockAcceptsProp(blockType, from) && blockAcceptsProp(blockType, to);
|
|
87
78
|
}
|
|
88
79
|
/*
|
|
89
|
-
* The same question for a key inside a list item
|
|
90
|
-
*
|
|
91
|
-
*
|
|
80
|
+
* The same question for a key inside a list item — also shared, for the same
|
|
81
|
+
* reason. A block with no declared list metadata answers "no" to both halves
|
|
82
|
+
* and is therefore left alone.
|
|
92
83
|
*/
|
|
93
|
-
|
|
94
|
-
if (!blockType)
|
|
95
|
-
return false;
|
|
96
|
-
const itemFields = getBlockMeta(blockType)?.listFields?.[listKey]?.itemFields;
|
|
97
|
-
return Boolean(itemFields && itemKey in itemFields);
|
|
98
|
-
}
|
|
84
|
+
const listItemAcceptsKey = blockListItemAcceptsKey;
|
|
99
85
|
function shouldAliasItemKey(blockType, listKey, from, to) {
|
|
100
86
|
return (!listItemAcceptsKey(blockType, listKey, from) && listItemAcceptsKey(blockType, listKey, to));
|
|
101
87
|
}
|
|
@@ -711,6 +697,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
711
697
|
return args.currentPage.blocks[idx - 1]?.id;
|
|
712
698
|
};
|
|
713
699
|
const usedBlockIds = new Set((args?.currentPage?.blocks ?? []).map((b) => b.id));
|
|
700
|
+
/**
|
|
701
|
+
* Block ids this pass renamed, old → new.
|
|
702
|
+
*
|
|
703
|
+
* Renaming an `add_block` id is only safe if the rest of the plan follows it.
|
|
704
|
+
* A plan that adds `b_hero_x` and then updates `b_hero_x` means the block it
|
|
705
|
+
* just added; leave the reference pointing at the colliding name and the
|
|
706
|
+
* update lands on the *existing* block instead — content written over the
|
|
707
|
+
* wrong block, which is the failure mode this repo keeps re-shipping. Ops are
|
|
708
|
+
* normalized in order, so only ops after the rename are rewritten: a
|
|
709
|
+
* reference that came *before* the add_block can only have meant the block
|
|
710
|
+
* that was already there.
|
|
711
|
+
*/
|
|
712
|
+
const renamedBlockIds = new Map();
|
|
714
713
|
let createdPageSlug;
|
|
715
714
|
let droppedPageLevelUpdate = false;
|
|
716
715
|
// itemIds already claimed by a remove_item freeze in THIS plan — used to spot
|
|
@@ -972,6 +971,19 @@ export function normalizePlanCandidate(input, args) {
|
|
|
972
971
|
raw.afterBlockId =
|
|
973
972
|
raw.after_block_id ?? raw.after ?? raw.insertAfterId ?? beforeToAfter(raw.beforeId ?? raw.insertBeforeId);
|
|
974
973
|
}
|
|
974
|
+
// Follow any add_block id an earlier op in this plan had to uniquify. Runs
|
|
975
|
+
// after the alias resolution above so `after_block_id` and friends are
|
|
976
|
+
// already folded into the canonical keys. See `renamedBlockIds`.
|
|
977
|
+
if (renamedBlockIds.size > 0) {
|
|
978
|
+
for (const key of ["blockId", "afterBlockId", "newBlockId"]) {
|
|
979
|
+
const value = raw[key];
|
|
980
|
+
if (typeof value === "string") {
|
|
981
|
+
const renamed = renamedBlockIds.get(value);
|
|
982
|
+
if (renamed)
|
|
983
|
+
raw[key] = renamed;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
975
987
|
if (!raw.afterPageSlug) {
|
|
976
988
|
raw.afterPageSlug =
|
|
977
989
|
raw.afterPageSlug ??
|
|
@@ -1246,15 +1258,37 @@ export function normalizePlanCandidate(input, args) {
|
|
|
1246
1258
|
block.props = {};
|
|
1247
1259
|
}
|
|
1248
1260
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1261
|
+
/*
|
|
1262
|
+
* The model authors this id. Nothing in the `add_block` prompt says how
|
|
1263
|
+
* to build one, so it free-associates from the page and the block type —
|
|
1264
|
+
* `b_featuregrid_wellness` for a FeatureGrid on /avocado-wellness. That is
|
|
1265
|
+
* a near-deterministic function of the prompt, so the same request asked
|
|
1266
|
+
* twice produces the same id twice, and the second one dies in the ops
|
|
1267
|
+
* engine on "Block id … already exists", taking the whole plan down with
|
|
1268
|
+
* it: a committing apply is all-or-nothing.
|
|
1269
|
+
*
|
|
1270
|
+
* `usedBlockIds` is seeded from the page's existing blocks and knew this
|
|
1271
|
+
* all along. It was consulted only when the model *omitted* an id, which
|
|
1272
|
+
* is the one case that cannot collide; a supplied id went through
|
|
1273
|
+
* untouched and was never even registered, so two ops in one plan could
|
|
1274
|
+
* pick the same name. Uniquify instead — a fourth FeatureGrid is a
|
|
1275
|
+
* legitimate reading of "populate this page", and only the label was
|
|
1276
|
+
* wrong. The engine's own check stays as the backstop and must never be
|
|
1277
|
+
* relaxed into an overwrite.
|
|
1278
|
+
*/
|
|
1279
|
+
const suppliedId = typeof block.id === "string" && block.id.length > 0 ? block.id : null;
|
|
1280
|
+
const idStem = suppliedId ?? (typeof block.type === "string" ? `b_${String(block.type).toLowerCase()}_${Date.now()}` : null);
|
|
1281
|
+
if (idStem) {
|
|
1282
|
+
let candidate = idStem;
|
|
1251
1283
|
let sfx = 0;
|
|
1252
|
-
while (usedBlockIds.has(
|
|
1284
|
+
while (usedBlockIds.has(candidate)) {
|
|
1253
1285
|
sfx++;
|
|
1254
|
-
|
|
1286
|
+
candidate = `${idStem}_${sfx}`;
|
|
1255
1287
|
}
|
|
1256
|
-
usedBlockIds.add(
|
|
1257
|
-
|
|
1288
|
+
usedBlockIds.add(candidate);
|
|
1289
|
+
if (suppliedId && candidate !== suppliedId)
|
|
1290
|
+
renamedBlockIds.set(suppliedId, candidate);
|
|
1291
|
+
block.id = candidate;
|
|
1258
1292
|
}
|
|
1259
1293
|
raw.block = block;
|
|
1260
1294
|
// 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
|
}
|