@checkstack/anomaly-backend 1.1.8 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,138 @@
1
1
  # @checkstack/anomaly-backend
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9dcc848: Plugin-owned AI tools: every domain plugin contributes its own AI tools (chat assistant + automation AI action), and `ai-backend` is platform-only.
8
+
9
+ Every plugin-specific AI tool is owned by the plugin whose domain it acts on, registered via that plugin's own `aiToolExtensionPoint` / `aiToolProjectionExtensionPoint` from its init - the same path an external plugin author uses. `ai-backend` no longer imports or depends on any capability plugin's `*-common`; the dependency direction is strictly plugin -> ai-platform. Pure helpers (`computeFieldDiff`, capability-summary, `ScriptContextKind`) live in `@checkstack/ai-common`.
10
+
11
+ Tools shipped:
12
+
13
+ - Health checks and automations: full CRUD - `healthcheck.propose` / `automation.propose` and `*.update` (`mutate`, deep-validated) and `*.delete` (`destructive`, always confirm-gated). `healthcheck.propose`'s dry-run calls the new deep `validateConfiguration` so propose-time validation matches apply-time. Assertions are validated against the collector's result schema and the canonical operator vocabulary. Capability-catalog tools (`ai.listCapabilities`, `ai.getCapabilitySchema`), script context tools (`ai.getScriptContext`, `ai.testScript`), and notify-subscriber tools (`healthcheck.notifySystemSubscribers` / `...GroupSubscribers`).
14
+ - Catalog: `catalog.createSystem` / `updateSystem` / `createGroup` / `updateGroup` (`mutate`), `catalog.deleteSystem` / `deleteGroup` (`destructive`), membership tools (`mutate`), plus `catalog.listSystems` / `listGroups` read projections.
15
+ - Incident: `incident.create` / `update` / `addUpdate` / `resolve` / `addLink` (`mutate`), `incident.delete` / `removeLink` (`destructive`), and `incident.get` / `incident.list` read projections.
16
+ - Maintenance: `maintenance.create` / `update` / `addUpdate` / `close` / `addLink` (`mutate`), `maintenance.delete` / `removeLink` (`destructive`), and `maintenance.list` / `get` read projections.
17
+ - Read projections for SLO (`slo.listObjectives`), dependency (`dependency.list`), incident (`incident.list`), healthcheck (`healthcheck.status`), and anomaly (`anomaly.explain`), each gated by the source procedure's own access rule and routed as the principal.
18
+ - Documentation grounding: `ai.searchDocs` / `ai.getDoc` over a build-time bundled docs index (BM25-ish ranking), so the assistant grounds how-to answers in Checkstack's own docs offline.
19
+ - URL introspection: `ai.probeUrl`, an SSRF-guarded read tool the assistant uses to inspect a real endpoint before drafting a health check. Update tools compute a before -> after field diff rendered on the confirm card (approve mode) or an "Applied" card (auto mode), so a change is never silent.
20
+
21
+ `ai_analyze` automation action (automation-backend, with an editor connection picker + audited tool calls): runs a bounded AI agent on the run context as the automation's `runAs` service account, so it can never exceed that identity's permissions; destructive tools are never offered; mutating tools auto-apply through the service account's client. Produces an `automation.analysis` artifact downstream actions can branch on. The agent loop is exposed as a headless `aiAgentRunnerRef` service so automation-backend can drive it without depending on ai-backend.
22
+
23
+ `notification.notifyForSubscription` is now callable by user / application principals holding `notification.send` (previously service-only). Every tool routes through the user-scoped client, so handler-side authorization is enforced exactly as a direct UI/RPC action; the resolver gate plus the propose/apply re-check at propose AND apply are the additional authority. A systemic authz regression test asserts every registered tool falls into exactly one safe authorization category.
24
+
25
+ A new `ai_transport` enum value `automation` records the AI action's tool calls in the `ai_tool_calls` audit log. No new durable state beyond that; each tool is a thin, deterministic wrapper over an existing RPC, so every pod behaves identically.
26
+
27
+ This is a beta minor.
28
+
29
+ - 9dcc848: Auto-resolve anomalies that settle at a new normal, and add global suppression.
30
+
31
+ Part A (bug fix): a confirmed anomaly used to stay stuck in `anomaly` indefinitely when the metric settled at a new stable level. Both detectors now carry a baseline-independent self-resolution path - spike: after `STABLE_RESOLUTION_RUN_COUNT` (5) consecutive healthy samples within `STABLE_RESOLUTION_RELATIVE_BAND` (10%) the row self-resolves to `recovered`; drift: when the projected change goes flat relative to the new mean for `STABLE_DRIFT_RESOLUTION_RUN_COUNT` (2) analyzer runs. The original baseline-relative recovery path is unchanged.
32
+
33
+ Part B (feature): global (per-row) suppression. New `suppressedAt` / `suppressedValue` / `suppressedBaseline` columns (Drizzle migration `0005`), `suppressAnomaly` / `unsuppressAnomaly` RPCs gated by `anomaly_feed.manage`, and a `suppression` filter on `getAnomalies` (default `active` hides suppressed rows). Suppressed rows drop out of the dashboard badge/widget active count; the widget exposes an eye-off suppress affordance. Suppression auto-clears once the observed value moves more than `SUPPRESSION_REACTIVATION_DELTA` (25%) from the value it was suppressed at. All suppression state lives on the shared `anomalies` row, so every pod reads the same active/suppressed set. Distinct from the existing per-user notification mute.
34
+
35
+ This is a beta minor.
36
+
37
+ - 9dcc848: Harden config-versioning so stored configs always migrate-then-validate and broken migration chains fail fast at boot.
38
+
39
+ - `@checkstack/backend-api` `Versioned<T>` gains `parseAssumingV1` (migrate-from-v1 then validate leniently, runtime path), `parseStrictAssumingV1` (migrate then validate strictly, editor path), and `validateMigrationChainFromV1()`. A standalone pure helper `assertMigrationChainFromV1({ version, migrations })` is the single shared implementation behind the constructor guard and `validateMigrationChainFromV1`.
40
+ - `Versioned` now validates its own v1 -> `version` chain in the constructor, which runs at module import / plugin registration. A new `no-restricted-syntax` ESLint rule bans calling `parse` / `safeParse` / `parseAsync` / `strict` directly on a `Versioned`'s `.schema` member.
41
+ - Auth strategy migration chains are validated at the `betterAuthExtensionPoint.addStrategy` chokepoint (`@checkstack/auth-backend`).
42
+ - Automation action AND trigger configs migrate-then-validate (lenient at dispatch, strict in the editor validator, recursing into `choose`/`parallel`/`repeat`/`sequence` blocks). The `run_script` / `run_shell` action configs bump to `version: 2` dropping the removed `sandbox` key, fixing the editor's `Unrecognized key: sandbox` error.
43
+ - Anomaly read path now validates: `getAnomalyConfig` / `getAnomalyAssignmentConfig` run stored records through `Versioned.parseRecord`; `PartialAnomalySettingsSchema` moved to `@checkstack/anomaly-common`. Notification ConfigService reads thread the migrations argument, and per-strategy `userConfig` is migrate-then-validated before `send()`.
44
+ - gitops-apply migrate-then-validates authored health-check config; integration connection validation routes through `safeValidate`. The latent HTTP health-check `result` schema (at `version: 3` with no migrations) now ships a pass-through v1 -> v2 -> v3 chain.
45
+
46
+ BREAKING CHANGES (fail-fast at boot, intended):
47
+
48
+ - Any `Versioned` config with `version > 1` and an incomplete or non-contiguous migration chain now throws at construction (boot) instead of failing lazily on first read. This covers every `Versioned` instance repo-wide, including future plugin types. Out-of-tree plugins shipping such a config must add the missing migration step(s); all in-repo strategies already have complete chains.
49
+ - An auth strategy declaring `configVersion > 1` without a complete chain throws at registration.
50
+ - A trigger's per-automation config is now a versioned `config: Versioned<TConfig>` instead of a bare `configSchema?`. Plugins registering triggers with `configSchema:` must wrap it: `config: new Versioned({ version: 1, schema })`. The underlying schema stays reachable via `config.schema`; triggers without per-automation config are unaffected.
51
+
52
+ State and scale: all affected reads resolve from shared Postgres / in-process registries, so every pod sees the same migrated answer. No new framework-owned current-state store.
53
+
54
+ This is a beta minor.
55
+
56
+ - 9dcc848: Align workspace dependency versions and migrate React Router to v7.
57
+
58
+ BREAKING CHANGES (React Router v7): All frontend packages now depend on `react-router-dom@^7.16.0`. Previously the workspace declared four divergent ranges (`^6.20.0`, `^6.22.0`, `^7.1.1`, `^7.14.2`), which resolved both `react-router@6` and `react-router@7` into a single bundle. Everything is now unified on v7. The public imports the app uses (`BrowserRouter`, `Routes`, `Route`, `Link`, `NavLink`, `MemoryRouter`, `useNavigate`, `useParams`, `useSearchParams`, `useLocation`) are unchanged between v6 and v7, so no source rewrites were required - but any out-of-tree plugin still on react-router v6 should upgrade to v7 (see the React Router v6 -> v7 upgrade guide) to share the host's single router instance via the import map.
59
+
60
+ Other unified ranges (no API change): `react` -> `^18.3.1`, the `@orpc/*` family (`contract`, `server`, `client`, `tanstack-query`, `openapi`, `zod`) -> `^1.14.4`, and `better-auth` -> `^1.6.13`.
61
+
62
+ Removed the pre-rename `@orpc/react-query` leftover from `@checkstack/frontend-api`; its `createRouterUtils` / `RouterUtils` / `ProcedureUtils` now come from `@orpc/tanstack-query` (the package already in use).
63
+
64
+ Stale in-range runtime deps pulled up to current published versions: `hono` `^4.12.23`, `@tanstack/react-query` (+devtools) `^5.100.14`, `date-fns` `^4.4.0`, `jose` `^6.2.3`, `tar` `^7.5.16`, `semver` `^7.8.1`, `@xyflow/react` `^12.11.0`.
65
+
66
+ ### Patch Changes
67
+
68
+ - 9dcc848: Assorted bug fixes and small hardening across the platform.
69
+
70
+ - announcement-backend: `updateAnnouncement` now invalidates the active-announcements and admin-list caches (it was missing the `invalidateAllActive` / `invalidateListAll` calls), so an edited announcement no longer stays stale up to the 45s TTL.
71
+ - anomaly-backend: anomaly/drift state transitions (confirmations, recoveries, self-resolutions) now log at `debug` instead of info/warn - they are already surfaced via the `ANOMALY_STATE_CHANGED` signal, so logging them louder just added noise; genuine failure paths stay `warn`.
72
+ - backend: the `/api/:pluginId/*` dispatcher now populates `requestHeaders` on the per-request RPC context, so a handler that re-enters the router as the originating user (e.g. an AI tool's user-scoped client) can forward the caller's session cookie / bearer - previously the loopback failed with "Authentication required". Guarded by a real end-to-end integration test. The HTTP server idle timeout is also raised (default 255s, configurable via `CHECKSTACK_SERVER_IDLE_TIMEOUT_SECONDS`, clamped 0-255, reset on each streamed chunk) so long AI chat SSE turns are not severed mid-stream.
73
+ - backend: a request for an unknown plugin id (`/api/<unknown>/...`) now returns `404 Not Found` instead of `500` (and logs at warn, not error, since it is a client request) - an unknown _procedure_ on a known plugin already 404'd. The in-app docs namespace `/checkstack/*` now serves Starlight's own `404.html` with a real 404 status for a missing doc, instead of falling through to the SPA catch-all and 200-ing the app shell. Both guarded by tests.
74
+ - automation-common: remove polynomial-time backtracking from `toShellEnvKey`'s underscore-trim (CodeQL `js/polynomial-redos`); a negative look-behind anchors the trailing run, keeping the trim linear.
75
+ - common + script-packages-common: the pure transport-safe sandbox-policy schema (`sandboxPolicySchema` and its sub-schemas + inferred types) moved to `@checkstack/common` (the neutral base), removing two inverted deps that existed only to reach the shape; `@checkstack/backend-api` continues to re-export it. The schema is no longer exported from `@checkstack/script-packages-common`. Pure refactor, no behavior change.
76
+ - catalog-backend: reject duplicate system names (a `CONFLICT` on create/rename, enforced by a pre-write check AND a new DB unique index on `systems.name`, migration 0004 which first resolves pre-existing duplicates by suffixing).
77
+ - catalog-frontend: detail-page cleanups (use `<NotFound />` not `<AccessDenied />` on the not-found branch, a readable key/value metadata list via `normalizeMetadata`, runtime locale via `formatDate`); and stop the browse view re-rendering on every health report (adopt a new statuses report only when a value actually changed, via `healthStatusesEqual`, so rows stay stable and interactive).
78
+ - healthcheck-backend: fix the daily-rollup retention step failing with an `ON CONFLICT` mismatch (SQLSTATE 42P10) after `environmentId` joined the `health_check_aggregates` unique constraint - the rollup now groups by (day, environmentId, sourceId) and uses a single exported conflict-target constant (`DAILY_AGGREGATE_CONFLICT_TARGET`) kept in lock-step with the schema by a unit test.
79
+ - automation-frontend: the service-account picker's "Learn more" links are now absolute URLs to the deployed Astro docs site (they 404ed as in-app relative paths). The Monaco script editor double-init crash is fixed (serialized cold init, a guarded `monacoGuard` accessor, theme/type effects gated on `apiReady`).
80
+ - auth-frontend: bound the desktop user-menu popover height (`max-h-[var(--radix-popover-content-available-height)]` + `overflow-y-auto`) so it no longer clips on short viewports, and fold the standalone `Account > Profile` item into a focusable name/email header (`profileHref` on `UserMenu`); the now-empty `Account` group no longer renders.
81
+ - satellite-frontend: picked up via the sidebar-nav migration (account-only user menu).
82
+
83
+ (Related UI fixes - the Monaco editor following the app theme, the `DynamicOptionsField` no-flash fix, the shared `Spinner`, GFM tables, and the user-menu popover bound - land their `@checkstack/ui` bump in the UI/perf changesets where `@checkstack/ui` is already minored.)
84
+
85
+ This is a beta patch.
86
+
87
+ - Updated dependencies [9dcc848]
88
+ - Updated dependencies [9dcc848]
89
+ - Updated dependencies [9dcc848]
90
+ - Updated dependencies [9dcc848]
91
+ - Updated dependencies [9dcc848]
92
+ - Updated dependencies [9dcc848]
93
+ - Updated dependencies [9dcc848]
94
+ - Updated dependencies [9dcc848]
95
+ - Updated dependencies [9dcc848]
96
+ - Updated dependencies [9dcc848]
97
+ - Updated dependencies [9dcc848]
98
+ - Updated dependencies [9dcc848]
99
+ - Updated dependencies [9dcc848]
100
+ - Updated dependencies [9dcc848]
101
+ - Updated dependencies [9dcc848]
102
+ - Updated dependencies [9dcc848]
103
+ - Updated dependencies [9dcc848]
104
+ - Updated dependencies [9dcc848]
105
+ - Updated dependencies [9dcc848]
106
+ - @checkstack/ai-backend@0.1.0
107
+ - @checkstack/backend-api@0.21.0
108
+ - @checkstack/healthcheck-backend@1.6.0
109
+ - @checkstack/healthcheck-common@1.5.0
110
+ - @checkstack/catalog-backend@1.4.0
111
+ - @checkstack/notification-common@1.3.0
112
+ - @checkstack/anomaly-common@1.3.0
113
+ - @checkstack/catalog-common@2.3.0
114
+ - @checkstack/common@0.13.0
115
+ - @checkstack/gitops-backend@0.5.0
116
+ - @checkstack/gitops-common@0.6.0
117
+ - @checkstack/cache-api@0.3.9
118
+ - @checkstack/queue-api@0.3.9
119
+ - @checkstack/signal-common@0.2.6
120
+ - @checkstack/cache-utils@0.2.14
121
+
122
+ ## 1.1.9
123
+
124
+ ### Patch Changes
125
+
126
+ - Updated dependencies [a57f7db]
127
+ - Updated dependencies [0d9e5d8]
128
+ - @checkstack/backend-api@0.20.0
129
+ - @checkstack/healthcheck-backend@1.5.0
130
+ - @checkstack/cache-api@0.3.8
131
+ - @checkstack/catalog-backend@1.3.1
132
+ - @checkstack/gitops-backend@0.4.1
133
+ - @checkstack/queue-api@0.3.8
134
+ - @checkstack/cache-utils@0.2.13
135
+
3
136
  ## 1.1.8
4
137
 
5
138
  ### Patch Changes
@@ -0,0 +1,3 @@
1
+ ALTER TABLE "anomalies" ADD COLUMN "suppressed_at" timestamp;--> statement-breakpoint
2
+ ALTER TABLE "anomalies" ADD COLUMN "suppressed_value" double precision;--> statement-breakpoint
3
+ ALTER TABLE "anomalies" ADD COLUMN "suppressed_baseline" double precision;
@@ -0,0 +1,419 @@
1
+ {
2
+ "id": "845d6dfb-760a-47da-820b-6357758651ca",
3
+ "prevId": "d2a17ab6-efcd-4936-a870-ee045c74e10e",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.anomalies": {
8
+ "name": "anomalies",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "system_id": {
19
+ "name": "system_id",
20
+ "type": "text",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "configuration_id": {
25
+ "name": "configuration_id",
26
+ "type": "uuid",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "field_path": {
31
+ "name": "field_path",
32
+ "type": "text",
33
+ "primaryKey": false,
34
+ "notNull": true
35
+ },
36
+ "kind": {
37
+ "name": "kind",
38
+ "type": "anomaly_kind",
39
+ "typeSchema": "public",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "'spike'"
43
+ },
44
+ "state": {
45
+ "name": "state",
46
+ "type": "anomaly_state",
47
+ "typeSchema": "public",
48
+ "primaryKey": false,
49
+ "notNull": true
50
+ },
51
+ "direction": {
52
+ "name": "direction",
53
+ "type": "anomaly_direction",
54
+ "typeSchema": "public",
55
+ "primaryKey": false,
56
+ "notNull": true
57
+ },
58
+ "baseline_value": {
59
+ "name": "baseline_value",
60
+ "type": "double precision",
61
+ "primaryKey": false,
62
+ "notNull": false
63
+ },
64
+ "baseline_std_dev": {
65
+ "name": "baseline_std_dev",
66
+ "type": "double precision",
67
+ "primaryKey": false,
68
+ "notNull": false
69
+ },
70
+ "observed_value": {
71
+ "name": "observed_value",
72
+ "type": "text",
73
+ "primaryKey": false,
74
+ "notNull": true
75
+ },
76
+ "deviation": {
77
+ "name": "deviation",
78
+ "type": "double precision",
79
+ "primaryKey": false,
80
+ "notNull": true
81
+ },
82
+ "suspicious_run_count": {
83
+ "name": "suspicious_run_count",
84
+ "type": "integer",
85
+ "primaryKey": false,
86
+ "notNull": true,
87
+ "default": 0
88
+ },
89
+ "confirmation_threshold": {
90
+ "name": "confirmation_threshold",
91
+ "type": "integer",
92
+ "primaryKey": false,
93
+ "notNull": true
94
+ },
95
+ "started_at": {
96
+ "name": "started_at",
97
+ "type": "timestamp",
98
+ "primaryKey": false,
99
+ "notNull": true,
100
+ "default": "now()"
101
+ },
102
+ "confirmed_at": {
103
+ "name": "confirmed_at",
104
+ "type": "timestamp",
105
+ "primaryKey": false,
106
+ "notNull": false
107
+ },
108
+ "recovered_at": {
109
+ "name": "recovered_at",
110
+ "type": "timestamp",
111
+ "primaryKey": false,
112
+ "notNull": false
113
+ },
114
+ "suppressed_at": {
115
+ "name": "suppressed_at",
116
+ "type": "timestamp",
117
+ "primaryKey": false,
118
+ "notNull": false
119
+ },
120
+ "suppressed_value": {
121
+ "name": "suppressed_value",
122
+ "type": "double precision",
123
+ "primaryKey": false,
124
+ "notNull": false
125
+ },
126
+ "suppressed_baseline": {
127
+ "name": "suppressed_baseline",
128
+ "type": "double precision",
129
+ "primaryKey": false,
130
+ "notNull": false
131
+ },
132
+ "metadata": {
133
+ "name": "metadata",
134
+ "type": "jsonb",
135
+ "primaryKey": false,
136
+ "notNull": false
137
+ }
138
+ },
139
+ "indexes": {},
140
+ "foreignKeys": {},
141
+ "compositePrimaryKeys": {},
142
+ "uniqueConstraints": {},
143
+ "policies": {},
144
+ "checkConstraints": {},
145
+ "isRLSEnabled": false
146
+ },
147
+ "public.anomaly_assignments": {
148
+ "name": "anomaly_assignments",
149
+ "schema": "",
150
+ "columns": {
151
+ "system_id": {
152
+ "name": "system_id",
153
+ "type": "text",
154
+ "primaryKey": false,
155
+ "notNull": true
156
+ },
157
+ "configuration_id": {
158
+ "name": "configuration_id",
159
+ "type": "uuid",
160
+ "primaryKey": false,
161
+ "notNull": true
162
+ },
163
+ "config": {
164
+ "name": "config",
165
+ "type": "jsonb",
166
+ "primaryKey": false,
167
+ "notNull": true
168
+ }
169
+ },
170
+ "indexes": {},
171
+ "foreignKeys": {},
172
+ "compositePrimaryKeys": {},
173
+ "uniqueConstraints": {
174
+ "anomaly_assignments_pk": {
175
+ "name": "anomaly_assignments_pk",
176
+ "nullsNotDistinct": false,
177
+ "columns": [
178
+ "system_id",
179
+ "configuration_id"
180
+ ]
181
+ }
182
+ },
183
+ "policies": {},
184
+ "checkConstraints": {},
185
+ "isRLSEnabled": false
186
+ },
187
+ "public.anomaly_baselines": {
188
+ "name": "anomaly_baselines",
189
+ "schema": "",
190
+ "columns": {
191
+ "id": {
192
+ "name": "id",
193
+ "type": "uuid",
194
+ "primaryKey": true,
195
+ "notNull": true,
196
+ "default": "gen_random_uuid()"
197
+ },
198
+ "system_id": {
199
+ "name": "system_id",
200
+ "type": "text",
201
+ "primaryKey": false,
202
+ "notNull": true
203
+ },
204
+ "configuration_id": {
205
+ "name": "configuration_id",
206
+ "type": "uuid",
207
+ "primaryKey": false,
208
+ "notNull": true
209
+ },
210
+ "field_path": {
211
+ "name": "field_path",
212
+ "type": "text",
213
+ "primaryKey": false,
214
+ "notNull": true
215
+ },
216
+ "mean": {
217
+ "name": "mean",
218
+ "type": "double precision",
219
+ "primaryKey": false,
220
+ "notNull": true
221
+ },
222
+ "std_dev": {
223
+ "name": "std_dev",
224
+ "type": "double precision",
225
+ "primaryKey": false,
226
+ "notNull": true
227
+ },
228
+ "trend_slope": {
229
+ "name": "trend_slope",
230
+ "type": "double precision",
231
+ "primaryKey": false,
232
+ "notNull": true
233
+ },
234
+ "sample_count": {
235
+ "name": "sample_count",
236
+ "type": "integer",
237
+ "primaryKey": false,
238
+ "notNull": true
239
+ },
240
+ "computed_at": {
241
+ "name": "computed_at",
242
+ "type": "timestamp",
243
+ "primaryKey": false,
244
+ "notNull": true
245
+ },
246
+ "dominant_value": {
247
+ "name": "dominant_value",
248
+ "type": "text",
249
+ "primaryKey": false,
250
+ "notNull": false
251
+ },
252
+ "dominant_ratio": {
253
+ "name": "dominant_ratio",
254
+ "type": "double precision",
255
+ "primaryKey": false,
256
+ "notNull": false
257
+ }
258
+ },
259
+ "indexes": {},
260
+ "foreignKeys": {},
261
+ "compositePrimaryKeys": {},
262
+ "uniqueConstraints": {
263
+ "anomaly_baselines_unique_path": {
264
+ "name": "anomaly_baselines_unique_path",
265
+ "nullsNotDistinct": false,
266
+ "columns": [
267
+ "system_id",
268
+ "configuration_id",
269
+ "field_path"
270
+ ]
271
+ }
272
+ },
273
+ "policies": {},
274
+ "checkConstraints": {},
275
+ "isRLSEnabled": false
276
+ },
277
+ "public.anomaly_configurations": {
278
+ "name": "anomaly_configurations",
279
+ "schema": "",
280
+ "columns": {
281
+ "configuration_id": {
282
+ "name": "configuration_id",
283
+ "type": "uuid",
284
+ "primaryKey": true,
285
+ "notNull": true
286
+ },
287
+ "config": {
288
+ "name": "config",
289
+ "type": "jsonb",
290
+ "primaryKey": false,
291
+ "notNull": true
292
+ }
293
+ },
294
+ "indexes": {},
295
+ "foreignKeys": {},
296
+ "compositePrimaryKeys": {},
297
+ "uniqueConstraints": {},
298
+ "policies": {},
299
+ "checkConstraints": {},
300
+ "isRLSEnabled": false
301
+ },
302
+ "public.anomaly_notification_mutes": {
303
+ "name": "anomaly_notification_mutes",
304
+ "schema": "",
305
+ "columns": {
306
+ "user_id": {
307
+ "name": "user_id",
308
+ "type": "text",
309
+ "primaryKey": false,
310
+ "notNull": true
311
+ },
312
+ "system_id": {
313
+ "name": "system_id",
314
+ "type": "text",
315
+ "primaryKey": false,
316
+ "notNull": true
317
+ },
318
+ "field_path": {
319
+ "name": "field_path",
320
+ "type": "text",
321
+ "primaryKey": false,
322
+ "notNull": true
323
+ },
324
+ "muted_at": {
325
+ "name": "muted_at",
326
+ "type": "timestamp",
327
+ "primaryKey": false,
328
+ "notNull": true,
329
+ "default": "now()"
330
+ }
331
+ },
332
+ "indexes": {
333
+ "anomaly_notification_mutes_user_idx": {
334
+ "name": "anomaly_notification_mutes_user_idx",
335
+ "columns": [
336
+ {
337
+ "expression": "user_id",
338
+ "isExpression": false,
339
+ "asc": true,
340
+ "nulls": "last"
341
+ }
342
+ ],
343
+ "isUnique": false,
344
+ "concurrently": false,
345
+ "method": "btree",
346
+ "with": {}
347
+ },
348
+ "anomaly_notification_mutes_system_idx": {
349
+ "name": "anomaly_notification_mutes_system_idx",
350
+ "columns": [
351
+ {
352
+ "expression": "system_id",
353
+ "isExpression": false,
354
+ "asc": true,
355
+ "nulls": "last"
356
+ }
357
+ ],
358
+ "isUnique": false,
359
+ "concurrently": false,
360
+ "method": "btree",
361
+ "with": {}
362
+ }
363
+ },
364
+ "foreignKeys": {},
365
+ "compositePrimaryKeys": {
366
+ "anomaly_notification_mutes_user_id_system_id_field_path_pk": {
367
+ "name": "anomaly_notification_mutes_user_id_system_id_field_path_pk",
368
+ "columns": [
369
+ "user_id",
370
+ "system_id",
371
+ "field_path"
372
+ ]
373
+ }
374
+ },
375
+ "uniqueConstraints": {},
376
+ "policies": {},
377
+ "checkConstraints": {},
378
+ "isRLSEnabled": false
379
+ }
380
+ },
381
+ "enums": {
382
+ "public.anomaly_direction": {
383
+ "name": "anomaly_direction",
384
+ "schema": "public",
385
+ "values": [
386
+ "above",
387
+ "below",
388
+ "changed"
389
+ ]
390
+ },
391
+ "public.anomaly_kind": {
392
+ "name": "anomaly_kind",
393
+ "schema": "public",
394
+ "values": [
395
+ "spike",
396
+ "drift"
397
+ ]
398
+ },
399
+ "public.anomaly_state": {
400
+ "name": "anomaly_state",
401
+ "schema": "public",
402
+ "values": [
403
+ "suspicious",
404
+ "anomaly",
405
+ "recovered"
406
+ ]
407
+ }
408
+ },
409
+ "schemas": {},
410
+ "sequences": {},
411
+ "roles": {},
412
+ "policies": {},
413
+ "views": {},
414
+ "_meta": {
415
+ "columns": {},
416
+ "schemas": {},
417
+ "tables": {}
418
+ }
419
+ }
@@ -36,6 +36,13 @@
36
36
  "when": 1777549186811,
