static-fire 0.44.0 → 0.54.1
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/README.md +40 -34
- package/package.json +2 -1
- package/src/adapters/index.ts +3 -3
- package/src/api.ts +38 -6
- package/src/cli.ts +106 -20
- package/src/core/batch.ts +16 -1
- package/src/core/commands.ts +7 -6
- package/src/core/compare.ts +4 -1
- package/src/core/coverage.ts +84 -0
- package/src/core/db.ts +36 -0
- package/src/core/facets.ts +23 -8
- package/src/core/init.ts +99 -9
- package/src/core/plan.ts +84 -0
- package/src/core/project.ts +95 -9
- package/src/core/regrade.ts +10 -1
- package/src/core/score.ts +1 -1
- package/src/core/task-page.ts +54 -0
- package/src/core/trial-detail.ts +3 -3
- package/src/core/types.ts +1 -1
- package/src/core/view-query.ts +30 -13
- package/src/core/view.ts +131 -53
- package/src/guide.md +128 -51
- package/src/server.ts +7 -3
- package/web/app.tsx +276 -133
- package/web/filter-menu.ts +35 -4
- package/web/how-it-works.tsx +92 -0
- package/web/index-switches.ts +29 -0
- package/web/levels.tsx +15 -0
- package/web/notices.ts +74 -0
- package/web/output-view.ts +61 -0
- package/web/pages.ts +26 -0
- package/web/styles.css +57 -22
- package/web/task-card.ts +6 -0
- package/web/tasks.tsx +122 -0
- package/web/suite-notes.ts +0 -20
package/src/core/trial-detail.ts
CHANGED
|
@@ -44,8 +44,8 @@ export function trialDetail(store: Store, trialId: string): TrialDetail | null {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** The trial's output file as the drawer shows it: an image (by the output path's extension) or text. */
|
|
48
|
-
export type TrialOutputFile = { image: Uint8Array<ArrayBuffer>; mediaType: string } | { text: string };
|
|
47
|
+
/** The trial's output file as the drawer shows it: an image (by the output path's extension) or text, with the output path it was read from. */
|
|
48
|
+
export type TrialOutputFile = ({ image: Uint8Array<ArrayBuffer>; mediaType: string } | { text: string }) & { path: string };
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* The trial's output file: the task's `output` path, as recorded with the task version the trial ran on, inside
|
|
@@ -69,5 +69,5 @@ export function readTrialOutputFile(store: Store, dbPath: string, trialId: strin
|
|
|
69
69
|
const rel = relative(realpathSync(workspace), real);
|
|
70
70
|
if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { missing: `trial ${trialId}: its output path ${output} leaves the workspace` };
|
|
71
71
|
const mediaType = IMAGE_MEDIA[extname(output).toLowerCase()];
|
|
72
|
-
try { return mediaType ? { image: readFileSync(real), mediaType } : { text: readFileSync(real, "utf8") }; } catch { return none; } // a folder, or unreadable
|
|
72
|
+
try { return mediaType ? { image: readFileSync(real), mediaType, path: output } : { text: readFileSync(real, "utf8"), path: output }; } catch { return none; } // a folder, or unreadable
|
|
73
73
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -184,7 +184,7 @@ export interface ConfigSummary {
|
|
|
184
184
|
profile?: string;
|
|
185
185
|
tasks: Record<string, TaskSummary>;
|
|
186
186
|
suites: Record<string, Stat>; // Suite Index per suite
|
|
187
|
-
index: Stat; //
|
|
187
|
+
index: Stat; // Agent Performance Index
|
|
188
188
|
costUsd: number | null; // mean cost per task over all tasks
|
|
189
189
|
durationMs: number | null; // mean time per task
|
|
190
190
|
}
|
package/src/core/view-query.ts
CHANGED
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
// the route and the dashboard's loading code pick it up. This file is the one home of the config facet list.
|
|
5
5
|
import type { ViewFilters } from "./view";
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* What the chart is about: the Agent Performance Index over the suites in view (the picked suites, else the default
|
|
9
|
+
* set), or one task.
|
|
10
|
+
*/
|
|
11
|
+
export type Scope = { kind: "index" } | { kind: "task"; id: string };
|
|
9
12
|
|
|
10
13
|
/** The view a page asks for. Every field is optional; an empty object is the default view. */
|
|
11
14
|
export interface ViewQuery {
|
|
12
15
|
scope?: Scope;
|
|
16
|
+
/**
|
|
17
|
+
* The picked suites of the index, as the repeated `suite` parameter (`?suite=short&suite=writing`). None picked:
|
|
18
|
+
* the view chooses the default set. Only with the index scope.
|
|
19
|
+
*/
|
|
20
|
+
suites?: string[];
|
|
13
21
|
judge?: string;
|
|
14
22
|
grading?: string;
|
|
15
23
|
/**
|
|
@@ -24,14 +32,14 @@ export interface ViewQuery {
|
|
|
24
32
|
until?: string;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
/** Scope in a URL: absent or `index`, `
|
|
28
|
-
export const scopeParam = (s: Scope): string => (s.kind === "index" ? "index" :
|
|
35
|
+
/** Scope in a URL: absent or `index`, `task:<id>`. A link from before #80 can say `suite:<id>`: that is `suite=<id>`. */
|
|
36
|
+
export const scopeParam = (s: Scope): string => (s.kind === "index" ? "index" : `task:${s.id}`);
|
|
29
37
|
|
|
30
|
-
function parseScope(v: string): Scope {
|
|
31
|
-
if (v === "index") return { kind: "index" };
|
|
38
|
+
function parseScope(v: string): { scope: Scope; suite?: string } {
|
|
39
|
+
if (v === "index") return { scope: { kind: "index" } };
|
|
32
40
|
const m = /^(suite|task):(.+)$/.exec(v);
|
|
33
41
|
if (!m) throw new Error(`scope ${v} is not index, suite:<id> or task:<id>`);
|
|
34
|
-
return
|
|
42
|
+
return m[1] === "task" ? { scope: { kind: "task", id: m[2]! } } : { scope: { kind: "index" }, suite: m[2]! };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/** Parameters that take one value each. A view has one judge, one grading version and one date range. */
|
|
@@ -41,8 +49,8 @@ const SINGLE: Single[] = ["judge", "grading", "since", "until"];
|
|
|
41
49
|
export const CONFIG_FACETS = ["harness", "model", "effort", "profile"] as const;
|
|
42
50
|
export type ConfigFacet = (typeof CONFIG_FACETS)[number];
|
|
43
51
|
/** 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]);
|
|
52
|
+
const PARAMS: readonly ("scope" | "suite" | Single | ConfigFacet)[] = ["scope", "suite", "judge", "grading", "harness", "model", "effort", "profile", "since", "until"];
|
|
53
|
+
const KNOWN = new Set<string>(["scope", "suite", ...SINGLE, ...CONFIG_FACETS]);
|
|
46
54
|
const isConfig = (k: string): k is ConfigFacet => (CONFIG_FACETS as readonly string[]).includes(k);
|
|
47
55
|
|
|
48
56
|
/** The values in one stable order, each once, with empty ones dropped: a set of picks has one URL. */
|
|
@@ -56,7 +64,15 @@ export function parseViewQuery(params: URLSearchParams): ViewQuery {
|
|
|
56
64
|
if (repeated.length) throw new Error(`view parameter(s) ${repeated.join(", ")} can take only one value (repeat only ${CONFIG_FACETS.join(", ")})`);
|
|
57
65
|
const q: ViewQuery = {};
|
|
58
66
|
const scope = params.get("scope");
|
|
59
|
-
|
|
67
|
+
const suites = params.getAll("suite");
|
|
68
|
+
if (scope) {
|
|
69
|
+
const parsed = parseScope(scope);
|
|
70
|
+
q.scope = parsed.scope;
|
|
71
|
+
if (parsed.suite) suites.push(parsed.suite);
|
|
72
|
+
}
|
|
73
|
+
const picked = valueSet(suites);
|
|
74
|
+
if (picked.length && q.scope?.kind === "task") throw new Error(`suite cannot be picked with scope ${scope}: a task's view has no suites`);
|
|
75
|
+
if (picked.length) q.suites = picked;
|
|
60
76
|
for (const k of SINGLE) { const v = params.get(k); if (v) q[k] = v; }
|
|
61
77
|
for (const k of CONFIG_FACETS) { const v = valueSet(params.getAll(k)); if (v.length) q[k] = v; }
|
|
62
78
|
return q;
|
|
@@ -66,19 +82,20 @@ export function parseViewQuery(params: URLSearchParams): ViewQuery {
|
|
|
66
82
|
export function viewQueryParams(q: ViewQuery): URLSearchParams {
|
|
67
83
|
const p = new URLSearchParams();
|
|
68
84
|
if (q.scope && q.scope.kind !== "index") p.set("scope", scopeParam(q.scope));
|
|
85
|
+
else for (const v of valueSet(q.suites ?? [])) p.append("suite", v);
|
|
69
86
|
for (const k of PARAMS) {
|
|
70
|
-
if (k === "scope") continue;
|
|
87
|
+
if (k === "scope" || k === "suite") continue;
|
|
71
88
|
if (isConfig(k)) for (const v of valueSet(q[k] ?? [])) p.append(k, v);
|
|
72
89
|
else if (q[k]) p.set(k, q[k]!);
|
|
73
90
|
}
|
|
74
91
|
return p;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
|
-
/** The view function's filters for `q`:
|
|
94
|
+
/** The view function's filters for `q`: the picked suites of the index, or a task scope's task. */
|
|
78
95
|
export function toViewFilters(q: ViewQuery): ViewFilters {
|
|
79
96
|
const f: ViewFilters = {};
|
|
80
|
-
if (q.scope?.kind === "suite") f.suites = [q.scope.id];
|
|
81
97
|
if (q.scope?.kind === "task") f.tasks = [q.scope.id];
|
|
98
|
+
else if (q.suites?.length) f.suites = valueSet(q.suites);
|
|
82
99
|
for (const k of SINGLE) if (q[k]) f[k] = q[k];
|
|
83
100
|
for (const k of CONFIG_FACETS) { const v = valueSet(q[k] ?? []); if (v.length) f[k] = v; }
|
|
84
101
|
return f;
|
package/src/core/view.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { NamedGradingSetup, VersionName } from "./grading-versions";
|
|
|
11
11
|
import { gradingHash as currentGradingHash, taskHash as currentTaskHash } from "./identity";
|
|
12
12
|
import type { CriterionContent, GradingContent } from "./identity";
|
|
13
13
|
import { summarize } from "./score";
|
|
14
|
+
import { defaultSuites } from "./coverage";
|
|
15
|
+
import type { Coverage } from "./coverage";
|
|
14
16
|
import { CONFIG_FACETS, valueSet } from "./view-query";
|
|
15
17
|
import type { ConfigFacet } from "./view-query";
|
|
16
18
|
import { loadJudges, loadSuites, loadTask } from "./authored";
|
|
@@ -21,9 +23,12 @@ import type { AgentConfig, ConfigSummary, Criterion, Experiment, Grades, JudgeCo
|
|
|
21
23
|
|
|
22
24
|
/** What the caller asks for. Every field is optional; an empty object is the default view. */
|
|
23
25
|
export interface ViewFilters {
|
|
24
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Suite ids. Their tasks are in scope and the Agent Performance Index is the mean over these suites. Default: the
|
|
28
|
+
* default set of the data in view (coverage.ts defaultSuites, DEC-019).
|
|
29
|
+
*/
|
|
25
30
|
suites?: string[];
|
|
26
|
-
/** Task ids. When set, only these tasks are in scope and no suite is (so no Suite or
|
|
31
|
+
/** Task ids. When set, only these tasks are in scope and no suite is (so no Suite or Agent Performance Index). Wins over `suites`. */
|
|
27
32
|
tasks?: string[];
|
|
28
33
|
/** Judge id from judges.yaml. Default: the first one (the default judge). */
|
|
29
34
|
judge?: string;
|
|
@@ -185,6 +190,8 @@ export interface View {
|
|
|
185
190
|
/** The values of each config facet applied, sorted (absent when not set). */
|
|
186
191
|
harness?: string[]; model?: string[]; effort?: string[]; profile?: string[];
|
|
187
192
|
};
|
|
193
|
+
/** Which suites the index covers and why each other suite is left out (DEC-020). Null for a view of tasks. */
|
|
194
|
+
coverage: Coverage | null;
|
|
188
195
|
/** Task id → the task in scope. */
|
|
189
196
|
tasks: Record<string, ViewTask>;
|
|
190
197
|
/** The trials in the view, by task, config key, then id. */
|
|
@@ -198,63 +205,66 @@ export interface View {
|
|
|
198
205
|
|
|
199
206
|
/** Selects and summarises the comparable trials for `filters`. Throws on an unknown id or an unusable filter. */
|
|
200
207
|
export function buildView(store: Store, filters: ViewFilters = {}, catalog: Catalog = projectCatalog()): View {
|
|
201
|
-
const { suites, taskIds } = scope(filters, catalog);
|
|
208
|
+
const { suites: candidates, taskIds: allTaskIds, picked: suitesPicked } = scope(filters, catalog);
|
|
202
209
|
const since = bound(filters.since, "since"), until = bound(filters.until, "until");
|
|
203
210
|
const picked = filters.grading ? pickGrading(store, filters.grading) : null;
|
|
204
211
|
// A view shows one judge: a picked grading setup's judge, for every task in scope.
|
|
205
212
|
const judge = picked ? setupJudge(picked, filters.judge, catalog.judges) : pickJudge(filters.judge, catalog.judges);
|
|
206
213
|
const pickedTasks = new Set(picked ? store.gradingSetupTaskIds(picked.hash) : []);
|
|
207
|
-
if (picked && !
|
|
208
|
-
throw new Error(`grading version ${picked.hash} is for task ${[...pickedTasks].join(", ")}, which is not in scope (${
|
|
214
|
+
if (picked && !allTaskIds.some((t) => pickedTasks.has(t))) {
|
|
215
|
+
throw new Error(`grading version ${picked.hash} is for task ${[...pickedTasks].join(", ")}, which is not in scope (${allTaskIds.join(", ")})`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 1. Gather each task that can be in view (every suite's, when the suites are not picked).
|
|
219
|
+
const gathered = new Map<string, Gathered>();
|
|
220
|
+
for (const taskId of allTaskIds) gathered.set(taskId, gatherTask(store, catalog.loadTask(taskId), judge, picked, pickedTasks, filters, since, until));
|
|
221
|
+
|
|
222
|
+
// 2. The suites in view: the picked ones, else the default set (DEC-019). A view of tasks has no suites.
|
|
223
|
+
let suites = candidates;
|
|
224
|
+
let coverage: Coverage | null = null;
|
|
225
|
+
if (!filters.tasks?.length) {
|
|
226
|
+
const completedBy = new Map(catalog.suites.map((s) => [s.id, completers(s, gathered)]));
|
|
227
|
+
if (!suitesPicked) {
|
|
228
|
+
const ids = new Set(defaultSuites(candidates.map((s) => s.id), completedBy));
|
|
229
|
+
suites = candidates.filter((s) => ids.has(s.id));
|
|
230
|
+
// The default set is the one rule, with no exception for a picked grading version: a view that must show its
|
|
231
|
+
// task picks that task's suite (the dashboard puts the suite pick in the address when it sets the grading pick).
|
|
232
|
+
if (picked && !suites.some((s) => s.tasks.some((t) => pickedTasks.has(t)))) {
|
|
233
|
+
const of = candidates.filter((s) => s.tasks.some((t) => pickedTasks.has(t))).map((s) => s.id);
|
|
234
|
+
throw new Error(`grading version ${picked.hash} is for task ${[...pickedTasks].join(", ")}, which is not in the default set of suites (${suites.map((s) => s.id).join(", ")}); pick its suite (${of.join(", ")}) or its task to see it`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const inView = new Set(suites.map((s) => s.id));
|
|
238
|
+
const ref = (s: Suite) => ({ id: s.id, title: s.title });
|
|
239
|
+
const both = (ids: Set<string>[]) => ids.length ? [...ids[0]!].filter((c) => ids.every((x) => x.has(c))).length : 0;
|
|
240
|
+
coverage = {
|
|
241
|
+
picked: suitesPicked,
|
|
242
|
+
covered: suites.map(ref),
|
|
243
|
+
leftOut: catalog.suites.filter((s) => !inView.has(s.id)).map((s) => {
|
|
244
|
+
if (suitesPicked) return { ...ref(s), completed: null, reason: "not-picked" as const };
|
|
245
|
+
const completed = completedBy.get(s.id)!.size;
|
|
246
|
+
return { ...ref(s), completed, reason: completed ? "too-few" as const : "none-completed" as const };
|
|
247
|
+
}),
|
|
248
|
+
complete: both(suites.map((s) => completedBy.get(s.id)!)),
|
|
249
|
+
};
|
|
209
250
|
}
|
|
251
|
+
const taskIds = filters.tasks?.length ? allTaskIds : [...new Set(suites.flatMap((s) => s.tasks))];
|
|
210
252
|
|
|
253
|
+
// 3. The view of those tasks.
|
|
211
254
|
const tasks: Record<string, ViewTask> = {};
|
|
212
255
|
const inView: ViewTrial[] = [];
|
|
213
256
|
const records = new Map<string, TrialRecord>(); // trial id → its row, for the config fields
|
|
214
257
|
const left = { otherGrading: new Counter(), otherTask: new Counter(), ungraded: new Counter() };
|
|
215
|
-
const
|
|
258
|
+
const versions: OtherTaskVersion[] = [];
|
|
216
259
|
const gradable: string[] = [];
|
|
217
|
-
|
|
218
260
|
for (const taskId of taskIds) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
tasks[taskId] = {
|
|
227
|
-
taskHash: tHash, gradingHash: gHash, gradingName: names.get(gHash)?.name ?? null, // null: today's grading version is not stored yet
|
|
228
|
-
label,
|
|
229
|
-
current: tHash === nowTask && gHash === nowGrading,
|
|
230
|
-
def: gHash === nowGrading ? snapshotOf(task) : taskDefinition(task, picked!, label),
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const rows = store.listTrials({ taskId }).filter((r) => ENDED.has(r.status) && inRange(r.startedAt, since, until) && matchesConfig(r, filters));
|
|
234
|
-
const same: TrialRecord[] = [];
|
|
235
|
-
for (const r of rows) {
|
|
236
|
-
if (r.taskHash === tHash) { same.push(r); continue; }
|
|
237
|
-
left.otherTask.add(r.configKey);
|
|
238
|
-
const key = `${taskId}\0${r.taskHash}`; // two tasks can share an assignment (task hash): each keeps its own entry
|
|
239
|
-
const v = otherVersions.get(key) ?? otherVersions.set(key, { version: otherVersion(store, taskId, r.taskHash, names), rows: [] }).get(key)!;
|
|
240
|
-
v.version.trials++;
|
|
241
|
-
v.rows.push(r);
|
|
242
|
-
}
|
|
243
|
-
for (const r of latestHumanOnly(same)) {
|
|
244
|
-
records.set(r.id, r);
|
|
245
|
-
if (r.status !== "done") { inView.push(viewTrial(r, null, [])); continue; } // failed, or copied the held-out original
|
|
246
|
-
const g = store.currentGrading(r.id, gHash);
|
|
247
|
-
if (g) { inView.push(viewTrial(r, g, store.listVerdicts(g.id))); continue; }
|
|
248
|
-
const other = store.listGradings(r.id).some((x) => x.status !== "grading");
|
|
249
|
-
(other ? left.otherGrading : left.ungraded).add(r.configKey);
|
|
250
|
-
gradable.push(r.id);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
for (const { version: v, rows } of otherVersions.values()) {
|
|
255
|
-
const shown = latestHumanOnly(rows).filter((r) => r.status === "done");
|
|
256
|
-
for (const g of v.gradingSetups) g.trials = shown.filter((r) => store.currentGrading(r.id, g.hash)?.status === "done").length;
|
|
257
|
-
v.link = linkTarget(v.gradingSetups, judge);
|
|
261
|
+
const g = gathered.get(taskId)!;
|
|
262
|
+
tasks[taskId] = g.task;
|
|
263
|
+
inView.push(...g.trials);
|
|
264
|
+
for (const r of g.records) records.set(r.id, r);
|
|
265
|
+
for (const k of ["otherGrading", "otherTask", "ungraded"] as const) for (const key of g.left[k]) left[k].add(key);
|
|
266
|
+
versions.push(...g.versions);
|
|
267
|
+
gradable.push(...g.gradable);
|
|
258
268
|
}
|
|
259
269
|
|
|
260
270
|
inView.sort((a, b) => cmp(a.taskId, b.taskId) || cmp(a.configKey, b.configKey) || cmp(a.id, b.id));
|
|
@@ -263,30 +273,98 @@ export function buildView(store: Store, filters: ViewFilters = {}, catalog: Cata
|
|
|
263
273
|
suiteIds: suites.map((s) => s.id), taskIds, judge, grading: picked?.hash ?? null, since, until,
|
|
264
274
|
...Object.fromEntries(CONFIG_FACETS.map((k) => [k, valueSet(filters[k] ?? [])] as const).filter(([, v]) => v.length)),
|
|
265
275
|
},
|
|
276
|
+
coverage,
|
|
266
277
|
tasks,
|
|
267
278
|
trials: inView,
|
|
268
279
|
summary: summarizeTrials(inView, records, tasks, suites),
|
|
269
280
|
leftOut: {
|
|
270
281
|
otherGrading: left.otherGrading.count(),
|
|
271
|
-
otherTask: { ...left.otherTask.count(), versions
|
|
282
|
+
otherTask: { ...left.otherTask.count(), versions },
|
|
272
283
|
ungraded: left.ungraded.count(),
|
|
273
284
|
gradable: gradable.sort(),
|
|
274
285
|
},
|
|
275
286
|
};
|
|
276
287
|
}
|
|
277
288
|
|
|
289
|
+
/** One task's part of a view: its definition in view, its trials in view, and its left-out trials by config key. */
|
|
290
|
+
interface Gathered {
|
|
291
|
+
task: ViewTask;
|
|
292
|
+
trials: ViewTrial[];
|
|
293
|
+
records: TrialRecord[];
|
|
294
|
+
left: { otherGrading: string[]; otherTask: string[]; ungraded: string[] };
|
|
295
|
+
versions: OtherTaskVersion[];
|
|
296
|
+
gradable: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function gatherTask(store: Store, task: Task, judge: JudgeConfig, picked: GradingSetupRecord | null, pickedTasks: Set<string>,
|
|
300
|
+
filters: ViewFilters, since: string | null, until: string | null): Gathered {
|
|
301
|
+
const taskId = task.id;
|
|
302
|
+
const nowTask = currentTaskHash(task), nowGrading = currentGradingHash(task, judge);
|
|
303
|
+
const names = versionNames(store, taskId);
|
|
304
|
+
const tHash = pickedTasks.has(taskId) ? picked!.taskHash : nowTask;
|
|
305
|
+
const gHash = pickedTasks.has(taskId) ? picked!.hash : nowGrading;
|
|
306
|
+
const stored = store.getTaskVersion(tHash);
|
|
307
|
+
const label = stored ? stored.label : task.version ?? null; // a stored task version keeps its label, even none
|
|
308
|
+
const out: Gathered = {
|
|
309
|
+
task: {
|
|
310
|
+
taskHash: tHash, gradingHash: gHash, gradingName: names.get(gHash)?.name ?? null, // null: today's grading version is not stored yet
|
|
311
|
+
label,
|
|
312
|
+
current: tHash === nowTask && gHash === nowGrading,
|
|
313
|
+
def: gHash === nowGrading ? snapshotOf(task) : taskDefinition(task, picked!, label),
|
|
314
|
+
},
|
|
315
|
+
trials: [], records: [], left: { otherGrading: [], otherTask: [], ungraded: [] }, versions: [], gradable: [],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const rows = store.listTrials({ taskId }).filter((r) => ENDED.has(r.status) && inRange(r.startedAt, since, until) && matchesConfig(r, filters));
|
|
319
|
+
const same: TrialRecord[] = [];
|
|
320
|
+
const otherVersions = new Map<string, { version: OtherTaskVersion; rows: TrialRecord[] }>(); // with its trials, to count per grading version
|
|
321
|
+
for (const r of rows) {
|
|
322
|
+
if (r.taskHash === tHash) { same.push(r); continue; }
|
|
323
|
+
out.left.otherTask.push(r.configKey);
|
|
324
|
+
const v = otherVersions.get(r.taskHash) ?? otherVersions.set(r.taskHash, { version: otherVersion(store, taskId, r.taskHash, names), rows: [] }).get(r.taskHash)!;
|
|
325
|
+
v.version.trials++;
|
|
326
|
+
v.rows.push(r);
|
|
327
|
+
}
|
|
328
|
+
for (const r of latestHumanOnly(same)) {
|
|
329
|
+
out.records.push(r);
|
|
330
|
+
if (r.status !== "done") { out.trials.push(viewTrial(r, null, [])); continue; } // failed, or copied the held-out original
|
|
331
|
+
const g = store.currentGrading(r.id, gHash);
|
|
332
|
+
if (g) { out.trials.push(viewTrial(r, g, store.listVerdicts(g.id))); continue; }
|
|
333
|
+
const other = store.listGradings(r.id).some((x) => x.status !== "grading");
|
|
334
|
+
(other ? out.left.otherGrading : out.left.ungraded).push(r.configKey);
|
|
335
|
+
out.gradable.push(r.id);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const { version: v, rows } of otherVersions.values()) {
|
|
339
|
+
const shown = latestHumanOnly(rows).filter((r) => r.status === "done");
|
|
340
|
+
for (const g of v.gradingSetups) g.trials = shown.filter((r) => store.currentGrading(r.id, g.hash)?.status === "done").length;
|
|
341
|
+
v.link = linkTarget(v.gradingSetups, judge);
|
|
342
|
+
out.versions.push(v);
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The configs (config keys) that have a valid score on every task of `suite` in what was gathered. */
|
|
348
|
+
function completers(suite: Suite, gathered: Map<string, Gathered>): Set<string> {
|
|
349
|
+
const per = suite.tasks.map((t) => new Set((gathered.get(t)?.trials ?? []).filter((x) => x.status === "done" && x.grades).map((x) => x.configKey)));
|
|
350
|
+
if (!per.length) return new Set();
|
|
351
|
+
return new Set([...per[0]!].filter((c) => per.every((p) => p.has(c))));
|
|
352
|
+
}
|
|
353
|
+
|
|
278
354
|
// ---- Selection ---------------------------------------------------------------------------------
|
|
279
355
|
|
|
280
356
|
const ENDED = new Set(["done", "failed", "invalid"]);
|
|
281
357
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
|
282
358
|
|
|
283
|
-
function scope(f: ViewFilters, catalog: Catalog): { suites: Suite[]; taskIds: string[] } {
|
|
284
|
-
if (f.tasks?.length) return { suites: [], taskIds: [...new Set(f.tasks)] };
|
|
359
|
+
function scope(f: ViewFilters, catalog: Catalog): { suites: Suite[]; taskIds: string[]; picked: boolean } {
|
|
360
|
+
if (f.tasks?.length) return { suites: [], taskIds: [...new Set(f.tasks)], picked: false };
|
|
285
361
|
const byId = new Map(catalog.suites.map((s) => [s.id, s]));
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
362
|
+
const picked = !!f.suites?.length;
|
|
363
|
+
// Picked suites are in catalog order, so a set of picks has one view whatever order it was given in.
|
|
364
|
+
const ids = new Set(f.suites ?? []);
|
|
365
|
+
for (const id of ids) if (!byId.has(id)) fail(`no suite ${id} (suites: ${[...byId.keys()].join(", ") || "none"})`);
|
|
366
|
+
const suites = picked ? catalog.suites.filter((s) => ids.has(s.id)) : catalog.suites;
|
|
367
|
+
return { suites, taskIds: [...new Set(suites.flatMap((s) => s.tasks))], picked };
|
|
290
368
|
}
|
|
291
369
|
|
|
292
370
|
function pickJudge(id: string | undefined, judges: JudgeConfig[]): JudgeConfig {
|