@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
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { Operation } from "@avocadostudio-ai/shared";
|
|
2
|
+
export type FindingSeverity = "error" | "warning" | "info";
|
|
3
|
+
/**
|
|
4
|
+
* `open` → a live problem. `fixed` is set by reconciliation, never by a rule.
|
|
5
|
+
* `dismissed` and `snoozed` are the user's decision and must survive every
|
|
6
|
+
* subsequent run — a checker whose dismissals evaporate is a checker people
|
|
7
|
+
* turn off in week two.
|
|
8
|
+
*
|
|
9
|
+
* The two are not synonyms, and the difference is a clock: `dismissed` means
|
|
10
|
+
* *never show me this again*, `snoozed` means *not this week*. A snooze without
|
|
11
|
+
* an expiry is a second word for dismiss, so a snoozed record carries
|
|
12
|
+
* `snoozedUntil` and `wakeSnoozedFindings` reopens it when that passes.
|
|
13
|
+
*/
|
|
14
|
+
export type FindingStatus = "open" | "snoozed" | "dismissed" | "fixed";
|
|
15
|
+
export type FindingEvidence = {
|
|
16
|
+
source: "draft" | "rendered" | "model";
|
|
17
|
+
blockId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Carried so a client can select the block without a lookup. The editor's
|
|
20
|
+
* property panel collapses to its page-level fallback when it is handed an
|
|
21
|
+
* id with no type, so a finding that knows the block but not its type sends
|
|
22
|
+
* the user to a panel that cannot show them the field.
|
|
23
|
+
*/
|
|
24
|
+
blockType?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The block's own heading, for telling two blocks of one type apart. A page
|
|
27
|
+
* with three Card Grids yields three findings that all read
|
|
28
|
+
* `cards[0].imageAlt`; without this the panel cannot say which is which and
|
|
29
|
+
* the reader has to press "Go to" on each to find out.
|
|
30
|
+
*/
|
|
31
|
+
blockLabel?: string;
|
|
32
|
+
/** Editable path into the block props, e.g. `meta.description`. */
|
|
33
|
+
path?: string;
|
|
34
|
+
excerpt?: string;
|
|
35
|
+
};
|
|
36
|
+
export type FindingInput = {
|
|
37
|
+
/** Stable across runs. Excludes the offending *value* — see `fingerprint`. */
|
|
38
|
+
fingerprint: string;
|
|
39
|
+
scopeKey: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
ruleId: string;
|
|
42
|
+
agent: string;
|
|
43
|
+
severity: FindingSeverity;
|
|
44
|
+
/**
|
|
45
|
+
* Severity × how much the page matters, 0–1. See `finding-impact.ts`.
|
|
46
|
+
*
|
|
47
|
+
* Recomputed by every run rather than fixed at first sight, because the page
|
|
48
|
+
* weight it derives from moves: link a page from the nav and its findings
|
|
49
|
+
* become more important than they were this morning, without anything about
|
|
50
|
+
* the findings themselves having changed.
|
|
51
|
+
*
|
|
52
|
+
* Optional on the way in — a caller that has not scored its pages should get
|
|
53
|
+
* the neutral fallback rather than a made-up number.
|
|
54
|
+
*/
|
|
55
|
+
impact?: number;
|
|
56
|
+
title: string;
|
|
57
|
+
detail?: string;
|
|
58
|
+
evidence?: FindingEvidence;
|
|
59
|
+
/** Ops that would resolve the finding. Applied only through the normal gate. */
|
|
60
|
+
proposedOps?: Operation[];
|
|
61
|
+
};
|
|
62
|
+
export type FindingRecord = FindingInput & {
|
|
63
|
+
id: string;
|
|
64
|
+
/** Always present on a record; `fallbackImpact` fills it for unscored rows. */
|
|
65
|
+
impact: number;
|
|
66
|
+
status: FindingStatus;
|
|
67
|
+
/** The run that most recently emitted this finding. Reconciliation reads it. */
|
|
68
|
+
lastRunId: string;
|
|
69
|
+
firstSeenAt: number;
|
|
70
|
+
lastSeenAt: number;
|
|
71
|
+
resolvedAt?: number;
|
|
72
|
+
/** When a `snoozed` record becomes `open` again. Set only for that status. */
|
|
73
|
+
snoozedUntil?: number;
|
|
74
|
+
};
|
|
75
|
+
export type FindingQuery = {
|
|
76
|
+
scopeKey: string;
|
|
77
|
+
slug?: string;
|
|
78
|
+
agent?: string;
|
|
79
|
+
status?: FindingStatus | FindingStatus[];
|
|
80
|
+
severity?: FindingSeverity;
|
|
81
|
+
limit?: number;
|
|
82
|
+
};
|
|
83
|
+
export type CheckRunTrigger = "manual" | "on_apply" | "on_publish" | "scheduled" | "webhook";
|
|
84
|
+
export type CheckRunInput = {
|
|
85
|
+
id: string;
|
|
86
|
+
scopeKey: string;
|
|
87
|
+
agent: string;
|
|
88
|
+
trigger: CheckRunTrigger;
|
|
89
|
+
startedAt: number;
|
|
90
|
+
};
|
|
91
|
+
export type CheckRunRecord = CheckRunInput & {
|
|
92
|
+
finishedAt?: number;
|
|
93
|
+
pagesScanned: number;
|
|
94
|
+
findingsOpened: number;
|
|
95
|
+
findingsClosed: number;
|
|
96
|
+
/** Model spend for the judgement tier. Cost invisible is cost unbounded. */
|
|
97
|
+
costUsd: number;
|
|
98
|
+
error?: string;
|
|
99
|
+
};
|
|
100
|
+
export type CheckRunPatch = Partial<Omit<CheckRunRecord, keyof CheckRunInput>>;
|
|
101
|
+
export type MemoryScope = "site" | "session" | "page";
|
|
102
|
+
export type MemoryKind = "fact" | "preference" | "decision" | "glossary";
|
|
103
|
+
export type MemorySource = "user" | "inferred" | "agent" | "import";
|
|
104
|
+
export type MemoryStatus = "active" | "superseded" | "rejected";
|
|
105
|
+
export type MemoryInput = {
|
|
106
|
+
scope: MemoryScope;
|
|
107
|
+
scopeKey: string;
|
|
108
|
+
kind: MemoryKind;
|
|
109
|
+
/** Namespaced, unique per (scope, scopeKey, kind). A new value supersedes. */
|
|
110
|
+
key: string;
|
|
111
|
+
value: string;
|
|
112
|
+
source: MemorySource;
|
|
113
|
+
/** traceId, findingId or run id — whatever lets a human audit the claim. */
|
|
114
|
+
sourceRef?: string;
|
|
115
|
+
/** 0–1. `inferred` should enter low; a wrong remembered fact steers silently. */
|
|
116
|
+
confidence?: number;
|
|
117
|
+
};
|
|
118
|
+
export type MemoryRecord = Required<Pick<MemoryInput, "confidence">> & MemoryInput & {
|
|
119
|
+
id: string;
|
|
120
|
+
status: MemoryStatus;
|
|
121
|
+
supersedesId?: string;
|
|
122
|
+
createdAt: number;
|
|
123
|
+
lastUsedAt?: number;
|
|
124
|
+
useCount: number;
|
|
125
|
+
};
|
|
126
|
+
export type MemoryQuery = {
|
|
127
|
+
scopeKey?: string;
|
|
128
|
+
scope?: MemoryScope;
|
|
129
|
+
kind?: MemoryKind | MemoryKind[];
|
|
130
|
+
status?: MemoryStatus;
|
|
131
|
+
limit?: number;
|
|
132
|
+
};
|
|
133
|
+
export type CorrectionOutcome = "accepted" | "edited" | "rejected" | "undone";
|
|
134
|
+
export type CorrectionInput = {
|
|
135
|
+
traceId: string;
|
|
136
|
+
scopeKey: string;
|
|
137
|
+
slug?: string;
|
|
138
|
+
request: string;
|
|
139
|
+
proposed: Operation[];
|
|
140
|
+
outcome: CorrectionOutcome;
|
|
141
|
+
/** What actually landed, when the human edited rather than accepted. */
|
|
142
|
+
applied?: Operation[];
|
|
143
|
+
};
|
|
144
|
+
export type CorrectionRecord = CorrectionInput & {
|
|
145
|
+
id: string;
|
|
146
|
+
at: number;
|
|
147
|
+
};
|
|
148
|
+
export type CorrectionQuery = {
|
|
149
|
+
scopeKey?: string;
|
|
150
|
+
outcome?: CorrectionOutcome;
|
|
151
|
+
since?: number;
|
|
152
|
+
limit?: number;
|
|
153
|
+
};
|
|
154
|
+
export type ProposalStatus = "pending" | "approved" | "discarded" | "expired";
|
|
155
|
+
/**
|
|
156
|
+
* `pendingApprovalPlanBySession` is an in-memory Map, not persisted, evicted by
|
|
157
|
+
* age. That is fine for "you typed a sentence and approved it ten seconds
|
|
158
|
+
* later" and useless for an agent that runs at 03:00: the plan it produced has
|
|
159
|
+
* nowhere to live until morning.
|
|
160
|
+
*/
|
|
161
|
+
export type ProposalInput = {
|
|
162
|
+
id: string;
|
|
163
|
+
scopeKey: string;
|
|
164
|
+
/** Which agent or chat turn produced it. */
|
|
165
|
+
origin: string;
|
|
166
|
+
summary: string;
|
|
167
|
+
ops: Operation[];
|
|
168
|
+
slugs: string[];
|
|
169
|
+
/** The finding this proposal resolves, when it came from one. */
|
|
170
|
+
findingId?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Opaque to this store: whatever the producer needs to reconstruct its own
|
|
173
|
+
* richer object. The chat pipeline's `PendingApprovalPlan` carries a prompt
|
|
174
|
+
* hash, a model key, pending image ops and the destructive-gate reasons, none
|
|
175
|
+
* of which belong in a generic proposals table — and all of which have to
|
|
176
|
+
* survive a restart, or rehydration hands back a plan the approval path
|
|
177
|
+
* cannot execute.
|
|
178
|
+
*
|
|
179
|
+
* `ops`/`slugs`/`summary` stay typed so a consumer that only wants to show or
|
|
180
|
+
* apply the plan never has to know what produced it.
|
|
181
|
+
*/
|
|
182
|
+
payload?: Record<string, unknown>;
|
|
183
|
+
createdAt: number;
|
|
184
|
+
expiresAt?: number;
|
|
185
|
+
};
|
|
186
|
+
export type ProposalRecord = ProposalInput & {
|
|
187
|
+
status: ProposalStatus;
|
|
188
|
+
resolvedAt?: number;
|
|
189
|
+
};
|
|
190
|
+
export type ProposalQuery = {
|
|
191
|
+
scopeKey: string;
|
|
192
|
+
status?: ProposalStatus;
|
|
193
|
+
limit?: number;
|
|
194
|
+
};
|
|
195
|
+
export interface DurableStore {
|
|
196
|
+
/**
|
|
197
|
+
* Upsert by fingerprint. An existing record keeps its `firstSeenAt` and — the
|
|
198
|
+
* part that matters — its `status`, so a dismissal survives every re-run. A
|
|
199
|
+
* record previously reconciled to `fixed` reopens.
|
|
200
|
+
*
|
|
201
|
+
* Returns counts, not records, because the caller is a run loop that wants a
|
|
202
|
+
* ledger line and not a page of rows.
|
|
203
|
+
*/
|
|
204
|
+
recordFindings(runId: string, findings: FindingInput[]): Promise<{
|
|
205
|
+
opened: number;
|
|
206
|
+
updated: number;
|
|
207
|
+
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Close every `open` or `snoozed` finding in scope that this run did not
|
|
210
|
+
* re-emit. The absence *is* the signal — nothing has to notice a problem went
|
|
211
|
+
* away.
|
|
212
|
+
*
|
|
213
|
+
* Snoozed rows are included because a snooze postpones the *report*, not the
|
|
214
|
+
* problem: leave them out and a finding somebody deferred on Monday, then
|
|
215
|
+
* fixed on Tuesday, comes back on Friday as an open finding about a page that
|
|
216
|
+
* is fine.
|
|
217
|
+
*
|
|
218
|
+
* `slugs` bounds it to what the run actually scanned: a run over two pages
|
|
219
|
+
* must not close findings on the other forty-three.
|
|
220
|
+
*/
|
|
221
|
+
reconcileFindings(args: {
|
|
222
|
+
runId: string;
|
|
223
|
+
scopeKey: string;
|
|
224
|
+
slugs: string[];
|
|
225
|
+
agent?: string;
|
|
226
|
+
at?: number;
|
|
227
|
+
}): Promise<{
|
|
228
|
+
closed: number;
|
|
229
|
+
}>;
|
|
230
|
+
listFindings(query: FindingQuery): Promise<FindingRecord[]>;
|
|
231
|
+
getFinding(id: string): Promise<FindingRecord | null>;
|
|
232
|
+
/**
|
|
233
|
+
* `snoozed` derives its own `snoozedUntil` from `at` plus `SNOOZE_WINDOW_MS`
|
|
234
|
+
* rather than taking one, so every caller and every implementation agree on
|
|
235
|
+
* what a snooze is worth. Nothing else may set it.
|
|
236
|
+
*/
|
|
237
|
+
setFindingStatus(id: string, status: FindingStatus, at?: number): Promise<void>;
|
|
238
|
+
/**
|
|
239
|
+
* Reopen snoozed findings whose window has passed. The read paths call it, so
|
|
240
|
+
* a snooze ends without anything having to be scheduled — the same shape as
|
|
241
|
+
* `expireProposals`, and for the same reason: there is no cron here.
|
|
242
|
+
*/
|
|
243
|
+
wakeSnoozedFindings(scopeKey: string, now?: number): Promise<{
|
|
244
|
+
woken: number;
|
|
245
|
+
}>;
|
|
246
|
+
startCheckRun(run: CheckRunInput): Promise<CheckRunRecord>;
|
|
247
|
+
finishCheckRun(id: string, patch: CheckRunPatch): Promise<void>;
|
|
248
|
+
listCheckRuns(scopeKey: string, limit?: number): Promise<CheckRunRecord[]>;
|
|
249
|
+
/** Supersedes the active record with the same (scope, scopeKey, kind, key). */
|
|
250
|
+
putMemory(input: MemoryInput, at?: number): Promise<MemoryRecord>;
|
|
251
|
+
listMemory(query: MemoryQuery): Promise<MemoryRecord[]>;
|
|
252
|
+
/** Bump `lastUsedAt`/`useCount` for records that were actually sent to a model. */
|
|
253
|
+
touchMemory(ids: string[], at?: number): Promise<void>;
|
|
254
|
+
setMemoryStatus(id: string, status: MemoryStatus): Promise<void>;
|
|
255
|
+
recordCorrection(input: CorrectionInput, at?: number): Promise<CorrectionRecord>;
|
|
256
|
+
listCorrections(query?: CorrectionQuery): Promise<CorrectionRecord[]>;
|
|
257
|
+
putProposal(input: ProposalInput): Promise<ProposalRecord>;
|
|
258
|
+
getProposal(id: string): Promise<ProposalRecord | null>;
|
|
259
|
+
listProposals(query: ProposalQuery): Promise<ProposalRecord[]>;
|
|
260
|
+
setProposalStatus(id: string, status: ProposalStatus, at?: number): Promise<void>;
|
|
261
|
+
/** Sweep proposals past `expiresAt`. Returns how many were expired. */
|
|
262
|
+
expireProposals(now?: number): Promise<{
|
|
263
|
+
expired: number;
|
|
264
|
+
}>;
|
|
265
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -167,6 +167,10 @@ export interface CreateOrchestratorConfig {
|
|
|
167
167
|
*
|
|
168
168
|
* Omit it if you render Avocado's blocks (a scaffolded site does). The
|
|
169
169
|
* declaration is exclusive: nothing outside the list is offered or accepted.
|
|
170
|
+
*
|
|
171
|
+
* Pass the same list to `createEditorApiHandler`. That handler serves
|
|
172
|
+
* `/api/editor/blocks`, the manifest the editor reads, and it is routinely
|
|
173
|
+
* asked for it before anything has caused this module to be evaluated.
|
|
170
174
|
*/
|
|
171
175
|
blockTypes?: readonly string[];
|
|
172
176
|
/**
|
|
@@ -34,6 +34,7 @@ import { historyStatus, historyLog, historyUndoAction, historyRedoAction, histor
|
|
|
34
34
|
import { whoamiAction } from "../http/session-actions.js";
|
|
35
35
|
import { blocksManifestAction } from "../http/blocks-actions.js";
|
|
36
36
|
import { screenshotAction } from "../http/screenshot-actions.js";
|
|
37
|
+
import { runChecksAction, listFindingsAction, listCheckRunsAction, updateFindingAction } from "../http/checks-actions.js";
|
|
37
38
|
import { fileImageStore, formatImageChatFrame, generateImageAction, imageChatAction, imageChatStreamAction, interpretImageAction, validateImageChatRequest } from "../http/image-generate-actions.js";
|
|
38
39
|
import { transcribeAudioAction, transcriptionUnavailable, validateAudioInput } from "../http/audio-actions.js";
|
|
39
40
|
import { formatVariationFrame, parseVariationRequest, scopeVariationSession, variationsAction, variationsStreamAction } from "../http/variations-actions.js";
|
|
@@ -214,6 +215,10 @@ const SUPPORTED_ROUTES = [
|
|
|
214
215
|
"POST /draft/bootstrap",
|
|
215
216
|
"GET+PUT /draft/site-config",
|
|
216
217
|
"POST /ops",
|
|
218
|
+
"POST /checks/run",
|
|
219
|
+
"GET /checks/findings",
|
|
220
|
+
"GET /checks/runs",
|
|
221
|
+
"POST /checks/findings/status",
|
|
217
222
|
"GET /history/status",
|
|
218
223
|
"GET /history/log",
|
|
219
224
|
"POST /history/undo",
|
|
@@ -298,10 +303,18 @@ export function createOrchestrator(config = {}) {
|
|
|
298
303
|
// up editing the bundled demo pages instead of the site's real content.
|
|
299
304
|
const effectiveSiteId = config.adapter ? (config.siteId ?? "library") : config.siteId;
|
|
300
305
|
/*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
306
|
+
* The registry holds this on `globalThis`, so it survives Next duplicating
|
|
307
|
+
* these modules across the RSC / SSR / route-handler layers — the declaration
|
|
308
|
+
* made here is visible to `/api/editor/blocks` in another copy.
|
|
309
|
+
*
|
|
310
|
+
* It is *not*, however, made "at mount" in any sense that covers the other
|
|
311
|
+
* route. This runs when this route's module is first evaluated, which Next
|
|
312
|
+
* does on the first request to this route — so an editor that asks
|
|
313
|
+
* `/api/editor/blocks` before anything has touched `/api/avocado/*` sees an
|
|
314
|
+
* undeclared catalogue and is offered Avocado's built-ins alongside the
|
|
315
|
+
* site's own. Pass `blockTypes` to `createEditorApiHandler` as well; it
|
|
316
|
+
* declares the same catalogue while building the manifest, which is the point
|
|
317
|
+
* that actually needs it.
|
|
305
318
|
*/
|
|
306
319
|
if (config.blockTypes)
|
|
307
320
|
declareBlockCatalogue(config.blockTypes);
|
|
@@ -1164,6 +1177,56 @@ export function createOrchestrator(config = {}) {
|
|
|
1164
1177
|
* The bodies are the same functions the Fastify app calls, so undo means
|
|
1165
1178
|
* the same thing in both. See orchestrator-core/http/history-actions.ts.
|
|
1166
1179
|
*/
|
|
1180
|
+
/*
|
|
1181
|
+
* ---- Checks: findings, runs, and their status --------------------------
|
|
1182
|
+
*
|
|
1183
|
+
* Mounted here in the same commit that adds them to Fastify, rather than
|
|
1184
|
+
* after somebody notices. Every route in `history-actions.ts` exists
|
|
1185
|
+
* because that lesson was learned the expensive way: a UI shipped a button
|
|
1186
|
+
* whose endpoint answered "not handled by createOrchestrator()".
|
|
1187
|
+
*/
|
|
1188
|
+
if (request.method === "POST" && path === "/checks/run") {
|
|
1189
|
+
const runtime = await getRuntime();
|
|
1190
|
+
await runtime.ready;
|
|
1191
|
+
let raw;
|
|
1192
|
+
try {
|
|
1193
|
+
raw = await request.json();
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return jsonResponse({ error: "invalid JSON body" }, { status: 400, cors });
|
|
1197
|
+
}
|
|
1198
|
+
const body = (raw ?? {});
|
|
1199
|
+
// The draft has to exist before it can be checked — a cold library-mode
|
|
1200
|
+
// process has no pages until this runs, and an unchecked empty session
|
|
1201
|
+
// would report every page as missing because there are none.
|
|
1202
|
+
const scoped = scope(body.session, body.siteId);
|
|
1203
|
+
await runtime.bootstrapCache.ensure(scoped, runtime.adapter, runtime.log);
|
|
1204
|
+
return actionResponse(await runChecksAction(body, runtime.log), cors);
|
|
1205
|
+
}
|
|
1206
|
+
if (request.method === "GET" && path === "/checks/findings") {
|
|
1207
|
+
const runtime = await getRuntime();
|
|
1208
|
+
await runtime.ready;
|
|
1209
|
+
const query = Object.fromEntries(url.searchParams);
|
|
1210
|
+
return actionResponse(await listFindingsAction(query, runtime.log), cors);
|
|
1211
|
+
}
|
|
1212
|
+
if (request.method === "GET" && path === "/checks/runs") {
|
|
1213
|
+
const runtime = await getRuntime();
|
|
1214
|
+
await runtime.ready;
|
|
1215
|
+
const query = Object.fromEntries(url.searchParams);
|
|
1216
|
+
return actionResponse(await listCheckRunsAction(query), cors);
|
|
1217
|
+
}
|
|
1218
|
+
if (request.method === "POST" && path === "/checks/findings/status") {
|
|
1219
|
+
const runtime = await getRuntime();
|
|
1220
|
+
await runtime.ready;
|
|
1221
|
+
let raw;
|
|
1222
|
+
try {
|
|
1223
|
+
raw = await request.json();
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
return jsonResponse({ error: "invalid JSON body" }, { status: 400, cors });
|
|
1227
|
+
}
|
|
1228
|
+
return actionResponse(await updateFindingAction((raw ?? {})), cors);
|
|
1229
|
+
}
|
|
1167
1230
|
if (request.method === "GET" && path === "/history/status") {
|
|
1168
1231
|
const runtime = await getRuntime();
|
|
1169
1232
|
await runtime.ready;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* keep their own reader — Fastify streams the part (aborting mid-upload once it
|
|
16
16
|
* crosses the cap), library mode reads a `File` off `request.formData()`.
|
|
17
17
|
*/
|
|
18
|
-
import type { ActionResult } from "./history-actions.
|
|
18
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
19
19
|
export type { ActionResult };
|
|
20
20
|
/**
|
|
21
21
|
* Formats both providers read natively.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CheckRunTrigger } from "../durable/types.ts";
|
|
2
|
+
import type { Logger } from "../logger.ts";
|
|
3
|
+
import type { ActionResult } from "./history-actions.ts";
|
|
4
|
+
export type { ActionResult };
|
|
5
|
+
export type ChecksScope = {
|
|
6
|
+
session?: string;
|
|
7
|
+
siteId?: string;
|
|
8
|
+
};
|
|
9
|
+
export type RunChecksParams = ChecksScope & {
|
|
10
|
+
/** Restrict the scan. Cross-page rules still see the whole site. */
|
|
11
|
+
slugs?: string[];
|
|
12
|
+
trigger?: CheckRunTrigger;
|
|
13
|
+
};
|
|
14
|
+
/** POST /checks/run — scan the session's draft and reconcile its findings. */
|
|
15
|
+
export declare function runChecksAction(params: RunChecksParams, log?: Logger): Promise<ActionResult>;
|
|
16
|
+
export type ListFindingsParams = ChecksScope & {
|
|
17
|
+
slug?: string;
|
|
18
|
+
agent?: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
limit?: number;
|
|
21
|
+
};
|
|
22
|
+
/** GET /checks/findings — what is currently wrong, newest and most severe first. */
|
|
23
|
+
export declare function listFindingsAction(params: ListFindingsParams, log?: Logger): Promise<ActionResult>;
|
|
24
|
+
/** GET /checks/runs — the ledger, including what each run cost. */
|
|
25
|
+
export declare function listCheckRunsAction(params: ChecksScope & {
|
|
26
|
+
limit?: number;
|
|
27
|
+
}): Promise<ActionResult>;
|
|
28
|
+
export type UpdateFindingParams = ChecksScope & {
|
|
29
|
+
id?: string;
|
|
30
|
+
status?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* POST /checks/findings/status — dismiss, snooze, or reopen one finding.
|
|
34
|
+
*
|
|
35
|
+
* `fixed` is not accepted: it is reconciliation's word, meaning "a run looked
|
|
36
|
+
* and the problem was gone". Letting a client assert it would put a finding
|
|
37
|
+
* into a state the next run immediately contradicts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function updateFindingAction(params: UpdateFindingParams): Promise<ActionResult>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { scopedSessionKey } from "../state/session-state.js";
|
|
2
|
+
import { getDurableStore, durableHealth } from "../durable/durable-store-singleton.js";
|
|
3
|
+
import { runChecksForSession } from "../checks/session-runner.js";
|
|
4
|
+
/*
|
|
5
|
+
* The checks HTTP surface, as transport-agnostic actions.
|
|
6
|
+
*
|
|
7
|
+
* Written this way from the first commit rather than as Fastify handlers,
|
|
8
|
+
* because the alternative is already documented next door: `history-actions.ts`
|
|
9
|
+
* exists because `/history/undo` was wired into Fastify only, and answered "not
|
|
10
|
+
* handled by createOrchestrator()" in a library-mode UI that shows an Undo
|
|
11
|
+
* button unconditionally. A findings panel added to one transport would be a
|
|
12
|
+
* panel that is permanently empty in the other.
|
|
13
|
+
*/
|
|
14
|
+
const badRequest = (error) => ({ code: 400, body: { error } });
|
|
15
|
+
/**
|
|
16
|
+
* What the panel asks for when it does not say. Snoozed rows are excluded:
|
|
17
|
+
* including them is what made Snooze a button that removed a row until the next
|
|
18
|
+
* refresh put it straight back.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_STATUSES = ["open"];
|
|
21
|
+
/**
|
|
22
|
+
* End any snooze whose week is up, on the way in.
|
|
23
|
+
*
|
|
24
|
+
* There is no scheduler in this process, so the wake has to hang off something
|
|
25
|
+
* that already happens; the two read paths are the only moments a snooze
|
|
26
|
+
* expiring is observable. Best-effort — a failed sweep leaves a finding hidden a
|
|
27
|
+
* little longer, which is not worth failing a request over.
|
|
28
|
+
*/
|
|
29
|
+
async function wakeSnoozed(scopeKey, log) {
|
|
30
|
+
try {
|
|
31
|
+
await getDurableStore().wakeSnoozedFindings(scopeKey);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
log?.warn({ err: String(err), scopeKey }, "waking snoozed findings failed");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** POST /checks/run — scan the session's draft and reconcile its findings. */
|
|
38
|
+
export async function runChecksAction(params, log) {
|
|
39
|
+
if (!params.session)
|
|
40
|
+
return badRequest("session is required");
|
|
41
|
+
const scopeKey = scopedSessionKey(params.session, params.siteId);
|
|
42
|
+
try {
|
|
43
|
+
await wakeSnoozed(scopeKey, log);
|
|
44
|
+
const run = await runChecksForSession({
|
|
45
|
+
scopeKey,
|
|
46
|
+
trigger: params.trigger ?? "manual",
|
|
47
|
+
...(params.slugs?.length ? { slugs: params.slugs } : {})
|
|
48
|
+
});
|
|
49
|
+
const findings = await getDurableStore().listFindings({ scopeKey, status: DEFAULT_STATUSES });
|
|
50
|
+
// `durable` rides along on every response: findings written to the
|
|
51
|
+
// in-memory fallback look identical to durable ones until the process
|
|
52
|
+
// restarts, and a scheduled run is exactly when nobody is watching a log.
|
|
53
|
+
return { code: 200, body: { run, findings, durable: durableHealth() } };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
57
|
+
log?.error({ err: reason, scopeKey }, "checks run failed");
|
|
58
|
+
return { code: 500, body: { error: reason } };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const FINDING_STATUSES = ["open", "snoozed", "dismissed", "fixed"];
|
|
62
|
+
function parseStatuses(raw) {
|
|
63
|
+
if (!raw)
|
|
64
|
+
return undefined;
|
|
65
|
+
const wanted = raw
|
|
66
|
+
.split(",")
|
|
67
|
+
.map((s) => s.trim())
|
|
68
|
+
.filter((s) => FINDING_STATUSES.includes(s));
|
|
69
|
+
return wanted.length > 0 ? wanted : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** GET /checks/findings — what is currently wrong, newest and most severe first. */
|
|
72
|
+
export async function listFindingsAction(params, log) {
|
|
73
|
+
if (!params.session)
|
|
74
|
+
return badRequest("session is required");
|
|
75
|
+
const scopeKey = scopedSessionKey(params.session, params.siteId);
|
|
76
|
+
await wakeSnoozed(scopeKey, log);
|
|
77
|
+
const status = parseStatuses(params.status) ?? DEFAULT_STATUSES;
|
|
78
|
+
const limitRaw = Number(params.limit);
|
|
79
|
+
const findings = await getDurableStore().listFindings({
|
|
80
|
+
scopeKey,
|
|
81
|
+
status,
|
|
82
|
+
...(params.slug ? { slug: params.slug } : {}),
|
|
83
|
+
...(params.agent ? { agent: params.agent } : {}),
|
|
84
|
+
...(Number.isFinite(limitRaw) && limitRaw > 0 ? { limit: Math.floor(limitRaw) } : {})
|
|
85
|
+
});
|
|
86
|
+
return { code: 200, body: { findings, durable: durableHealth() } };
|
|
87
|
+
}
|
|
88
|
+
/** GET /checks/runs — the ledger, including what each run cost. */
|
|
89
|
+
export async function listCheckRunsAction(params) {
|
|
90
|
+
if (!params.session)
|
|
91
|
+
return badRequest("session is required");
|
|
92
|
+
const scopeKey = scopedSessionKey(params.session, params.siteId);
|
|
93
|
+
const limitRaw = Number(params.limit);
|
|
94
|
+
const runs = await getDurableStore().listCheckRuns(scopeKey, Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 50);
|
|
95
|
+
return { code: 200, body: { runs } };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* POST /checks/findings/status — dismiss, snooze, or reopen one finding.
|
|
99
|
+
*
|
|
100
|
+
* `fixed` is not accepted: it is reconciliation's word, meaning "a run looked
|
|
101
|
+
* and the problem was gone". Letting a client assert it would put a finding
|
|
102
|
+
* into a state the next run immediately contradicts.
|
|
103
|
+
*/
|
|
104
|
+
export async function updateFindingAction(params) {
|
|
105
|
+
if (!params.session)
|
|
106
|
+
return badRequest("session is required");
|
|
107
|
+
if (!params.id)
|
|
108
|
+
return badRequest("id is required");
|
|
109
|
+
const status = params.status;
|
|
110
|
+
if (status !== "dismissed" && status !== "snoozed" && status !== "open") {
|
|
111
|
+
return badRequest("status must be dismissed, snoozed or open");
|
|
112
|
+
}
|
|
113
|
+
const scopeKey = scopedSessionKey(params.session, params.siteId);
|
|
114
|
+
const store = getDurableStore();
|
|
115
|
+
const finding = await store.getFinding(params.id);
|
|
116
|
+
// Scope check, not politeness: findings are per-site and the id is opaque, so
|
|
117
|
+
// without this any session could dismiss any other site's finding.
|
|
118
|
+
if (!finding || finding.scopeKey !== scopeKey)
|
|
119
|
+
return { code: 404, body: { error: "finding not found" } };
|
|
120
|
+
await store.setFindingStatus(params.id, status);
|
|
121
|
+
return { code: 200, body: { finding: await store.getFinding(params.id) } };
|
|
122
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* status code and body to send; neither Fastify nor `Response` appears in this
|
|
16
16
|
* file.
|
|
17
17
|
*/
|
|
18
|
-
import type { Logger } from "../logger.
|
|
18
|
+
import type { Logger } from "../logger.ts";
|
|
19
19
|
/** What a caller sends. `siteId` scopes the session; both transports pass it through. */
|
|
20
20
|
export type HistoryScope = {
|
|
21
21
|
session?: string;
|
|
@@ -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.
|
|
@@ -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.
|