37
37
  "tag": "0004_gray_trauma",
38
38
  "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "7",
43
+ "when": 1780347895517,
44
+ "tag": "0005_cute_blonde_phantom",
45
+ "breakpoints": true
39
46
  }
40
47
  ]
41
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/anomaly-backend",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,32 +14,34 @@
14
14
  "lint:code": "eslint . --max-warnings 0"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/backend-api": "0.18.0",
17
+ "@checkstack/backend-api": "0.20.0",
18
+ "@checkstack/ai-backend": "0.0.0",
18
19
  "@checkstack/common": "0.12.0",
19
20
  "@checkstack/anomaly-common": "1.2.3",
20
21
  "@checkstack/signal-common": "0.2.5",
21
- "@checkstack/healthcheck-common": "1.3.0",
22
- "@checkstack/queue-api": "0.3.6",
23
- "@checkstack/cache-api": "0.3.6",
24
- "@checkstack/cache-utils": "0.2.11",
25
- "@checkstack/healthcheck-backend": "1.3.0",
26
- "@checkstack/catalog-backend": "1.2.0",
27
- "@checkstack/gitops-backend": "0.3.7",
28
- "@checkstack/gitops-common": "0.4.2",
22
+ "@checkstack/healthcheck-common": "1.4.0",
23
+ "@checkstack/queue-api": "0.3.8",
24
+ "@checkstack/cache-api": "0.3.8",
25
+ "@checkstack/cache-utils": "0.2.13",
26
+ "@checkstack/healthcheck-backend": "1.5.0",
27
+ "@checkstack/catalog-backend": "1.3.1",
28
+ "@checkstack/gitops-backend": "0.4.1",
29
+ "@checkstack/gitops-common": "0.5.0",
29
30
  "@checkstack/catalog-common": "2.2.3",
