@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.
Files changed (69) hide show
  1. package/dist/chat/anthropic-planner.d.ts +8 -0
  2. package/dist/chat/anthropic-planner.js +166 -12
  3. package/dist/chat/chat-pipeline-translation.d.ts +13 -0
  4. package/dist/chat/chat-pipeline-translation.js +109 -45
  5. package/dist/chat/chat-pipeline.d.ts +1 -1
  6. package/dist/chat/chat-pipeline.js +296 -53
  7. package/dist/chat/gemini-planner.d.ts +2 -0
  8. package/dist/chat/gemini-planner.js +2 -1
  9. package/dist/chat/planner-types.d.ts +15 -0
  10. package/dist/chat/planner-types.js +2 -2
  11. package/dist/chat/planner.d.ts +12 -0
  12. package/dist/chat/planner.js +16 -2
  13. package/dist/chat/translation-chunking.d.ts +124 -0
  14. package/dist/chat/translation-chunking.js +371 -0
  15. package/dist/checks/field-walk.d.ts +25 -0
  16. package/dist/checks/field-walk.js +152 -0
  17. package/dist/checks/index.d.ts +5 -0
  18. package/dist/checks/index.js +4 -0
  19. package/dist/checks/page-weight.d.ts +22 -0
  20. package/dist/checks/page-weight.js +200 -0
  21. package/dist/checks/rules-draft.d.ts +2 -0
  22. package/dist/checks/rules-draft.js +375 -0
  23. package/dist/checks/run-checks.d.ts +32 -0
  24. package/dist/checks/run-checks.js +152 -0
  25. package/dist/checks/session-runner.d.ts +19 -0
  26. package/dist/checks/session-runner.js +95 -0
  27. package/dist/checks/types.d.ts +65 -0
  28. package/dist/checks/types.js +1 -0
  29. package/dist/durable/durable-store-singleton.d.ts +37 -0
  30. package/dist/durable/durable-store-singleton.js +179 -0
  31. package/dist/durable/finding-impact.d.ts +30 -0
  32. package/dist/durable/finding-impact.js +53 -0
  33. package/dist/durable/in-memory-durable-store.d.ts +203 -0
  34. package/dist/durable/in-memory-durable-store.js +363 -0
  35. package/dist/durable/index.d.ts +5 -0
  36. package/dist/durable/index.js +4 -0
  37. package/dist/durable/pending-plan-store.d.ts +28 -0
  38. package/dist/durable/pending-plan-store.js +156 -0
  39. package/dist/durable/sqlite-durable-store.d.ts +71 -0
  40. package/dist/durable/sqlite-durable-store.js +631 -0
  41. package/dist/durable/types.d.ts +265 -0
  42. package/dist/durable/types.js +1 -0
  43. package/dist/handler/create-orchestrator.d.ts +4 -0
  44. package/dist/handler/create-orchestrator.js +67 -4
  45. package/dist/http/audio-actions.d.ts +1 -1
  46. package/dist/http/checks-actions.d.ts +39 -0
  47. package/dist/http/checks-actions.js +122 -0
  48. package/dist/http/history-actions.d.ts +1 -1
  49. package/dist/http/image-generate-actions.d.ts +2 -2
  50. package/dist/http/ops-actions.d.ts +2 -2
  51. package/dist/http/publish-actions.d.ts +4 -4
  52. package/dist/http/restore-actions.d.ts +3 -3
  53. package/dist/http/screenshot-actions.d.ts +2 -2
  54. package/dist/http/session-actions.d.ts +1 -1
  55. package/dist/http/telemetry-feedback-actions.d.ts +2 -2
  56. package/dist/http/unsplash-actions.d.ts +2 -2
  57. package/dist/http/variations-actions.d.ts +2 -2
  58. package/dist/index.d.ts +7 -0
  59. package/dist/index.js +27 -0
  60. package/dist/nlp/deterministic-planner-context.d.ts +16 -0
  61. package/dist/nlp/deterministic-planner-context.js +33 -7
  62. package/dist/nlp/plan-normalizer.js +54 -6
  63. package/dist/ops/destructive-action-gate.js +7 -2
  64. package/dist/ops/ops-engine.d.ts +12 -1
  65. package/dist/ops/ops-engine.js +41 -14
  66. package/dist/publish/publish-target-registry.js +1 -1
  67. package/dist/publish/publish-target.d.ts +1 -1
  68. package/dist/state/session-state.js +8 -1
  69. 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
- * Declared at mount, before any request can build a manifest. The registry
302
- * holds this on `globalThis`, so it survives Next duplicating these modules
303
- * across the RSC / SSR / route-handler layers — the declaration made here has
304
- * to be visible to `/api/editor/blocks` in another copy.
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.js";
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.js";
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.js";
27
- import type { ActionResult } from "./history-actions.js";
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.js";
21
- import type { ActionResult } from "./history-actions.js";
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.js";
20
- import { refreshPublishStatusFromVercel } from "../publish/publish-helpers.js";
21
- import type { Logger } from "../logger.js";
22
- import type { ActionResult } from "./history-actions.js";
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.js";
18
- import type { Logger } from "../logger.js";
19
- import type { ActionResult } from "./history-actions.js";
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.js";
28
- import type { ActionResult } from "./history-actions.js";
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.js";
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.js";
19
- import type { ActionResult } from "./history-actions.js";
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.js";
19
- import type { ActionResult } from "./history-actions.js";
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.