static-fire 0.0.0-stage → 0.44.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -2
  3. package/bin/static-fire.js +46 -0
  4. package/package.json +29 -4
  5. package/pricing.yaml +20 -0
  6. package/src/adapters/index.ts +153 -0
  7. package/src/api.ts +218 -0
  8. package/src/cli.ts +297 -0
  9. package/src/core/apkit-install.ts +47 -0
  10. package/src/core/authored.ts +47 -0
  11. package/src/core/batch-state.ts +25 -0
  12. package/src/core/batch.ts +435 -0
  13. package/src/core/batches.ts +221 -0
  14. package/src/core/checks.ts +63 -0
  15. package/src/core/commands.ts +42 -0
  16. package/src/core/compare.ts +203 -0
  17. package/src/core/config.ts +25 -0
  18. package/src/core/copying.ts +18 -0
  19. package/src/core/db.ts +572 -0
  20. package/src/core/derived.ts +74 -0
  21. package/src/core/engine.ts +85 -0
  22. package/src/core/facets.ts +128 -0
  23. package/src/core/grade.ts +197 -0
  24. package/src/core/grading-versions.ts +281 -0
  25. package/src/core/identity.ts +263 -0
  26. package/src/core/init.ts +231 -0
  27. package/src/core/judge-schema.ts +37 -0
  28. package/src/core/judge.ts +166 -0
  29. package/src/core/label.ts +43 -0
  30. package/src/core/lock.ts +40 -0
  31. package/src/core/migrate.ts +288 -0
  32. package/src/core/new-task.ts +85 -0
  33. package/src/core/outside-tools.ts +129 -0
  34. package/src/core/profiles.ts +42 -0
  35. package/src/core/project.ts +54 -0
  36. package/src/core/regrade.ts +233 -0
  37. package/src/core/score.ts +148 -0
  38. package/src/core/settings.ts +44 -0
  39. package/src/core/task-file.ts +95 -0
  40. package/src/core/trial-detail.ts +73 -0
  41. package/src/core/types.ts +195 -0
  42. package/src/core/view-query.ts +85 -0
  43. package/src/core/view.ts +498 -0
  44. package/src/guide.md +547 -0
  45. package/src/server.ts +24 -0
  46. package/web/api.ts +6 -0
  47. package/web/app.tsx +1019 -0
  48. package/web/brand/offset-tokens.css +126 -0
  49. package/web/charts.tsx +190 -0
  50. package/web/compare.tsx +317 -0
  51. package/web/encoding.tsx +55 -0
  52. package/web/filter-menu.ts +164 -0
  53. package/web/index.html +17 -0
  54. package/web/pages.ts +16 -0
  55. package/web/run-status.ts +51 -0
  56. package/web/styles.css +333 -0
  57. package/web/suite-notes.ts +20 -0
  58. package/web/wording.ts +38 -0