30
31
  "@checkstack/notification-common": "1.2.1",
31
32
  "drizzle-orm": "^0.45.0",
32
- "hono": "^4.12.14",
33
+ "hono": "^4.12.23",
33
34
  "zod": "^4.2.1",
34
- "@orpc/server": "^1.13.2"
35
+ "@orpc/contract": "^1.14.4",
36
+ "@orpc/server": "^1.14.4"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@checkstack/drizzle-helper": "0.0.5",
38
40
  "@checkstack/scripts": "0.3.4",
39
- "@checkstack/test-utils-backend": "0.1.31",
41
+ "@checkstack/test-utils-backend": "0.1.33",
40
42
  "@checkstack/tsconfig": "0.0.7",
41
43
  "@types/bun": "^1.0.0",
42
- "date-fns": "^4.1.0",
44
+ "date-fns": "^4.4.0",
43
45
  "drizzle-kit": "^0.31.10",
44
46
  "typescript": "^5.0.0"
45
47
  }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ buildProjectedTool,
4
+ deferredProjectionExecute,
5
+ } from "@checkstack/ai-backend";
6
+ import { anomalyContract, pluginMetadata } from "@checkstack/anomaly-common";
7
+
8
+ describe("anomaly AI projection (anomaly.explain)", () => {
9
+ test("projects getAnomalies as a read-only tool with the source procedure's access rules", () => {
10
+ const tool = buildProjectedTool({
11
+ procedure: anomalyContract.getAnomalies,
12
+ sourcePluginMetadata: pluginMetadata,
13
+ procedureKey: "getAnomalies",
14
+ name: "anomaly.explain",
15
+ description:
16
+ "List detected anomalies (statistical sigma/drift) for context. Read-only.",
17
+ effect: "read",
18
+ execute: deferredProjectionExecute,
19
+ });
20
+
21
+ expect(tool.name).toBe("anomaly.explain");
22
+ expect(tool.effect).toBe("read");
23
+
24
+ // The projection inherits the source procedure's gating — it must NOT
25
+ // collapse to the generic chat scope.
26
+ expect(tool.requiredAccessRules.length).toBeGreaterThan(0);
27
+ expect(tool.requiredAccessRules).not.toEqual(["ai.chat.read"]);
28
+ });
29
+ });