@giamme/forge 0.10.0 → 0.11.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/CHANGELOG.md +43 -0
- package/SKILL.md +4 -3
- package/VERSION +1 -1
- package/docs/artifacts.md +13 -1
- package/docs/commands.md +11 -5
- package/docs/dataset.md +13 -0
- package/docs/decomposed-runs.md +13 -3
- package/docs/forge-ui.md +8 -3
- package/docs/memory.md +4 -2
- package/docs/reviews.md +73 -2
- package/forge +16 -1
- package/package.json +1 -1
- package/references/dataset.md +86 -4
- package/references/decompose.md +24 -6
- package/references/harnesses.md +43 -1
- package/references/ui.md +4 -1
- package/scripts/forge-config.py +5 -2
- package/scripts/forge-control.py +4 -0
- package/scripts/forge-dispatch.sh +235 -121
- package/scripts/forge-issues.py +345 -17
- package/scripts/forge-lib-plans.sh +6 -2
- package/scripts/forge-lib-review.sh +158 -3
- package/scripts/forge-lib-task.sh +132 -14
- package/scripts/forge-parallel.sh +96 -11
- package/scripts/forge-prompt.py +61 -4
- package/scripts/forge-route.py +72 -12
- package/scripts/forge-runtime.py +47 -1
- package/scripts/forge-ui.py +8 -4
- package/scripts/forge_dataset/__init__.py +7 -2
- package/scripts/forge_dataset/cli.py +44 -1
- package/scripts/forge_dataset/export.py +36 -3
- package/scripts/forge_dataset/forecast.py +355 -0
- package/scripts/forge_dataset/harvest.py +23 -2
- package/scripts/forge_dataset/importer.py +20 -8
- package/scripts/forge_dataset/reviewers.py +145 -0
- package/scripts/forge_dataset/routing.py +1 -0
- package/scripts/forge_jev/__init__.py +7 -3
- package/scripts/forge_registry.py +15 -10
- package/ui/README.md +5 -4
- package/ui/docs/api.md +88 -23
- package/ui/forge_ui/control.py +26 -5
- package/ui/forge_ui/reader.py +184 -11
- package/ui/forge_ui/static/app.js +24 -10
- package/ui/forge_ui/static/cloud3d.js +4 -1
- package/ui/forge_ui/static/cloudmodel.js +1 -1
- package/ui/forge_ui/static/cloudui.js +13 -4
- package/ui/forge_ui/static/findingsview.js +65 -14
- package/ui/forge_ui/static/graph.js +6 -5
- package/ui/forge_ui/static/leaderboard.js +18 -0
- package/ui/forge_ui/static/modelstats.js +19 -1
- package/ui/forge_ui/static/panel.js +52 -10
- package/ui/forge_ui/static/runmodel.js +128 -13
- package/ui/forge_ui/static/scene3d.js +32 -15
- package/ui/forge_ui/static/settings.js +48 -12
- package/ui/forge_ui/static/style.css +7 -3
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const MAX_FPS = 25, MAX_LABELS = 5, MAX_LABEL = 50, MAX_REASON = 500;
|
|
17
17
|
const list = v => Array.isArray(v) ? v : [];
|
|
18
18
|
const SEVERITIES = ['P0', 'P1', 'P2', 'P3'];
|
|
19
|
-
const ORIGINS = ['tolerated', 'review', 'plausible'];
|
|
19
|
+
const ORIGINS = ['tolerated', 'accepted', 'review', 'plausible'];
|
|
20
20
|
const STATUSES = ['open', 'filed', 'fixed', 'dismissed'];
|
|
21
21
|
|
|
22
22
|
/* ---------- pure ---------- */
|
|
@@ -64,6 +64,20 @@
|
|
|
64
64
|
const note = n > MAX_FPS ? `${n} selected: at most ${MAX_FPS} per action` : '';
|
|
65
65
|
return { fps, count: n, enabled: n >= 1 && n <= MAX_FPS, note };
|
|
66
66
|
}
|
|
67
|
+
// Fingerprints "Select shown open" adds. `rows` are the rows the filters leave shown. The cap is how many of
|
|
68
|
+
// those are already selected and open — the same rows File and Dismiss take — so a selection a filter hides stays
|
|
69
|
+
// selected and does not count. A shown row that is not open is not added.
|
|
70
|
+
function selectShownOpen(rows, selected) {
|
|
71
|
+
const chosen = new Set(selected instanceof Set || Array.isArray(selected) ? selected : []);
|
|
72
|
+
const add = [];
|
|
73
|
+
for (const r of list(rows)) {
|
|
74
|
+
if (!selectable('file', r) || chosen.has(r.fp)) continue;
|
|
75
|
+
if (selection('file', rows, chosen).count >= MAX_FPS) break;
|
|
76
|
+
chosen.add(r.fp);
|
|
77
|
+
add.push(r.fp);
|
|
78
|
+
}
|
|
79
|
+
return add;
|
|
80
|
+
}
|
|
67
81
|
const bit = v => v ? '1' : '0';
|
|
68
82
|
// The GET path: `o` = { scope, offline, plausible, dismissed, only (fingerprints) }.
|
|
69
83
|
function issuesUrl(key, o) {
|
|
@@ -104,8 +118,27 @@
|
|
|
104
118
|
return { kind: 'note', text: c && c.note ? c.note : 'no detail' };
|
|
105
119
|
});
|
|
106
120
|
}
|
|
121
|
+
// The CLI's `notes` as lines for the dialog, only when the POST created nothing: a bare "0 created" says no reason, the
|
|
122
|
+
// notes ("--only fp: no such open finding") do. When something was created the notes are not about the dialog.
|
|
123
|
+
function emptyNotes(created, notes) {
|
|
124
|
+
return list(created).some(c => c && c.url) ? [] : list(notes).map(n => String(n)).filter(Boolean);
|
|
125
|
+
}
|
|
107
126
|
// An untrusted URL becomes a link only when it is a plain https address.
|
|
108
127
|
const safeUrl = u => typeof u === 'string' && /^https:\/\/[^\s"'<>]+$/.test(u) ? u : null;
|
|
128
|
+
// The marker for a finding whose whole-run review is outdated, or null. The lamp is the hollow unlit
|
|
129
|
+
// `.st-outdated`. `title` is the same sentence the issue body adds, ids cut after 5, so a row can show it
|
|
130
|
+
// without building HTML. A current review, and any finding that is not from a whole-run review, has none.
|
|
131
|
+
function reviewMarker(row) {
|
|
132
|
+
const review = row && row.review;
|
|
133
|
+
if (!review || review.outdated !== true) return null;
|
|
134
|
+
const ids = list(review.merged_since).map(id => String(id));
|
|
135
|
+
const shown = ids.slice(0, 5), extra = ids.length - shown.length;
|
|
136
|
+
const names = shown.join(', ') + (extra > 0 ? ', +' + extra + ' more' : '');
|
|
137
|
+
const n = ids.length;
|
|
138
|
+
const sentence = `The review is outdated: ${n} ${n === 1 ? 'task' : 'tasks'} merged into the integration branch after it`
|
|
139
|
+
+ (names ? ` (${names})` : '') + ', so check whether they fixed it first.';
|
|
140
|
+
return { lamp: 'st-outdated', label: 'outdated review', title: sentence, sentence };
|
|
141
|
+
}
|
|
109
142
|
|
|
110
143
|
/* ---------- DOM ---------- */
|
|
111
144
|
function render(host, ctx) {
|
|
@@ -162,7 +195,7 @@
|
|
|
162
195
|
const dismissKey = key_('Dismiss', undefined, () => openDismiss());
|
|
163
196
|
const undismissKey = key_('Undismiss', undefined, () => doDismiss(true, ''));
|
|
164
197
|
const allKey = key_('Select shown open', undefined, () => {
|
|
165
|
-
for (const
|
|
198
|
+
for (const fp of selectShownOpen(shown(), s.selected)) s.selected.add(fp);
|
|
166
199
|
drawTable(); syncActions();
|
|
167
200
|
});
|
|
168
201
|
const noneKey = key_('Clear selection', undefined, () => { s.selected.clear(); drawTable(); syncActions(); });
|
|
@@ -179,6 +212,9 @@
|
|
|
179
212
|
host.append(dlg);
|
|
180
213
|
|
|
181
214
|
const shown = () => filterRows(s.rows, { ...s.filter, dismissed: s.dismissed });
|
|
215
|
+
// What an action takes: the selected rows the filters leave visible. Rows hidden by a filter stay in s.selected, so
|
|
216
|
+
// they come back with the filter, but no button counts them and no action reaches them.
|
|
217
|
+
const picked = action => selection(action, shown(), s.selected);
|
|
182
218
|
function syncScope() {
|
|
183
219
|
for (const [v, b] of Object.entries(scopeKeys)) b.setAttribute('aria-pressed', String(s.scope === v));
|
|
184
220
|
lede.textContent = s.scope === 'run' ? `Findings this run left unfixed${ctx.run && ctx.run.goal ? ': ' + String(ctx.run.goal).split('\n')[0] : ''}.`
|
|
@@ -187,17 +223,27 @@
|
|
|
187
223
|
function syncActions() {
|
|
188
224
|
actions.hidden = !s.writes;
|
|
189
225
|
if (!s.writes) return;
|
|
190
|
-
const f =
|
|
226
|
+
const f = picked('file'), d = picked('dismiss'), u = picked('undismiss');
|
|
191
227
|
fileKey.textContent = f.count ? `File ${f.count} issue${f.count === 1 ? '' : 's'}` : 'File issues';
|
|
192
228
|
fileKey.disabled = !f.enabled || s.busy; dismissKey.disabled = !d.enabled || s.busy;
|
|
193
229
|
undismissKey.textContent = u.count ? `Undismiss ${u.count}` : 'Undismiss'; undismissKey.disabled = !u.enabled || s.busy;
|
|
194
|
-
actionNote.textContent = f.note || d.note || u.note || (
|
|
230
|
+
actionNote.textContent = f.note || d.note || u.note || (f.count + u.count ? `${f.count} open, ${u.count} dismissed selected` : '');
|
|
195
231
|
}
|
|
196
232
|
function setLamp(cls, text) {
|
|
197
233
|
lamp.replaceChildren(el('i', undefined, 'lamp ' + cls), el('span', text));
|
|
198
234
|
}
|
|
199
235
|
|
|
200
236
|
// cells
|
|
237
|
+
const originCell = r => {
|
|
238
|
+
const marker = reviewMarker(r);
|
|
239
|
+
if (!marker) return r.origin || '—';
|
|
240
|
+
const box = el('span', undefined, 'fnd-status');
|
|
241
|
+
const lamp = el('i', undefined, 'lamp ' + marker.lamp);
|
|
242
|
+
lamp.title = marker.title;
|
|
243
|
+
box.title = marker.title;
|
|
244
|
+
box.append(lamp, el('span', r.origin || '—'), el('span', marker.label));
|
|
245
|
+
return box;
|
|
246
|
+
};
|
|
201
247
|
const statusCell = r => {
|
|
202
248
|
const st = statusOf(r), box = el('span', undefined, 'fnd-status fnd-' + st);
|
|
203
249
|
box.append(el('i', undefined, 'lamp ' + (st === 'filed' ? 'fnd-amber' : st === 'fixed' ? 'st-pass' : 'lamp-off')));
|
|
@@ -223,6 +269,8 @@
|
|
|
223
269
|
more.append(el('span', '▸', 'fnd-caret'));
|
|
224
270
|
const detail = el('div', undefined, 'fnd-detail'); detail.hidden = true;
|
|
225
271
|
detail.append(el('pre', r.text || r.summary || '', 'fnd-text'));
|
|
272
|
+
const marker = reviewMarker(r);
|
|
273
|
+
if (marker) detail.append(el('p', marker.sentence, 'muted'));
|
|
226
274
|
const meta = el('dl', undefined, 'fnd-meta');
|
|
227
275
|
for (const [k, v] of [['Reviewer', r.reviewer], ['Commit', r.commit], ['Fingerprint', r.fp], ['Plan', r.plan_dir || r.plan],
|
|
228
276
|
['Dismissed', r.dismissed ? `${r.dismissed.at || ''} ${r.dismissed.reason || ''}`.trim() : null]]) {
|
|
@@ -245,7 +293,7 @@
|
|
|
245
293
|
cols.push(
|
|
246
294
|
{ id: 'severity', label: 'Severity', value: r => SEVERITIES.indexOf(r.severity), cell: sevCell },
|
|
247
295
|
{ id: 'confidence', label: 'Confidence', value: r => r.confidence, cell: r => r.confidence || '—' },
|
|
248
|
-
{ id: 'origin', label: 'Origin', value: r => r.origin, cell:
|
|
296
|
+
{ id: 'origin', label: 'Origin', value: r => r.origin, cell: originCell },
|
|
249
297
|
{ id: 'task', label: 'Task', value: r => r.task, mono: true, cell: r => r.task || 'whole run' });
|
|
250
298
|
if (s.scope === 'repo') cols.push({ id: 'plan', label: 'Plan', value: r => r.plan, mono: true, cell: r => r.plan || '—' });
|
|
251
299
|
cols.push(
|
|
@@ -304,7 +352,7 @@
|
|
|
304
352
|
// dismiss: an optional reason, then the call
|
|
305
353
|
async function openDismiss() {
|
|
306
354
|
reasonHost.replaceChildren();
|
|
307
|
-
const sel =
|
|
355
|
+
const sel = picked('dismiss');
|
|
308
356
|
if (!sel.enabled) return;
|
|
309
357
|
const l = el('label', undefined, 'fnd-field fnd-reason-field'), input = el('input');
|
|
310
358
|
input.type = 'text'; input.maxLength = MAX_REASON; input.placeholder = 'why (optional)';
|
|
@@ -319,7 +367,7 @@
|
|
|
319
367
|
input.focus();
|
|
320
368
|
}
|
|
321
369
|
async function doDismiss(undo, reason) {
|
|
322
|
-
const sel =
|
|
370
|
+
const sel = picked(undo ? 'undismiss' : 'dismiss');
|
|
323
371
|
if (!sel.enabled || s.busy) return;
|
|
324
372
|
s.busy = true; syncActions(); outcomeHost.replaceChildren();
|
|
325
373
|
const res = await UI.post(dismissPath(key), dismissBody(sel.fps, reason, undo));
|
|
@@ -330,17 +378,19 @@
|
|
|
330
378
|
|
|
331
379
|
// filing: drafts for exactly the selection, a preview, then the POST
|
|
332
380
|
async function openFile() {
|
|
333
|
-
const sel =
|
|
381
|
+
const sel = picked('file');
|
|
334
382
|
if (!sel.enabled || s.busy) return;
|
|
335
383
|
s.busy = true; syncActions(); outcomeHost.replaceChildren();
|
|
336
384
|
setLamp('st-running', 'preparing the issue drafts…');
|
|
337
|
-
|
|
385
|
+
// The scope and the toggle stay usable while the drafts are made: the preview files what these drafts were made for.
|
|
386
|
+
const scope = s.scope, plausible = s.plausible;
|
|
387
|
+
const res = await UI.get(issuesUrl(key, { scope, offline: false, plausible, only: sel.fps }));
|
|
338
388
|
s.busy = false; syncActions();
|
|
339
389
|
if (!res.ok) { setLamp('st-fail', 'could not prepare the drafts'); UI.outcome(outcomeHost, res); return; }
|
|
340
390
|
setLamp('st-pass', 'drafts ready');
|
|
341
|
-
preview(sel.fps, list(res.data.drafts));
|
|
391
|
+
preview(sel.fps, list(res.data.drafts), { scope, plausible });
|
|
342
392
|
}
|
|
343
|
-
function preview(fps, drafts) {
|
|
393
|
+
function preview(fps, drafts, made) {
|
|
344
394
|
dlg.replaceChildren();
|
|
345
395
|
dlg.append(el('h2', 'File issues on GitHub'));
|
|
346
396
|
const warn = el('p', undefined, 'fnd-warn');
|
|
@@ -373,7 +423,7 @@
|
|
|
373
423
|
labelNote.hidden = !parsed.error; labelNote.textContent = parsed.error || '';
|
|
374
424
|
if (parsed.error) return;
|
|
375
425
|
go.disabled = true; cancel.disabled = true; labels.disabled = true; results.replaceChildren(); posting = true;
|
|
376
|
-
const res = await UI.post(issuesPath(key), fileBody(
|
|
426
|
+
const res = await UI.post(issuesPath(key), fileBody(made.scope, fps, made.plausible, parsed.labels));
|
|
377
427
|
posting = false;
|
|
378
428
|
if (!res.ok) { go.disabled = false; cancel.disabled = false; labels.disabled = false; UI.outcome(results, res); return; }
|
|
379
429
|
const lines = resultLines(res.data.created), ul = el('ul', undefined, 'fnd-created');
|
|
@@ -384,6 +434,7 @@
|
|
|
384
434
|
ul.append(li);
|
|
385
435
|
}
|
|
386
436
|
results.replaceChildren(el('p', `${lines.filter(l => l.kind === 'created').length} created`, 'fnd-done'), ul);
|
|
437
|
+
for (const n of emptyNotes(res.data.created, res.data.notes)) results.append(el('p', n, 'muted fnd-result-note'));
|
|
387
438
|
go.remove(); cancel.textContent = 'Close'; cancel.disabled = false;
|
|
388
439
|
for (const fp of fps) s.selected.delete(fp);
|
|
389
440
|
load();
|
|
@@ -407,6 +458,6 @@
|
|
|
407
458
|
return { reload: load };
|
|
408
459
|
}
|
|
409
460
|
|
|
410
|
-
return { render, statusOf, filterRows, mergeFiled, selectable, selection, issuesUrl, issuesPath, dismissPath, fileBody, dismissBody,
|
|
411
|
-
parseLabels, resultLines, safeUrl, MAX_FPS };
|
|
461
|
+
return { render, statusOf, filterRows, mergeFiled, selectable, selection, selectShownOpen, issuesUrl, issuesPath, dismissPath, fileBody, dismissBody,
|
|
462
|
+
parseLabels, resultLines, emptyNotes, safeUrl, reviewMarker, ORIGINS, MAX_FPS };
|
|
412
463
|
});
|
|
@@ -201,8 +201,9 @@
|
|
|
201
201
|
if (drawn[t.id]) return;
|
|
202
202
|
drawn[t.id] = true;
|
|
203
203
|
var p = pos[t.id];
|
|
204
|
-
var status =
|
|
205
|
-
var
|
|
204
|
+
var status = window.ForgeRunModel.taskStatus(t);
|
|
205
|
+
var verdict = window.ForgeRunModel.taskVerdict(t);
|
|
206
|
+
var stClass = 'st-' + String(status || 'pending').toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
206
207
|
var selected = options.selected === t.id;
|
|
207
208
|
var id = String(t.id);
|
|
208
209
|
|
|
@@ -217,7 +218,7 @@
|
|
|
217
218
|
|
|
218
219
|
var tip = [str(t.title, id), 'status: ' + status,
|
|
219
220
|
'dwarf: ' + str(t.dwarf, '—') + ' / qa: ' + str(t.qa, '—')];
|
|
220
|
-
if (
|
|
221
|
+
if (verdict.text) tip.push('verdict: ' + verdict.text);
|
|
221
222
|
if (t.explored) tip.push('explored');
|
|
222
223
|
var title = el('title', null, g);
|
|
223
224
|
title.textContent = tip.join('\n');
|
|
@@ -238,8 +239,8 @@
|
|
|
238
239
|
x: 12, y: 36, 'font-size': 12, fill: 'currentColor', 'fill-opacity': 0.75
|
|
239
240
|
});
|
|
240
241
|
|
|
241
|
-
if (
|
|
242
|
-
var pass =
|
|
242
|
+
if (verdict.mark === 'PASS' || verdict.mark === 'FAIL') {
|
|
243
|
+
var pass = verdict.mark === 'PASS';
|
|
243
244
|
// a drawn tick or cross, in the verdict's colour
|
|
244
245
|
el('path', { 'class': 'fg-verdict' + (pass ? ' fg-pass' : ''), d: pass ? 'M' + (NODE_W - 20) + ' 12l4 4 7-8' : 'M' + (NODE_W - 19) + ' 8l8 8m0-8l-8 8',
|
|
245
246
|
fill: 'none', 'stroke-width': 1.8, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, g);
|
|
@@ -149,6 +149,7 @@ window.ForgeLeaderboard = (() => {
|
|
|
149
149
|
['Verdicts', '', 'PASS and FAIL verdicts over every review'],
|
|
150
150
|
...MS.SEVERITIES.map(s => [s, 'lb-num', SEV_TITLE(s)]),
|
|
151
151
|
['Shadow agreement', 'lb-num', 'How often this model\'s shadow verdict matched the primary reviewer\'s (both PASS or FAIL)'],
|
|
152
|
+
['Did not hold', 'lb-num', 'Of the work this model PASSed that was integrated, the share later reverted or with under 80% of its lines kept (forge dataset reviewers, every run in the dataset: the filters do not apply). Advisory: routing never reads it'],
|
|
152
153
|
['Median time', 'lb-num', 'Median duration of a review'],
|
|
153
154
|
['Tokens in / out', 'lb-num', 'Summed over the reviews that recorded tokens'],
|
|
154
155
|
];
|
|
@@ -182,7 +183,23 @@ window.ForgeLeaderboard = (() => {
|
|
|
182
183
|
td.title = `${a.k} of ${a.n} shadow verdicts matched the primary's; 95% Wilson interval ${pct(a.lo)}–${pct(a.hi)}${a.unknown ? ` · ${a.unknown} without two comparable verdicts` : ''}`;
|
|
183
184
|
return td;
|
|
184
185
|
}
|
|
186
|
+
function heldCell(h) {
|
|
187
|
+
const days = h.horizon ? ` ${h.horizon} days after integrate` : '';
|
|
188
|
+
if (h.state === 'unavailable') return numCell('—', 'The run dataset could not be read (forge dataset reviewers)');
|
|
189
|
+
if (h.state === 'none') return numCell('—', `No work this model PASSed has a survival outcome${days} yet (forge dataset outcomes records them)`);
|
|
190
|
+
const detail = `${h.reverted} reverted, ${h.rewritten} rewritten, of ${h.n} PASSed and integrated${days}`;
|
|
191
|
+
if (h.state === 'few') {
|
|
192
|
+
const td = numCell('not enough data', `${detail}; a rate needs ${h.need || 'more'} outcomes`);
|
|
193
|
+
td.classList.add('muted');
|
|
194
|
+
return td;
|
|
195
|
+
}
|
|
196
|
+
const td = el('td', undefined, 'lb-num');
|
|
197
|
+
td.append(el('strong', pct(h.p)), document.createTextNode(' '), el('span', `${h.k}/${h.n}`, 'muted'));
|
|
198
|
+
td.title = `${detail}; 95% Wilson interval ${pct(h.lo)}–${pct(h.hi)}`;
|
|
199
|
+
return td;
|
|
200
|
+
}
|
|
185
201
|
function reviewerTable(rows, ctx) {
|
|
202
|
+
const held = r => MS.heldUp(ctx.reviewerOutcomes, r.alias);
|
|
186
203
|
const columns = colsOf(REV_COLS, [
|
|
187
204
|
{ id: 'model', value: r => r.alias, cell: r => modelCell(r.alias, ctx, '') },
|
|
188
205
|
{ id: 'primary', value: r => r.reviews.primary, cell: r => numCell(r.reviews.primary) },
|
|
@@ -191,6 +208,7 @@ window.ForgeLeaderboard = (() => {
|
|
|
191
208
|
{ id: 'verdicts', value: r => { const v = r.verdicts, t = v.PASS + v.FAIL + v.other; return t ? v.PASS / t : null; }, cell: r => verdictCell(r.verdicts) },
|
|
192
209
|
...MS.SEVERITIES.map(sv => ({ id: 'sev-' + sv, value: r => r.findings[sv], cell: r => findingCell(r, sv) })),
|
|
193
210
|
{ id: 'agreement', value: r => r.agreement.n ? r.agreement.p : null, cell: r => agreementCell(r.agreement) },
|
|
211
|
+
{ id: 'held', value: r => { const h = held(r); return h.state === 'rate' ? h.p : null; }, cell: r => heldCell(held(r)) },
|
|
194
212
|
{ id: 'time', value: r => num(r.medianDurationS), cell: r => numCell(dur(r.medianDurationS)) },
|
|
195
213
|
{ id: 'tokens', value: r => (r.tokens.attempts ?? r.tokens.reviews) ? (r.tokens.input || 0) + (r.tokens.output || 0) : null, cell: r => tokenCell(r.tokens, r.reviews.total, 'review') },
|
|
196
214
|
]);
|
|
@@ -245,5 +245,23 @@
|
|
|
245
245
|
return { implementers: imp.rows, reviewers: rev.rows, byDifficulty, totals, caveats, filter: f };
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
// How the work a reviewer PASSed held up, from GET /api/models/reviewers (`forge dataset reviewers --json`):
|
|
249
|
+
// { state: 'unavailable' } without a report, 'none' when the alias has no landed outcome, 'few' below the
|
|
250
|
+
// report's minimum (no rate is shown then), else 'rate' with the share that did not hold and its interval.
|
|
251
|
+
function heldUp(report, alias) {
|
|
252
|
+
const r = obj(report);
|
|
253
|
+
if (!r || !Array.isArray(r.reviewers)) return { state: 'unavailable' };
|
|
254
|
+
const horizon = num(r.horizon_days), row = r.reviewers.find(x => obj(x) && x.reviewer === alias);
|
|
255
|
+
if (!row) return { state: 'none', horizon };
|
|
256
|
+
const n = num(row.landed), k = num(row.did_not_hold);
|
|
257
|
+
if (n === null || k === null) return { state: 'unavailable' };
|
|
258
|
+
const base = { n, k, reverted: num(row.reverted) || 0, rewritten: num(row.rewritten) || 0, horizon };
|
|
259
|
+
const ci = list(row.ci);
|
|
260
|
+
if (row.enough !== true || num(row.rate) === null || num(ci[0]) === null || num(ci[1]) === null) {
|
|
261
|
+
return { state: 'few', need: num(r.min_landed), ...base };
|
|
262
|
+
}
|
|
263
|
+
return { state: 'rate', p: row.rate, lo: ci[0], hi: ci[1], ...base };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { DIFFS, KINDS, SEVERITIES, DEFAULT_MIN_N, wilson, emptyFilter, normFilter, taskMatch, options, leaderboard, heldUp };
|
|
249
267
|
});
|
|
@@ -9,7 +9,7 @@ window.ForgePanel = (() => {
|
|
|
9
9
|
const num = v => typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
10
10
|
const display = v => v === null || v === undefined || v === '' ? '—' : typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v);
|
|
11
11
|
const STATUSES = new Set(['merged', 'pass', 'running', 'pending', 'fail', 'error', 'infra', 'timeout', 'unknown', 'invalidated', 'blocked',
|
|
12
|
-
'interrupted', 'nochanges', 'conflict', 'accepted', 'none']);
|
|
12
|
+
'interrupted', 'nochanges', 'conflict', 'accepted', 'none', 'outdated']);
|
|
13
13
|
const cache = new Map(); // path → {data, error, at}
|
|
14
14
|
function el(tag, text, className) {
|
|
15
15
|
const node = document.createElement(tag);
|
|
@@ -51,6 +51,7 @@ window.ForgePanel = (() => {
|
|
|
51
51
|
function section(parent, title) { const s = el('section', undefined, 'insp-section'); s.append(el('h3', title)); parent.append(s); return s; }
|
|
52
52
|
function details(parent, title, value, key, { open = false, cls = '' } = {}) {
|
|
53
53
|
const d = el('details', undefined, cls); d.dataset.keep = key; if (open) d.open = true;
|
|
54
|
+
if (key && String(key).endsWith('known_issues')) d.id = key;
|
|
54
55
|
const pre = el('pre', value); pre.dataset.scroll = key + '-text';
|
|
55
56
|
d.append(el('summary', title), pre); parent.append(d); return d;
|
|
56
57
|
}
|
|
@@ -60,6 +61,21 @@ window.ForgePanel = (() => {
|
|
|
60
61
|
return a;
|
|
61
62
|
}
|
|
62
63
|
function muted(parent, text) { parent.append(el('p', text, 'muted')); }
|
|
64
|
+
// The acceptance chip is a key to the Known issues text in this panel, not another route.
|
|
65
|
+
function acceptedChip(task) {
|
|
66
|
+
const over = task.accepted_over === 'FAIL' || task.accepted_over === 'UNKNOWN' ? task.accepted_over : null;
|
|
67
|
+
const id = 'task-' + task.id + '-known_issues';
|
|
68
|
+
const a = el('a', over ? 'accepted over ' + over : 'accepted', 'chip st-accepted');
|
|
69
|
+
a.href = '#' + id;
|
|
70
|
+
a.addEventListener('click', event => {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
const node = document.getElementById(id);
|
|
73
|
+
if (!node) return;
|
|
74
|
+
node.open = true;
|
|
75
|
+
node.scrollIntoView({ block: 'nearest' });
|
|
76
|
+
});
|
|
77
|
+
return a;
|
|
78
|
+
}
|
|
63
79
|
// who decided an escalation: policy, a human's retry --dwarf (retry) or a pin; older rows read as policy
|
|
64
80
|
const sourcePlate = source => el('span', source || 'policy', 'plate src-plate src-' + String(source || 'policy').replace(/[^a-z]/g, ''));
|
|
65
81
|
// the write controls of a plan run (task undefined) or of one of its tasks; a page without controls.js says it is read-only
|
|
@@ -218,11 +234,23 @@ window.ForgePanel = (() => {
|
|
|
218
234
|
}
|
|
219
235
|
const f = section(host, 'Finish');
|
|
220
236
|
const F = RM.finishState(run);
|
|
221
|
-
if (run.kind === 'solo')
|
|
237
|
+
if (run.kind === 'solo') {
|
|
238
|
+
const soloTask = list(run.tasks)[0];
|
|
239
|
+
const verdict = soloTask ? RM.taskVerdict(soloTask) : { text: null, status: 'unknown' };
|
|
240
|
+
fields(f, [['Verdict', chip(verdict.text, verdict.status)]]);
|
|
241
|
+
}
|
|
222
242
|
else {
|
|
243
|
+
const finalSel = { kind: 'final' }, openFinal = () => ctx.onSelect(finalSel);
|
|
244
|
+
let finalValue;
|
|
245
|
+
if (F.final.status === 'OUTDATED') {
|
|
246
|
+
finalValue = el('a'); finalValue.href = ctx.hash(finalSel);
|
|
247
|
+
finalValue.addEventListener('click', e => { e.preventDefault(); openFinal(); });
|
|
248
|
+
finalValue.append(chip('OUTDATED'));
|
|
249
|
+
} else finalValue = link(F.final.status === 'NONE' ? 'not requested' : F.final.status, ctx.hash(finalSel), openFinal);
|
|
223
250
|
fields(f, [['Integration', link(`${F.integration.merged}/${F.integration.total} merged`, ctx.hash({ kind: 'integration' }), () => ctx.onSelect({ kind: 'integration' }))],
|
|
224
251
|
['Verification', link(F.verification.status, ctx.hash({ kind: 'verification' }), () => ctx.onSelect({ kind: 'verification' }))],
|
|
225
|
-
['Final review',
|
|
252
|
+
['Final review', finalValue]]);
|
|
253
|
+
if (F.final.status === 'OUTDATED') muted(f, RM.reviewSinceLine(F.final));
|
|
226
254
|
if (F.final.findings) findingCounts(f, F.final.findings);
|
|
227
255
|
}
|
|
228
256
|
if (list(run.deferrals).length) details(host, `Deferred tasks (${run.deferrals.length})`, run.deferrals.join('\n'), 'deferrals');
|
|
@@ -236,10 +264,13 @@ window.ForgePanel = (() => {
|
|
|
236
264
|
const head = el('div', undefined, 'insp-head');
|
|
237
265
|
head.append(el('h2', solo ? (task.title || 'Solo run') : `${task.id} · ${task.title || 'Untitled task'}`), el('div', solo ? 'solo task' : `task · wave ${display(task.wave)}`, 'insp-kind'));
|
|
238
266
|
const chips = el('div', undefined, 'chips');
|
|
239
|
-
chips.append(
|
|
240
|
-
|
|
267
|
+
if (RM.taskStatus(task) === 'ACCEPTED') chips.append(acceptedChip(task));
|
|
268
|
+
else {
|
|
269
|
+
chips.append(chip(RM.taskStatus(task)));
|
|
270
|
+
if (task.accepted === true) chips.append(acceptedChip(task));
|
|
271
|
+
else if (task.verdict) chips.append(chip('review ' + task.verdict, task.verdict.toLowerCase()));
|
|
272
|
+
}
|
|
241
273
|
if (task.explored) chips.append(el('span', '* explored', 'tag'));
|
|
242
|
-
if (task.accepted_by) chips.append(chip('accepted over FAIL', 'nochanges'));
|
|
243
274
|
head.append(chips);
|
|
244
275
|
const dl = diffLink(ctx, solo ? 'View the diff →' : 'View its diff, findings pinned to their lines →', { kind: 'task', task: task.id, source: 'current' }); if (dl) head.append(dl);
|
|
245
276
|
host.append(head);
|
|
@@ -270,7 +301,7 @@ window.ForgePanel = (() => {
|
|
|
270
301
|
for (const z of tp.zones) {
|
|
271
302
|
const box = el('div', undefined, 'attempt');
|
|
272
303
|
const row = list(task.attempts).filter(a => a.attempt === z.attempt).pop();
|
|
273
|
-
const h = el('div', undefined, 'attempt-head'); h.append(el('strong', z.name.toLowerCase())); if (row) h.append(chip(row.status), el('span', time(row.at), 'muted'));
|
|
304
|
+
const h = el('div', undefined, 'attempt-head'); h.append(el('strong', z.name.toLowerCase())); if (row) h.append(chip(RM.attemptStatus(task, row.attempt, row.status)), el('span', time(row.at), 'muted'));
|
|
274
305
|
box.append(h);
|
|
275
306
|
for (const pid of z.parts) {
|
|
276
307
|
const p = tp.parts.find(x => x.id === pid); if (!p || p.planned) continue;
|
|
@@ -329,7 +360,8 @@ window.ForgePanel = (() => {
|
|
|
329
360
|
const cs = section(host, 'Clef predictions');
|
|
330
361
|
for (const [label, p] of preds) {
|
|
331
362
|
const n = Number(String(label).match(/\d+/) || 1), outcome = list(task.attempts).filter(a => a.attempt === n).pop();
|
|
332
|
-
|
|
363
|
+
const happened = outcome ? RM.attemptStatus(task, outcome.attempt, outcome.status) : 'not finished';
|
|
364
|
+
fields(cs, [[label, `p(pass) = ${display(p.p)} → ${happened}`], ['Assignment', p.assignment ? plates([p.assignment.dwarf, p.assignment.qa]) : '—']]);
|
|
333
365
|
}
|
|
334
366
|
}
|
|
335
367
|
// the prompts and the final messages
|
|
@@ -338,10 +370,18 @@ window.ForgePanel = (() => {
|
|
|
338
370
|
else {
|
|
339
371
|
const t = detail.data.texts || {};
|
|
340
372
|
for (const [title, key] of [['Requirements (prompt.md)', 'prompt'], ['Intended approach', 'approach'], ['Retry findings', 'retry_findings'],
|
|
341
|
-
['Dwarf prompt as assembled (dwarf.input)', 'dwarf_input'], ['Review prompt as assembled (qa.input)', 'qa_input']
|
|
373
|
+
['Dwarf prompt as assembled (dwarf.input)', 'dwarf_input'], ['Review prompt as assembled (qa.input)', 'qa_input']]) {
|
|
342
374
|
if (t[key] != null) details(ps, title, t[key], `task-${task.id}-${key}`);
|
|
343
375
|
}
|
|
344
376
|
}
|
|
377
|
+
// Known issues is the text the acceptance chip opens. Accepted by keeps the reason, who, and when.
|
|
378
|
+
const texts = detail.data && detail.data.texts || null;
|
|
379
|
+
if (task.accepted === true || (texts && texts.known_issues != null)) {
|
|
380
|
+
const known = texts ? texts.known_issues : null;
|
|
381
|
+
details(ps, 'Known issues', known != null ? known : (detail.data ? '—' : 'Loading…'), `task-${task.id}-known_issues`);
|
|
382
|
+
}
|
|
383
|
+
const acceptedBy = texts && texts.accepted_by != null ? texts.accepted_by : task.accepted_by;
|
|
384
|
+
if (acceptedBy != null) details(ps, 'Accepted by', acceptedBy, `task-${task.id}-accepted_by`);
|
|
345
385
|
if (task.dwarf_tail != null) details(ps, 'Dwarf final message', task.dwarf_tail, `task-${task.id}-dwarf_tail`);
|
|
346
386
|
if (task.qa_tail != null) details(ps, 'Reviewer final message', task.qa_tail, `task-${task.id}-qa_tail`);
|
|
347
387
|
}
|
|
@@ -475,10 +515,12 @@ window.ForgePanel = (() => {
|
|
|
475
515
|
return;
|
|
476
516
|
}
|
|
477
517
|
const chips = el('div', undefined, 'chips');
|
|
478
|
-
|
|
518
|
+
const status = RM.finalStatus(pick);
|
|
519
|
+
chips.append(chip(status));
|
|
479
520
|
if (pick.published) chips.append(el('span', 'published', 'tag'));
|
|
480
521
|
chips.append(el('span', pick.source === 'integrate' ? 'during integrate' : 'forge review', 'tag'));
|
|
481
522
|
head.append(chips);
|
|
523
|
+
if (status === 'OUTDATED') muted(host, RM.reviewSinceLine(pick));
|
|
482
524
|
fields(host, [['Reviewer', plate(pick.spec)], ['Threshold', pick.threshold], ['Reviewed', pick.base || pick.tip ? `${String(pick.base || '?').slice(0, 10)}..${String(pick.tip || '?').slice(0, 10)}` : '—', true],
|
|
483
525
|
['Started', time(pick.started ?? pick.at)], ['Ended', time(pick.ended)], ['Duration', RM.fmtDur(pick.duration_s)]]);
|
|
484
526
|
if (pick.execution) { const s = { kind: 'execution', task: null, exec: pick.execution }; host.append(link('Model, timings and exact prompt →', ctx.hash(s), () => ctx.onSelect(s))); }
|
|
@@ -113,7 +113,9 @@
|
|
|
113
113
|
if (f.model && !list(run.models).some(m => alias(m) === f.model)) return false;
|
|
114
114
|
if (f.ageDays && num(now) !== null && !(num(run.updated) !== null && run.updated >= now - f.ageDays * 86400)) return false;
|
|
115
115
|
if (f.verdict) {
|
|
116
|
-
|
|
116
|
+
// An outdated published review matches OUTDATED, not the verdict it gave.
|
|
117
|
+
const review = runReviewStatus(run);
|
|
118
|
+
const v = review != null ? review : (run.verification_status || null);
|
|
117
119
|
if (f.verdict === 'none' ? v : v !== f.verdict) return false;
|
|
118
120
|
}
|
|
119
121
|
return true;
|
|
@@ -180,16 +182,116 @@
|
|
|
180
182
|
if (statuses.some(s => s === 'PENDING' || s === 'PASS')) return 'PENDING';
|
|
181
183
|
return 'BLOCKED';
|
|
182
184
|
}
|
|
185
|
+
// How one final review reads as a stage. OUTDATED only once it is done and the reader says the
|
|
186
|
+
// integration branch has moved: a running review, and a review the reader still calls current, keep their verdict.
|
|
187
|
+
function finalStatus(fr) {
|
|
188
|
+
if (!fr || typeof fr !== 'object') return null;
|
|
189
|
+
if (fr.state === 'running') return 'RUNNING';
|
|
190
|
+
if (fr.state === 'infra') return 'INFRA';
|
|
191
|
+
if (fr.state === 'done' && fr.outdated === true) return 'OUTDATED';
|
|
192
|
+
return fr.verdict || 'UNKNOWN';
|
|
193
|
+
}
|
|
194
|
+
// RunSummary carries the published verdict as a string plus final_review_outdated. Same rule as finalStatus.
|
|
195
|
+
function runReviewStatus(summary) {
|
|
196
|
+
if (!summary) return null;
|
|
197
|
+
const v = summary.final_review;
|
|
198
|
+
if (v && typeof v === 'object') return finalStatus(v);
|
|
199
|
+
if (typeof v !== 'string' || !v) return null;
|
|
200
|
+
return summary.final_review_outdated === true ? 'OUTDATED' : v;
|
|
201
|
+
}
|
|
202
|
+
// The lamp and status word for a task. Accepting a merge does not change the recorded status
|
|
203
|
+
// or the run's counts: only the display says ACCEPTED, and only once the task has merged.
|
|
204
|
+
function taskStatus(t) {
|
|
205
|
+
if (!t) return 'PENDING';
|
|
206
|
+
if (t.status === 'MERGED' && t.accepted === true) return 'ACCEPTED';
|
|
207
|
+
return t.status || 'PENDING';
|
|
208
|
+
}
|
|
209
|
+
// Verdict column and the 2D graph's tick or cross. text is the words, status the lamp class,
|
|
210
|
+
// mark the cross (FAIL) or tick (PASS). An acceptance names FAIL or UNKNOWN and never invents a FAIL,
|
|
211
|
+
// and it draws no mark: the graph must not put a red cross on a task a human accepted.
|
|
212
|
+
function taskVerdict(t) {
|
|
213
|
+
if (taskStatus(t) === 'ACCEPTED') {
|
|
214
|
+
const over = t.accepted_over === 'FAIL' || t.accepted_over === 'UNKNOWN' ? t.accepted_over : null;
|
|
215
|
+
return { text: over ? over + ' · accepted' : 'accepted', status: 'accepted', mark: null };
|
|
216
|
+
}
|
|
217
|
+
const v = t && t.verdict ? String(t.verdict) : null;
|
|
218
|
+
return { text: v, status: v ? v.toLowerCase() : 'unknown', mark: v === 'PASS' || v === 'FAIL' ? v : null };
|
|
219
|
+
}
|
|
220
|
+
// One attempt in the history. The attempt that was accepted is ACCEPTED; every earlier attempt
|
|
221
|
+
// keeps the status it ended with. execStatus itself is unchanged, so a QA part still shows the
|
|
222
|
+
// reviewer's verdict and an execution detail does too.
|
|
223
|
+
function attemptStatus(task, attempt, raw) {
|
|
224
|
+
if (taskStatus(task) !== 'ACCEPTED') return raw;
|
|
225
|
+
const n = num(attempt), want = num(task.accepted_attempt);
|
|
226
|
+
if (n !== null && want !== null && n === want) return 'ACCEPTED';
|
|
227
|
+
return raw;
|
|
228
|
+
}
|
|
229
|
+
function shortTip(tip) {
|
|
230
|
+
if (tip === null || tip === undefined || tip === '') return '—';
|
|
231
|
+
return String(tip).slice(0, 7);
|
|
232
|
+
}
|
|
233
|
+
// "<verdict> at <tip7>; N tasks merged since: <ids>", ids cut after 5.
|
|
234
|
+
function reviewSinceLine(src) {
|
|
235
|
+
if (!src) return '';
|
|
236
|
+
const ids = list(src.merged_since).map(id => String(id));
|
|
237
|
+
const shown = ids.slice(0, 5), extra = ids.length - shown.length;
|
|
238
|
+
const names = shown.join(', ') + (extra > 0 ? ' +' + extra + ' more' : '');
|
|
239
|
+
const n = ids.length;
|
|
240
|
+
return `${src.verdict || 'UNKNOWN'} at ${shortTip(src.tip)}; ${n} ${n === 1 ? 'task' : 'tasks'} merged since` + (names ? ': ' + names : '');
|
|
241
|
+
}
|
|
242
|
+
// The newest final review that had started by t. Same pick stateAt has always made.
|
|
243
|
+
function reviewAt(run, t) {
|
|
244
|
+
const started = r => num(r && r.started) ?? num(r && r.at);
|
|
245
|
+
const fr = list(run && run.final_reviews).filter(r => r && (started(r) ?? Infinity) <= t);
|
|
246
|
+
return fr.sort((a, b) => (a.at || 0) - (b.at || 0)).pop() || null;
|
|
247
|
+
}
|
|
248
|
+
// When an outdated review starts to show as OUTDATED. The current flag is the truth; this only places it.
|
|
249
|
+
// First id in merged_since, else (that list is empty) the first merge at or after the review's `at`,
|
|
250
|
+
// else the review's end when no merge time is known. A current review (outdated false or null) has no moment.
|
|
251
|
+
function outdatedMoment(run, review) {
|
|
252
|
+
if (!review || review.outdated !== true) return null;
|
|
253
|
+
const merges = list(run && run.integration && run.integration.merged).filter(m => m && num(m.at) !== null);
|
|
254
|
+
const since = list(review.merged_since);
|
|
255
|
+
if (since.length) {
|
|
256
|
+
const times = merges.filter(m => m.id === since[0]).map(m => m.at);
|
|
257
|
+
if (times.length) return Math.min(...times);
|
|
258
|
+
return num(review.ended);
|
|
259
|
+
}
|
|
260
|
+
const at = num(review.at);
|
|
261
|
+
if (at !== null) {
|
|
262
|
+
const later = merges.filter(m => m.at >= at).map(m => m.at);
|
|
263
|
+
if (later.length) return Math.min(...later);
|
|
264
|
+
}
|
|
265
|
+
return num(review.ended);
|
|
266
|
+
}
|
|
267
|
+
// Status of one review at time t. Before it ends it is RUNNING. OUTDATED only at and after outdatedMoment,
|
|
268
|
+
// and only when the review is done and currently outdated, so the present matches finalStatus.
|
|
269
|
+
function finalAt(run, review, t) {
|
|
270
|
+
if (!review) return 'NONE';
|
|
271
|
+
if (review.state === 'running') return 'RUNNING';
|
|
272
|
+
const ended = num(review.ended);
|
|
273
|
+
const end = ended !== null ? ended : (review.state === 'done' || review.state === 'infra' ? num(review.at) : null);
|
|
274
|
+
if (end === null) return finalStatus(review) || 'RUNNING';
|
|
275
|
+
if (end > t) return 'RUNNING';
|
|
276
|
+
if (review.state === 'infra') return 'INFRA';
|
|
277
|
+
if (review.state === 'done' && review.outdated === true) {
|
|
278
|
+
const from = outdatedMoment(run, review);
|
|
279
|
+
if (from === null || t >= from) return 'OUTDATED';
|
|
280
|
+
}
|
|
281
|
+
return review.verdict || 'UNKNOWN';
|
|
282
|
+
}
|
|
183
283
|
function finishState(run) {
|
|
184
284
|
const fr = run.final_review, v = run.verification, ig = run.integration;
|
|
185
285
|
const tasks = list(run.tasks), merged = tasks.filter(t => t.status === 'MERGED').length;
|
|
286
|
+
const frObj = fr && typeof fr === 'object' ? fr : null;
|
|
186
287
|
return {
|
|
187
288
|
integration: { status: integrationStatus(tasks.map(t => t.status)),
|
|
188
289
|
merged, total: tasks.length, base: ig && ig.base || null, accepted: ig && ig.accepted || null },
|
|
189
290
|
verification: { status: v && v.status ? String(v.status).toUpperCase() : 'PENDING', command: v && v.command || null, runs: list(v && v.runs).length },
|
|
190
|
-
final: { status: !
|
|
191
|
-
requested: !!(fr || run.settings && run.settings.final_qa), spec:
|
|
192
|
-
count: list(run.final_reviews).length
|
|
291
|
+
final: { status: !frObj ? (run.settings && run.settings.final_qa ? 'PENDING' : 'NONE') : finalStatus(frObj),
|
|
292
|
+
requested: !!(fr || run.settings && run.settings.final_qa), spec: frObj && frObj.spec || run.settings && run.settings.final_qa || null, findings: frObj && frObj.findings || null,
|
|
293
|
+
count: list(run.final_reviews).length,
|
|
294
|
+
verdict: frObj && frObj.verdict || null, tip: frObj && frObj.tip || null, merged_since: list(frObj && frObj.merged_since) },
|
|
193
295
|
};
|
|
194
296
|
}
|
|
195
297
|
function planModel(run) {
|
|
@@ -221,6 +323,8 @@
|
|
|
221
323
|
const put = (a, p) => { const k = a == null ? 'other' : String(a); if (!byAttempt.has(k)) byAttempt.set(k, []); byAttempt.get(k).push(p); };
|
|
222
324
|
for (const e of execs) {
|
|
223
325
|
if (e.role !== 'dwarf' && e.role !== 'qa') continue;
|
|
326
|
+
// The part keeps execStatus. The accepted attempt is relabelled by attemptStatus on the
|
|
327
|
+
// attempt itself; painting this part ACCEPTED would hide the reviewer's verdict.
|
|
224
328
|
put(e.attempt, { id: e.id, kind: e.role, exec: e, status: execStatus(e), label: e.role === 'dwarf' ? 'DWARF' : 'QA', spec: e.model && e.model.spec || null, attempt: e.attempt });
|
|
225
329
|
}
|
|
226
330
|
for (const s of list(task && task.shadow)) {
|
|
@@ -359,11 +463,13 @@
|
|
|
359
463
|
const start = num(e.started); if (start === null) continue;
|
|
360
464
|
const kids = execs.filter(k => k !== e && (k.parent === e.id || e.synthetic || (tops.length === 1 && !k.parent && (k.role === 'dwarf' || k.role === 'qa'))));
|
|
361
465
|
const row = rows.filter(r => r.attempt === e.attempt).pop();
|
|
362
|
-
|
|
466
|
+
let status = e.state === 'running' ? 'RUNNING' : row ? row.status : e.state === 'orphaned' ? 'INTERRUPTED' : t.status === 'MERGED' && e === tops[tops.length - 1] ? 'MERGED' : execStatus(e);
|
|
467
|
+
if (e === tops[tops.length - 1] && t.status === 'MERGED') status = 'MERGED';
|
|
468
|
+
status = attemptStatus(t, e.attempt, status);
|
|
363
469
|
const end = execEnd(e, now);
|
|
364
470
|
const running = kids.find(k => k.state === 'running');
|
|
365
471
|
items.push({ id: e.id, task: t.id, attempt: e.attempt, start, end: end !== null ? Math.max(end, start) : start, running: e.state === 'running',
|
|
366
|
-
status
|
|
472
|
+
status, role: running ? running.role : null, phase: running ? running.phase : null,
|
|
367
473
|
segs: kids.filter(k => k.role === 'dwarf' || k.role === 'qa').map(k => ({ id: k.id, role: k.role, start: num(k.started), end: execEnd(k, now), status: execStatus(k) })).filter(s => s.start !== null) });
|
|
368
474
|
}
|
|
369
475
|
}
|
|
@@ -492,9 +598,8 @@
|
|
|
492
598
|
}
|
|
493
599
|
tasks[task.id] = { status, role };
|
|
494
600
|
}
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
const final = !last ? 'NONE' : num(last.ended) !== null && last.ended <= t ? (last.verdict || 'UNKNOWN') : 'RUNNING';
|
|
601
|
+
const last = reviewAt(run, t);
|
|
602
|
+
const final = finalAt(run, last, t);
|
|
498
603
|
const vs = list(run && run.timeline && run.timeline.verifications).filter(v => num(v.started) !== null && v.started <= t);
|
|
499
604
|
const lv = vs.pop();
|
|
500
605
|
const verification = !lv ? 'PENDING' : num(lv.ended) !== null && lv.ended <= t ? (lv.exit_code === 0 ? 'PASS' : 'FAIL') : 'RUNNING';
|
|
@@ -502,7 +607,15 @@
|
|
|
502
607
|
}
|
|
503
608
|
function timeRange(run, now) {
|
|
504
609
|
const s = [], e = [];
|
|
505
|
-
for (const t of list(run && run.tasks))
|
|
610
|
+
for (const t of list(run && run.tasks)) {
|
|
611
|
+
for (const x of list(t.executions)) { if (num(x.started) !== null) s.push(x.started); const end = execEnd(x, now); if (end !== null) e.push(end); }
|
|
612
|
+
// The merged marker is written after the attempt ends. Leave it out and the slider stops on the
|
|
613
|
+
// verdict while the current view already shows the review as OUTDATED.
|
|
614
|
+
if (num(t.merged_at) !== null) e.push(t.merged_at);
|
|
615
|
+
}
|
|
616
|
+
// Same marker, as integration.merged. That list is what outdatedMoment reads, and a run detail can carry it
|
|
617
|
+
// without the task's merged_at matching, so the range has to take it on its own.
|
|
618
|
+
for (const m of list(run && run.integration && run.integration.merged)) if (num(m && m.at) !== null) e.push(m.at);
|
|
506
619
|
for (const r of list(run && run.final_reviews)) { const v = num(r.started) ?? num(r.at); if (v !== null) s.push(v); if (num(r.ended) !== null) e.push(r.ended); }
|
|
507
620
|
for (const v of list(run && run.timeline && run.timeline.verifications)) { if (num(v.started) !== null) s.push(v.started); if (num(v.ended) !== null) e.push(v.ended); }
|
|
508
621
|
if (!s.length) return null;
|
|
@@ -556,8 +669,9 @@
|
|
|
556
669
|
if (r.kind === 'solo') continue;
|
|
557
670
|
for (const t of list(r.task_statuses)) {
|
|
558
671
|
if (!t || !t.id) continue;
|
|
559
|
-
|
|
560
|
-
|
|
672
|
+
const shown = taskStatus(t);
|
|
673
|
+
out.push({ id: `task:${r.key}:${t.id}`, kind: 'task', key: r.key, task: t.id, label: t.title ? `${t.id} · ${t.title}` : t.id, sub: `${goal} · ${String(shown).toLowerCase()}`,
|
|
674
|
+
ids: [t.id], words: [r.run_id, basename(r.repo)].filter(Boolean), t: num(r.updated) || 0, status: shown, subSearch: false, // not found through its run's goal
|
|
561
675
|
href: `#/runs/${encodeURIComponent(r.key)}/tasks/${encodeURIComponent(t.id)}` });
|
|
562
676
|
}
|
|
563
677
|
}
|
|
@@ -661,7 +775,8 @@
|
|
|
661
775
|
return { pos, edges, cols, rows: next };
|
|
662
776
|
}
|
|
663
777
|
|
|
664
|
-
return { liveNow, runState, execStatus, matchRun, filterOptions, atlasModel, columns, planModel, finishState,
|
|
778
|
+
return { liveNow, runState, execStatus, matchRun, filterOptions, atlasModel, columns, planModel, finishState,
|
|
779
|
+
finalStatus, runReviewStatus, reviewSinceLine, reviewAt, taskStatus, taskVerdict, attemptStatus, taskParts,
|
|
665
780
|
taskTimes, metricValue, overlap, projectSchedule, execModel, criticalPath, concurrency, stateAt, timeRange, events, lineage, structureKey, innerKey,
|
|
666
781
|
fmtDur, alias, basename, weight, searchIndex, searchRank, statusEvents, fractalTree };
|
|
667
782
|
});
|