@stackbone/cli 0.2.7 → 0.2.10

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.
@@ -0,0 +1,272 @@
1
+ -- 0038_evals.sql — EVALS in the box (STA-408, PRD STA-393): the six tables that
2
+ -- turn "is the agent any good?" from an opinion into a number, plus the `eval`
3
+ -- run trigger.
4
+ --
5
+ -- Widening a named CHECK REPLACES IT WHOLESALE. `runs_trigger_check` was
6
+ -- re-listed by hand in 0001 (manual/webhook/cron/event), 0016 (+job) and 0017
7
+ -- (+workflow/chat); a migration that only remembers its own value silently
8
+ -- revokes the rest, and nothing conflicts because they are different files. All
9
+ -- eight are re-listed below, and `evals-migration.spec.ts` now pins the complete
10
+ -- set so the next widening breaks loudly.
11
+ ALTER TABLE stackbone_platform.runs DROP CONSTRAINT IF EXISTS runs_trigger_check;
12
+ ALTER TABLE stackbone_platform.runs
13
+ ADD CONSTRAINT runs_trigger_check
14
+ CHECK (trigger IN (
15
+ 'manual', 'webhook', 'cron', 'event', 'job', 'workflow', 'chat', 'eval'
16
+ ));
17
+
18
+ -- --------------------------------------------------------------------------
19
+ -- The case list: a header row plus one immutable version per edit.
20
+ -- --------------------------------------------------------------------------
21
+ --
22
+ -- Same shape as prompts (0013), and for the same reason: a run has to keep
23
+ -- saying WHICH cases it measured. Editing the list copies its cases into a new
24
+ -- version, so a run from last month still reads its exact cases even after three
25
+ -- edits. The copy costs one INSERT per case; for the expected size — tens or
26
+ -- hundreds of cases per list — that is irrelevant next to losing the ability to
27
+ -- compare two runs at all.
28
+ --
29
+ -- Both header tables carry `deleted_at` and are SOFT-deleted by the store,
30
+ -- because a run points at a dataset version (and a suite) and a sealed run that
31
+ -- lost its subject is not evidence of anything. The uniqueness of the operator's
32
+ -- key therefore has to be PARTIAL — a full UNIQUE would let a deleted list squat
33
+ -- its name forever, so an operator who removes "smoke tests" could never make
34
+ -- another one.
35
+
36
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_datasets (
37
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
38
+ -- Operator-facing handle, unique among the ACTIVE lists (index below).
39
+ key text NOT NULL,
40
+ name text NOT NULL,
41
+ description text,
42
+ -- Points at the live row in `eval_dataset_versions` for this list.
43
+ current_version integer NOT NULL DEFAULT 1 CHECK (current_version >= 1),
44
+ created_at timestamptz NOT NULL DEFAULT now(),
45
+ updated_at timestamptz NOT NULL DEFAULT now(),
46
+ -- The acting user, as the verified JWT / signed actor header reported them.
47
+ -- NULL on the un-authenticated `stackbone dev` datapath.
48
+ created_by text,
49
+ updated_by text,
50
+ -- Soft delete. Reads filter on it; the row survives so the runs that
51
+ -- measured against it keep their subject.
52
+ deleted_at timestamptz
53
+ );
54
+
55
+ -- Unique among ACTIVE lists only: see the note above on squatting.
56
+ CREATE UNIQUE INDEX IF NOT EXISTS eval_datasets_active_key_idx
57
+ ON stackbone_platform.eval_datasets (key)
58
+ WHERE deleted_at IS NULL;
59
+
60
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_dataset_versions (
61
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
62
+ dataset_id uuid NOT NULL
63
+ REFERENCES stackbone_platform.eval_datasets (id) ON DELETE CASCADE,
64
+ version integer NOT NULL CHECK (version >= 1),
65
+ -- Free text an operator can leave describing what this edit changed.
66
+ note text,
67
+ created_at timestamptz NOT NULL DEFAULT now(),
68
+ created_by text
69
+ );
70
+
71
+ -- The race backstop. The store assigns MAX(version) + 1 under a per-dataset
72
+ -- advisory lock inside the same transaction, exactly like prompts; this index is
73
+ -- what stops two writers both landing version 1 if that lock is ever lost.
74
+ CREATE UNIQUE INDEX IF NOT EXISTS eval_dataset_versions_version_idx
75
+ ON stackbone_platform.eval_dataset_versions (dataset_id, version);
76
+
77
+ -- Drives the version-history list (newest first).
78
+ CREATE INDEX IF NOT EXISTS eval_dataset_versions_version_desc_idx
79
+ ON stackbone_platform.eval_dataset_versions (dataset_id, version DESC);
80
+
81
+ -- A case belongs to ONE version. Editing the list copies the cases forward.
82
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_cases (
83
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
84
+ dataset_version_id uuid NOT NULL
85
+ REFERENCES stackbone_platform.eval_dataset_versions (id) ON DELETE CASCADE,
86
+ name text NOT NULL,
87
+ input jsonb NOT NULL,
88
+ -- Optional on purpose: most cases are judged by a rubric, and demanding a
89
+ -- reference answer would force the operator to invent one.
90
+ expected jsonb,
91
+ tags text[] NOT NULL DEFAULT '{}',
92
+ -- Which run this case was captured from, if it was captured. Deliberately a
93
+ -- BARE uuid with NO foreign key: a captured case has to outlive run
94
+ -- retention, and a case that vanished when its source run was pruned would
95
+ -- silently shrink the list an operator built. Do not "fix" this into an FK.
96
+ source_run_id uuid,
97
+ -- Explicit order within the version, so the list reads the way it was built.
98
+ position integer NOT NULL
99
+ );
100
+
101
+ CREATE INDEX IF NOT EXISTS eval_cases_version_position_idx
102
+ ON stackbone_platform.eval_cases (dataset_version_id, position);
103
+
104
+ -- --------------------------------------------------------------------------
105
+ -- The suite, its runs, and the per-case verdicts.
106
+ -- --------------------------------------------------------------------------
107
+ --
108
+ -- Two conventions inherited from guardrails (0034), and they are the reason this
109
+ -- table looks under-constrained:
110
+ --
111
+ -- 1. The DISCRIMINATOR columns — `target_kind`, `status` — carry NO value
112
+ -- CHECK. A box running an older build has to be able to STORE a kind or a
113
+ -- state it cannot execute rather than reject the write and lose the
114
+ -- operator's configuration. An unknown value fails CLOSED at evaluation
115
+ -- time, which is the safe place for that decision.
116
+ -- 2. No PAYLOAD default lives here. The documented defaults live in
117
+ -- `@stackbone/validators` → `studio-evals.ts`, so the runtime, the facade
118
+ -- and the Studio form all read one source of truth. The defaults below are
119
+ -- structural only: identity, timestamps, the progress counters whose
120
+ -- absence would make a half-written run row unreadable, and `repeats`,
121
+ -- whose 1 is the neutral value (run each case once) rather than a
122
+ -- configuration choice.
123
+ --
124
+ -- `scorers` and `variants` are free-form jsonb validated against the CATALOG at
125
+ -- the handler, never by the DDL. A CHECK here would be a second description of
126
+ -- each scorer that drifts from the one the screen renders its form from. Neither
127
+ -- gets an empty-array default: a suite that measures nothing, and a run with no
128
+ -- variant, are not rows worth making writable.
129
+
130
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_suites (
131
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
132
+ -- Operator-facing name, unique among the ACTIVE suites (index below), and
133
+ -- what `stackbone eval <suite>` is given on the command line.
134
+ name text NOT NULL,
135
+ description text,
136
+ -- `agent` | `workflow`. Deliberately unconstrained — see note 1 above.
137
+ target_kind text NOT NULL,
138
+ -- The agent or workflow name, resolved against the catalog at launch.
139
+ target_name text NOT NULL,
140
+ dataset_id uuid NOT NULL REFERENCES stackbone_platform.eval_datasets (id),
141
+ -- A pinned version number, or NULL for "the latest". NULL by default is what
142
+ -- the operator wants: the case captured yesterday joins today's run.
143
+ dataset_pin integer,
144
+ -- A list of { type, params }, validated against the scorer catalog.
145
+ scorers jsonb NOT NULL,
146
+ -- The pass mark. Left without a recommended value on purpose: inventing one
147
+ -- before there is data from a real suite would be a guess dressed as advice.
148
+ threshold numeric NOT NULL,
149
+ -- How many times each case runs. More than one is how a case that only
150
+ -- passes half the time stops sneaking through as a pass.
151
+ repeats integer NOT NULL DEFAULT 1 CHECK (repeats >= 1),
152
+ -- The spend brake. Every case costs real tokens out of the customer's own
153
+ -- budget, so a run that would exceed this truncates and SAYS SO.
154
+ max_cases integer NOT NULL CHECK (max_cases >= 1),
155
+ created_at timestamptz NOT NULL DEFAULT now(),
156
+ updated_at timestamptz NOT NULL DEFAULT now(),
157
+ created_by text,
158
+ updated_by text,
159
+ deleted_at timestamptz
160
+ );
161
+
162
+ -- Unique among ACTIVE suites only, for the same anti-squatting reason as the
163
+ -- dataset key above.
164
+ CREATE UNIQUE INDEX IF NOT EXISTS eval_suites_active_name_idx
165
+ ON stackbone_platform.eval_suites (name)
166
+ WHERE deleted_at IS NULL;
167
+
168
+ CREATE INDEX IF NOT EXISTS eval_suites_active_dataset_idx
169
+ ON stackbone_platform.eval_suites (dataset_id)
170
+ WHERE deleted_at IS NULL;
171
+
172
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_runs (
173
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
174
+ suite_id uuid NOT NULL
175
+ REFERENCES stackbone_platform.eval_suites (id) ON DELETE CASCADE,
176
+ -- The EXACT version measured, never "whatever is current now". Without this
177
+ -- pin, "this run scored 90 and last month's scored 70" is not a comparison.
178
+ dataset_version_id uuid NOT NULL
179
+ REFERENCES stackbone_platform.eval_dataset_versions (id),
180
+ -- Version seal: what was actually evaluated, so an improvement can be
181
+ -- attributed to a change instead of guessed at.
182
+ git_sha text,
183
+ agent_version text,
184
+ -- One or two variants, each with its configuration and its SEALED
185
+ -- aggregates. Sealed at close so opening the comparison screen does not
186
+ -- recompute — and so the numbers cannot drift under a reader.
187
+ variants jsonb NOT NULL,
188
+ -- `running` | `done` | `failed` | `cancelled`. Unconstrained — see note 1.
189
+ -- Also the CANCEL channel: the engine checks this row between cases and
190
+ -- aborts clean, sealing what it already measured. There is no live run
191
+ -- cancellation in the platform to lean on.
192
+ status text NOT NULL,
193
+ -- DISTINCT CASES this run meant to measure, after the spend brake. Not
194
+ -- evaluations: `truncation_notice` reads this number as cases ("stopped this
195
+ -- run at 5 cases"), and the plan LIMITs by it.
196
+ case_count integer NOT NULL,
197
+ -- A run that measured fewer cases than the suite asked for has to say so,
198
+ -- not present partial coverage as complete.
199
+ truncated boolean NOT NULL DEFAULT false,
200
+ -- How many times each case ran, SEALED from the suite at launch beside the
201
+ -- dataset version and the variants — and for the same reason: editing the
202
+ -- suite next week must not change what an old run says it measured. It is
203
+ -- also the missing factor in the progress denominator (cases × repeats ×
204
+ -- variants); without it on the row, a screen counting attempts against
205
+ -- cases reports a run of 30 evaluations as finished after 5.
206
+ repeats integer NOT NULL DEFAULT 1 CHECK (repeats >= 1),
207
+ -- Progress, counted in EVALUATIONS (one row of `eval_case_results` per case
208
+ -- × variant × repeat), polled by the screen while the run is in flight.
209
+ completed_count integer NOT NULL DEFAULT 0,
210
+ passed_count integer NOT NULL DEFAULT 0,
211
+ -- Sealed aggregates: NULL until the run closes.
212
+ score_avg numeric,
213
+ input_tokens integer,
214
+ output_tokens integer,
215
+ p95_duration_ms integer,
216
+ created_at timestamptz NOT NULL DEFAULT now(),
217
+ created_by text,
218
+ started_at timestamptz,
219
+ finished_at timestamptz
220
+ );
221
+
222
+ -- Drives both the suite's run history and the "diff against the previous run"
223
+ -- read. NULLS LAST keeps a queued run (no `started_at` yet) off the top.
224
+ CREATE INDEX IF NOT EXISTS eval_runs_suite_started_idx
225
+ ON stackbone_platform.eval_runs (suite_id, started_at DESC NULLS LAST);
226
+
227
+ CREATE TABLE IF NOT EXISTS stackbone_platform.eval_case_results (
228
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
229
+ eval_run_id uuid NOT NULL
230
+ REFERENCES stackbone_platform.eval_runs (id) ON DELETE CASCADE,
231
+ -- BARE uuid with NO foreign key, beside a denormalised `case_name`: the case
232
+ -- row dies with its dataset version, and the verdict has to stay readable
233
+ -- afterwards. Do not "fix" this into an FK.
234
+ case_id uuid,
235
+ case_name text NOT NULL,
236
+ -- Which of the (at most two) variants produced this verdict.
237
+ variant_label text NOT NULL,
238
+ attempt integer NOT NULL DEFAULT 1 CHECK (attempt >= 1),
239
+ -- The hinge: from a failed case, jump to its full trace. ON DELETE SET NULL
240
+ -- and not CASCADE — when run retention eventually prunes the run, the
241
+ -- VERDICT survives and only the explanation is lost. The score is the
242
+ -- evidence; the trace is the commentary.
243
+ run_id uuid REFERENCES stackbone_platform.runs (id) ON DELETE SET NULL,
244
+ passed boolean NOT NULL,
245
+ score numeric,
246
+ -- One entry per scorer, so a failure names WHICH criterion rejected it.
247
+ scores jsonb,
248
+ output jsonb,
249
+ duration_ms integer,
250
+ -- Structured failure: a case killed by a technical error counts as a failure
251
+ -- without taking the run down, and a case stopped by a guardrail or parked
252
+ -- on a human approval is distinguishable from one that simply answered badly.
253
+ error jsonb,
254
+ created_at timestamptz NOT NULL DEFAULT now()
255
+ );
256
+
257
+ CREATE INDEX IF NOT EXISTS eval_case_results_run_idx
258
+ ON stackbone_platform.eval_case_results (eval_run_id);
259
+
260
+ -- A note on visibility, since it is a consequence nobody chose explicitly:
261
+ -- 0005_stackbone_viewer.sql set DEFAULT PRIVILEGES granting SELECT on new tables
262
+ -- in this schema to `stackbone_viewer`, so the Studio DB Explorer can read these
263
+ -- rows — including `eval_cases.input`, which will hold real captured customer
264
+ -- data. That matches how `runs.input` already behaves. If a deployment ever
265
+ -- needs these hidden, 0011_secrets_revoke_viewer.sql is the precedent for
266
+ -- revoking; nothing is revoked here.
267
+ --
268
+ -- No pg_notify trigger is attached to any of these tables ON PURPOSE. Ten
269
+ -- earlier migrations do attach one, so copying it in is the obvious reflex — but
270
+ -- live progress for an eval run is done by POLLING the `eval_runs` row, which
271
+ -- carries its own counters. Emitting eval frames would mean touching the
272
+ -- trigger, the emulator bus and the client parser to paint a counter.
@@ -0,0 +1,22 @@
1
+ -- 0039_eval_case_results_idempotency.sql — the constraint behind "a retry does
2
+ -- not re-invoke the model" (STA-415, PRD STA-393).
3
+ --
4
+ -- The engine already reads which cases an eval run has verdicts for and skips
5
+ -- them when it resumes. That read-then-write is a RACE on its own: two workers
6
+ -- picking up the same interrupted run would each read "case 3 has no verdict"
7
+ -- and each pay for case 3 in the customer's own token budget. Every eval case
8
+ -- costs real money, so the skip needs a constraint under it and not just a
9
+ -- convention.
10
+ --
11
+ -- A verdict is identified by WHICH case, under WHICH variant, on WHICH attempt.
12
+ -- All four columns are needed: repeats are the whole reason `attempt` exists (a
13
+ -- case that only passes half the time has to be run more than once), and the two
14
+ -- variants of a comparison measure the same case on purpose.
15
+ --
16
+ -- NULLS NOT DISTINCT because `case_id` is nullable — a verdict outlives the case
17
+ -- row it measured. Under the default (NULLS DISTINCT) two verdicts with a null
18
+ -- case would both be allowed through the index and the append would stop being
19
+ -- idempotent exactly when a list had been edited underneath a resumed run.
20
+ CREATE UNIQUE INDEX IF NOT EXISTS eval_case_results_attempt_idx
21
+ ON stackbone_platform.eval_case_results (eval_run_id, case_id, variant_label, attempt)
22
+ NULLS NOT DISTINCT;
@@ -0,0 +1,27 @@
1
+ -- 0040_eval_case_results_run_idx.sql — the index behind the foreign key 0038 added
2
+ -- from `eval_case_results` to `runs` (STA-415, PRD STA-393).
3
+ --
4
+ -- `eval_case_results.run_id` REFERENCES `runs (id) ON DELETE SET NULL`, and
5
+ -- Postgres enforces that by running the equivalent of
6
+ -- UPDATE eval_case_results SET run_id = NULL WHERE run_id = $1
7
+ -- inside EVERY transaction that deletes a `runs` row. With no index on the
8
+ -- referencing column that is a full sequential scan of what becomes the largest
9
+ -- table this feature creates (cases x repeats x variants x runs, never pruned —
10
+ -- 0038 says out loud that a verdict must outlive its run).
11
+ --
12
+ -- The cost lands on code that has nothing to do with evals: the workflow
13
+ -- concurrency gate deletes a single `waiting` run row on every admission
14
+ -- (`clearWaitingRun` / `clearOldestWaitingRun`), so admission latency would grow
15
+ -- with the accumulated eval history of a box that merely ran nightly evals.
16
+ --
17
+ -- The repo already gets this right for the two other foreign keys of the same
18
+ -- shape — `approvals_run_idx` next to 0002's ON DELETE SET NULL, and
19
+ -- `run_steps_run_started_idx` next to 0018's cascade. This is the one that was
20
+ -- left bare. `eval_case_results_run_idx` (0038) reads like it covers this, but it
21
+ -- indexes `eval_run_id` — the EVAL run, not the platform run.
22
+ --
23
+ -- A separate migration rather than an edit to 0038: an applied migration's
24
+ -- checksum is verified on every boot, so rewriting it would brick a box that
25
+ -- already ran it.
26
+ CREATE INDEX IF NOT EXISTS eval_case_results_platform_run_idx
27
+ ON stackbone_platform.eval_case_results (run_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbone/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "drizzle-kit": "^0.31.10",
27
27
  "drizzle-orm": "^0.45.2",
28
28
  "hono": "^4.12.15",
29
+ "jose": "^6.2.3",
29
30
  "libsodium-wrappers": "^0.8.4",
30
31
  "open": "^11.0.0",
31
32
  "openai": "^6.42.0",