@@ -0,0 +1,195 @@
1
+ // The primitives (see CONTEXT.md): Suite → Task → Measure → Criterion → Grader → Verdict.
2
+ // Trials, gradings and batches are records in the record store (src/core/db.ts).
3
+
4
+ /** Agent harnesses run a model headless. `human` is a person: its output is a file they wrote. */
5
+ export type AgentHarness = "claude" | "codex" | "pi" | "agy";
6
+ export type Harness = AgentHarness | "human";
7
+ export const HARNESSES: Harness[] = ["claude", "codex", "pi", "agy", "human"];
8
+
9
+ export interface HumanOutput { path: string; note?: string }
10
+
11
+ export interface AgentConfig {
12
+ id: string; // file name in configs/ (the label is built from the fields: core/label.ts)
13
+ harness: Harness;
14
+ model: string; // "by hand" for a human config
15
+ effort?: string;
16
+ provider?: string; // pi only
17
+ profile?: string; // apkit Profile name; omitted = no Profile
18
+ family?: string; // model family for chart colour; defaults to a guess from `model`
19
+ outputs?: Record<string, HumanOutput>; // human only: task id → the file they wrote (absolute path)
20
+ }
21
+
22
+ export interface JudgeConfig {
23
+ id: string;
24
+ label: string;
25
+ harness: Exclude<AgentHarness, "pi">; // judge Hosts must support JSON Schema output
26
+ model: string;
27
+ effort?: string;
28
+ }
29
+
30
+ // ---- Task definition -------------------------------------------------------------------------
31
+
32
+ export type Level = "floor" | "strong" | "ceiling";
33
+ export type Evidence = "output" | "inputs" | "references" | "project" | "sources";
34
+
35
+ export type Check =
36
+ | { type: "regex"; pattern: string; flags?: string; min?: number; max?: number }
37
+ | { type: "words"; min?: number; max?: number }
38
+ | { type: "frontmatter"; field: string; minChars?: number; maxChars?: number }
39
+ | { type: "command"; run: string; timeoutSec?: number };
40
+
41
+ export interface Criterion {
42
+ id: string;
43
+ question: string; // phrased so that "yes" = pass
44
+ level: Level;
45
+ grader: "check" | "judge" | "human";
46
+ check?: Check; // grader: check
47
+ evidence?: Evidence[]; // grader: judge (default: ["output"])
48
+ isolate?: boolean; // grader: judge — its own call instead of the measure's batch
49
+ derived?: string; // grader: judge — a script under the task's grader/ folder that makes an image from the output; the judge sees it (README, "Derived evidence")
50
+ }
51
+
52
+ export interface Measure {
53
+ id: string;
54
+ label: string;
55
+ weight?: number; // default 1
56
+ criteria: Criterion[];
57
+ }
58
+
59
+ export interface Task {
60
+ id: string;
61
+ version?: number; // a readable label only, in neither hash; a task file can leave it out
62
+ title: string;
63
+ description: string;
64
+ prompt: string;
65
+ inputs: string; // copied into the trial workspace
66
+ output: string; // path the agent must write, relative to the workspace
67
+ references?: string; // graders only
68
+ project?: string; // files the agent would find in the real project (copied into the workspace root, not shown to graders)
69
+ sources?: string; // graders only: saved copies of the pages the notes link to, for fact checks
70
+ heldOut?: string; // the real original this task was built from; an agent output that copies it is invalid
71
+ context?: string; // one paragraph a judge reads before the criteria
72
+ gates: Criterion[];
73
+ measures: Measure[];
74
+ dir: string; // absolute task directory (set by the loader)
75
+ }
76
+
77
+ export interface Suite {
78
+ id: string;
79
+ title: string;
80
+ description?: string;
81
+ tasks: string[];
82
+ weights?: Record<string, number>;
83
+ }
84
+
85
+ // ---- Results -----------------------------------------------------------------------------------
86
+
87
+ export interface Usage {
88
+ inputTokens: number;
89
+ outputTokens: number;
90
+ cacheReadTokens: number;
91
+ cacheWriteTokens: number;
92
+ }
93
+
94
+ export interface AgentResult {
95
+ ok: boolean;
96
+ error?: string;
97
+ usage: Usage;
98
+ costUsd: number | null;
99
+ costSource: "reported" | "estimated" | "unknown";
100
+ turns?: number;
101
+ }
102
+
103
+ export interface Vote { pass: boolean; reason: string }
104
+
105
+ export interface Verdict {
106
+ score: number | null; // 0–1 (judge: share of votes passed); null = error
107
+ votes: Vote[];
108
+ error?: string;
109
+ }
110
+
111
+ export interface Grades {
112
+ verdicts: Record<string, Verdict>; // by criterion id, gates included
113
+ gatesPassed: boolean;
114
+ measures: Record<string, number>; // 0–100 per measure
115
+ score: number; // 0–100 task score
116
+ errors: string[]; // criterion ids that errored (→ trial invalid)
117
+ judgeCostUsd: number;
118
+ }
119
+
120
+ export type TrialStatus = "queued" | "running" | "grading" | "done" | "invalid" | "failed";
121
+
122
+ export interface Trial {
123
+ id: string; // the trial id
124
+ experimentId: string; // the batch id (the name stays because tests/score.test.ts sets it)
125
+ taskId: string;
126
+ configId: string;
127
+ index: number;
128
+ status: TrialStatus;
129
+ startedAt?: string;
130
+ endedAt?: string;
131
+ durationMs?: number; // agent wall-clock only
132
+ result?: AgentResult;
133
+ grades?: Grades;
134
+ error?: string;
135
+ }
136
+
137
+ /**
138
+ * The input summarize() in score.ts takes: the tasks, suites and configs of one view (view.ts builds it).
139
+ * It is also the shape of the old `experiment.json` that migrate.ts imports. Nothing writes it any more.
140
+ * The name stays because score.ts and tests/score.test.ts use it, and both stay as they are.
141
+ */
142
+ export interface Experiment {
143
+ schema: 2;
144
+ id: string;
145
+ suiteIds: string[];
146
+ tasks: Record<string, number | null>; // task id → version at run time (null: the task file has none)
147
+ configIds: string[];
148
+ trialsPerConfig: number;
149
+ judge: JudgeConfig & { votes: number };
150
+ createdAt: string;
151
+ status: "running" | "done";
152
+ configs: Record<string, AgentConfig>; // snapshot at run time
153
+ suites: Record<string, Suite>; // snapshot at run time
154
+ userSetup?: { files: Record<string, string> }; // the user-level setup trials ran with (path → hash)
155
+ blockedTools?: Partial<Record<AgentHarness, string[]>>; // outside-service tools blocked in trials
156
+ gradedAt?: string; // when the current grading started; createdAt before any regrade
157
+ taskDefs?: Record<string, TaskSnapshot>; // each task's criteria as the current grading used them
158
+ }
159
+
160
+ /** A task's definition as one grading setup used it, so an earlier grading setup shows its own criteria. */
161
+ export type TaskSnapshot = Pick<Task, "id" | "title" | "description" | "version" | "gates" | "measures">;
162
+
163
+ // ---- Summaries (what the dashboard and `static-fire show` read) -------------------------------------
164
+
165
+ export interface Stat { mean: number | null; ci: number | null; n: number }
166
+
167
+ export interface TaskSummary {
168
+ score: Stat;
169
+ measures: Record<string, number>; // mean 0–100 per measure
170
+ criteria: Record<string, number>; // mean pass rate 0–1 per criterion
171
+ costUsd: number | null;
172
+ costSource: AgentResult["costSource"];
173
+ durationMs: number | null;
174
+ valid: number; invalid: number; failed: number; total: number;
175
+ }
176
+
177
+ export interface ConfigSummary {
178
+ configId: string;
179
+ label: string;
180
+ harness: Harness;
181
+ model: string;
182
+ family: string;
183
+ effort?: string;
184
+ profile?: string;
185
+ tasks: Record<string, TaskSummary>;
186
+ suites: Record<string, Stat>; // Suite Index per suite
187
+ index: Stat; // Workflow Index
188
+ costUsd: number | null; // mean cost per task over all tasks
189
+ durationMs: number | null; // mean time per task
190
+ }
191
+
192
+ export interface ExperimentSummary {
193
+ configs: ConfigSummary[];
194
+ deadCriteria: Record<string, string[]>; // task id → criterion ids every config passed or failed
195
+ }
@@ -0,0 +1,85 @@
1
+ // The query of GET /api/view, shared by the server and the dashboard (no Node imports here).
2
+ // One table of parameters: a filter added to the view is one more field in ViewQuery and one more entry in
3
+ // SINGLE (one value) or CONFIG_FACETS (repeated, several values), and in PARAMS for its place in the URL. Both
4
+ // the route and the dashboard's loading code pick it up. This file is the one home of the config facet list.
5
+ import type { ViewFilters } from "./view";
6
+
7
+ /** What the chart is about: the Workflow Index over every suite, one suite, or one task. */
8
+ export type Scope = { kind: "index" } | { kind: "suite"; id: string } | { kind: "task"; id: string };
9
+
10
+ /** The view a page asks for. Every field is optional; an empty object is the default view. */
11
+ export interface ViewQuery {
12
+ scope?: Scope;
13
+ judge?: string;
14
+ grading?: string;
15
+ /**
16
+ * Config facets: the picked values of each. A trial matches a facet when its value is any of them.
17
+ * `none` selects configs without an effort or without a Profile.
18
+ */
19
+ harness?: string[];
20
+ model?: string[];
21
+ effort?: string[];
22
+ profile?: string[];
23
+ since?: string;
24
+ until?: string;
25
+ }
26
+
27
+ /** Scope in a URL: absent or `index`, `suite:<id>`, `task:<id>`. */
28
+ export const scopeParam = (s: Scope): string => (s.kind === "index" ? "index" : `${s.kind}:${s.id}`);
29
+
30
+ function parseScope(v: string): Scope {
31
+ if (v === "index") return { kind: "index" };
32
+ const m = /^(suite|task):(.+)$/.exec(v);
33
+ if (!m) throw new Error(`scope ${v} is not index, suite:<id> or task:<id>`);
34
+ return { kind: m[1] as "suite" | "task", id: m[2]! };
35
+ }
36
+
37
+ /** Parameters that take one value each. A view has one judge, one grading version and one date range. */
38
+ type Single = "judge" | "grading" | "since" | "until";
39
+ const SINGLE: Single[] = ["judge", "grading", "since", "until"];
40
+ /** The config facets, each a field of the trial row: the parameter repeats for each picked value (`?model=opus&model=haiku`). */
41
+ export const CONFIG_FACETS = ["harness", "model", "effort", "profile"] as const;
42
+ export type ConfigFacet = (typeof CONFIG_FACETS)[number];
43
+ /** Every parameter, in the order a URL the page writes lists them. */
44
+ const PARAMS: readonly ("scope" | Single | ConfigFacet)[] = ["scope", "judge", "grading", "harness", "model", "effort", "profile", "since", "until"];
45
+ const KNOWN = new Set<string>(["scope", ...SINGLE, ...CONFIG_FACETS]);
46
+ const isConfig = (k: string): k is ConfigFacet => (CONFIG_FACETS as readonly string[]).includes(k);
47
+
48
+ /** The values in one stable order, each once, with empty ones dropped: a set of picks has one URL. */
49
+ export const valueSet = (values: Iterable<string>): string[] => [...new Set(values)].filter(Boolean).sort();
50
+
51
+ /** Reads a query string. Throws on an unknown or repeated parameter or a bad scope, so a filter is never silently ignored. */
52
+ export function parseViewQuery(params: URLSearchParams): ViewQuery {
53
+ const unknown = [...new Set([...params.keys()].filter((k) => !KNOWN.has(k)))];
54
+ if (unknown.length) throw new Error(`unknown view parameter(s): ${unknown.join(", ")} (known: ${PARAMS.join(", ")})`);
55
+ const repeated = (["scope", ...SINGLE] as string[]).filter((k) => params.getAll(k).length > 1);
56
+ if (repeated.length) throw new Error(`view parameter(s) ${repeated.join(", ")} can take only one value (repeat only ${CONFIG_FACETS.join(", ")})`);
57
+ const q: ViewQuery = {};
58
+ const scope = params.get("scope");
59
+ if (scope) q.scope = parseScope(scope);
60
+ for (const k of SINGLE) { const v = params.get(k); if (v) q[k] = v; }
61
+ for (const k of CONFIG_FACETS) { const v = valueSet(params.getAll(k)); if (v.length) q[k] = v; }
62
+ return q;
63
+ }
64
+
65
+ /** The query string for `q`: only the fields that are set; the index scope is the default, so it is left out. */
66
+ export function viewQueryParams(q: ViewQuery): URLSearchParams {
67
+ const p = new URLSearchParams();
68
+ if (q.scope && q.scope.kind !== "index") p.set("scope", scopeParam(q.scope));
69
+ for (const k of PARAMS) {
70
+ if (k === "scope") continue;
71
+ if (isConfig(k)) for (const v of valueSet(q[k] ?? [])) p.append(k, v);
72
+ else if (q[k]) p.set(k, q[k]!);
73
+ }
74
+ return p;
75
+ }
76
+
77
+ /** The view function's filters for `q`: a suite scope is that suite, a task scope that task. */
78
+ export function toViewFilters(q: ViewQuery): ViewFilters {
79
+ const f: ViewFilters = {};
80
+ if (q.scope?.kind === "suite") f.suites = [q.scope.id];
81
+ if (q.scope?.kind === "task") f.tasks = [q.scope.id];
82
+ for (const k of SINGLE) if (q[k]) f[k] = q[k];
83
+ for (const k of CONFIG_FACETS) { const v = valueSet(q[k] ?? []); if (v.length) f[k] = v; }
84
+ return f;
85
+ }