@giamme/forge 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/SKILL.md +4 -3
- package/VERSION +1 -1
- package/docs/artifacts.md +11 -0
- package/docs/commands.md +8 -3
- 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 +344 -16
- 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_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 +4 -3
- package/ui/docs/api.md +75 -15
- package/ui/forge_ui/control.py +12 -0
- 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 +29 -3
- 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/style.css +7 -3
|
@@ -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
|
});
|
|
@@ -89,12 +89,13 @@ export async function createStage(host, cb = {}) {
|
|
|
89
89
|
el.col = col || E.statusColor(el.status);
|
|
90
90
|
el.pulse = el.status === 'RUNNING';
|
|
91
91
|
}
|
|
92
|
-
const PLAIN = new Set(['PENDING', 'NONE', 'UNKNOWN']);
|
|
92
|
+
const PLAIN = new Set(['PENDING', 'NONE', 'UNKNOWN', 'OUTDATED']); // unlit: no glow and no tint
|
|
93
93
|
function baseTargets(el) {
|
|
94
94
|
const T = el.tgt, s = el.status, light = E.theme === 'light';
|
|
95
95
|
T.lit = PLAIN.has(s) ? 0 : .32;
|
|
96
96
|
T.tint = PLAIN.has(s) ? 0 : light ? .34 : .1; // a pale panel shows a lit section by its colour: no glow to carry it
|
|
97
97
|
T.vis = 1; T.op = 1; T.opB = 1;
|
|
98
|
+
// Not started yet. OUTDATED is a finished review that is no longer current, so it stays solid.
|
|
98
99
|
if (s === 'PENDING' || s === 'NONE') { T.opB = .55; T.op = .8; }
|
|
99
100
|
// parts are small: their status has to read from the colour of the whole piece
|
|
100
101
|
if (el.kind === 'part' && !PLAIN.has(s)) { T.tint = light ? .5 : .32; T.lit = .55; }
|
|
@@ -166,7 +167,8 @@ export async function createStage(host, cb = {}) {
|
|
|
166
167
|
const parts = [r.kind === 'solo' ? (Object.keys(c)[0] || 'solo') : `${merged}/${total} done`];
|
|
167
168
|
if (running) parts.push(`${running} running`);
|
|
168
169
|
if (bad) parts.push(`${bad} failed`);
|
|
169
|
-
|
|
170
|
+
const reviewStatus = RM.runReviewStatus(r);
|
|
171
|
+
if (reviewStatus) parts.push(`review ${reviewStatus}`);
|
|
170
172
|
else if (r.verification_status) parts.push(`verify ${r.verification_status}`);
|
|
171
173
|
return parts.join(' · ');
|
|
172
174
|
}
|
|
@@ -191,7 +193,7 @@ export async function createStage(host, cb = {}) {
|
|
|
191
193
|
cx.fillText(E.ellipsis(`${RM.basename(r.repo)} · ${r.run_id || ''}${r.kind === 'solo' ? ' · solo' : ''}`, Math.floor((w - 30) / (7 * E.CW))), 2 * s, 44 * s);
|
|
192
194
|
}, 'cover', { x: x + 12, y: y + 8, z: h });
|
|
193
195
|
leds(el, Math.min(60, m.tasks.length), x + 17, y + d - 14, h + .16, { step: 8, s: 2.4, perRow: Math.max(4, Math.floor((w - 30) / 8)) });
|
|
194
|
-
setLeds(el, m.tasks.map(t => t
|
|
196
|
+
setLeds(el, m.tasks.map(t => RM.taskStatus(t)));
|
|
195
197
|
display(el, e => runInfo(e.data.run), { w: w - 24, x: x + 12, y: y + d, z: h - 8 });
|
|
196
198
|
halo(el);
|
|
197
199
|
}
|
|
@@ -206,17 +208,28 @@ export async function createStage(host, cb = {}) {
|
|
|
206
208
|
const el = EL.get('r:' + m.id); if (!el) continue;
|
|
207
209
|
el.data.run = m.run; el.data.module = m;
|
|
208
210
|
setStatus(el, STATE_STATUS[m.state]); halo(el);
|
|
209
|
-
setLeds(el, m.tasks.map(t => t
|
|
210
|
-
const sig = JSON.stringify([m.run.goal, m.run.counts, m.run.final_review, m.run.verification_status]);
|
|
211
|
+
setLeds(el, m.tasks.map(t => RM.taskStatus(t)));
|
|
212
|
+
const sig = JSON.stringify([m.run.goal, m.run.counts, m.run.final_review, m.run.final_review_outdated, m.run.verification_status]);
|
|
211
213
|
if (sig !== el.sig) { el.sig = sig; redraw(el); }
|
|
212
214
|
}
|
|
213
215
|
retarget();
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
/* ---------- plan: waves, tasks, the finish line ---------- */
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
// During a replay the recorded status is what it was then; acceptance shows only once that status is MERGED.
|
|
220
|
+
function shownTask(t) {
|
|
221
|
+
if (t && st.replay !== null && st.replayState) {
|
|
222
|
+
const s = st.replayState.tasks[t.id];
|
|
223
|
+
return Object.assign({}, t, { status: s ? s.status : 'PENDING' });
|
|
224
|
+
}
|
|
225
|
+
return t;
|
|
226
|
+
}
|
|
227
|
+
function taskStatus(t) { return RM.taskStatus(shownTask(t)); }
|
|
228
|
+
function attemptLamps(t) {
|
|
229
|
+
const shown = shownTask(t);
|
|
230
|
+
const rows = list(t.attempts).map(a => RM.attemptStatus(shown, a.attempt, a.status));
|
|
231
|
+
if (st.replay === null && t.status === 'RUNNING') rows.push('RUNNING');
|
|
232
|
+
return rows;
|
|
220
233
|
}
|
|
221
234
|
function taskLine(el) {
|
|
222
235
|
const t = el.data.task, s = el.status;
|
|
@@ -256,6 +269,7 @@ export async function createStage(host, cb = {}) {
|
|
|
256
269
|
const tries = Math.max(1, list(t.attempts).length);
|
|
257
270
|
leds(el, Math.min(20, tries), x + 17, y + d - 13, h + .16, { step: 9, s: 2.8 });
|
|
258
271
|
el.ledCount = tries;
|
|
272
|
+
setLeds(el, attemptLamps(t).slice(-el.ledCount));
|
|
259
273
|
display(el, taskLine, { w: w - 24, x: x + 12, y: y + d, z: h - 8 });
|
|
260
274
|
// the Clef ring: how likely the first try was to pass, in the colour of what happened
|
|
261
275
|
const p0 = t.clef && t.clef.first_pass && num(t.clef.first_pass.p);
|
|
@@ -276,9 +290,12 @@ export async function createStage(host, cb = {}) {
|
|
|
276
290
|
const F = st.plan && st.plan.finish, run = st.run; if (!F) return {};
|
|
277
291
|
if (id === 'f:integration') return { title: 'INTEGRATION', sub: `${F.integration.merged}/${F.integration.total} merged`, status: F.integration.status, line: F.integration.accepted ? 'at ' + String(F.integration.accepted).slice(0, 10) : 'into forge/' + (run.run_id || '…') + '-integration' };
|
|
278
292
|
if (id === 'f:verification') return { title: 'VERIFICATION', sub: F.verification.command || 'no command recorded', status: F.verification.status, line: F.verification.runs ? `${F.verification.runs} run${F.verification.runs > 1 ? 's' : ''}` : 'not run yet' };
|
|
293
|
+
const status = finishStatus(id, { status: F.final.status });
|
|
294
|
+
const src = status === 'OUTDATED' && st.replay !== null ? (RM.reviewAt(run, st.replay) || F.final) : F.final;
|
|
279
295
|
const f = F.final.findings;
|
|
280
|
-
|
|
281
|
-
|
|
296
|
+
const line = status === 'OUTDATED' ? RM.reviewSinceLine(src)
|
|
297
|
+
: F.final.status === 'NONE' ? 'opt-in: forge review <plan>' : F.final.count > 1 ? `${F.final.count} reviews` : status;
|
|
298
|
+
return { title: 'FINAL REVIEW', sub: F.final.requested ? (F.final.spec || 'reviewer not chosen') : 'not requested', status, findings: f, line };
|
|
282
299
|
}
|
|
283
300
|
function buildFinish(m, p, g) {
|
|
284
301
|
const [x, y, w, d, h] = p;
|
|
@@ -308,7 +325,7 @@ export async function createStage(host, cb = {}) {
|
|
|
308
325
|
}
|
|
309
326
|
function finishLeds(el) {
|
|
310
327
|
if (el.id === 'f:final') { const f = finishInfo(el.id).findings || {}; setLeds(el, ['P0', 'P1', 'P2', 'P3'].map(s => f[s] ? E.sevColor(s) : null)); }
|
|
311
|
-
else if (el.id === 'f:integration') setLeds(el, list(st.run.tasks).map(t => taskStatus(t) === 'MERGED'
|
|
328
|
+
else if (el.id === 'f:integration') setLeds(el, list(st.run.tasks).map(t => { const s = taskStatus(t); return s === 'MERGED' || s === 'ACCEPTED' ? s : null; }));
|
|
312
329
|
else setLeds(el, list(st.run.verification && st.run.verification.runs).map(r => r.exit === 0 ? 'PASS' : 'FAIL'));
|
|
313
330
|
}
|
|
314
331
|
function buildPlan(g) {
|
|
@@ -954,7 +971,7 @@ export async function createStage(host, cb = {}) {
|
|
|
954
971
|
if (c.tint > .001) el.M.body.color.lerp(TMP2.copy(col), c.tint * c.vis);
|
|
955
972
|
if (el.tintMat) { el.tintMat.color.copy(col); el.tintMat.opacity = (c.tint * (E.theme === 'light' ? 1.45 : 1.1) + .08 * c.sel) * ob * gf * c.vis; }
|
|
956
973
|
if (el.haloMat) el.haloMat.uniforms.opacity.value = (c.halo + (el.pulse ? .12 * pulseK : 0)) * gf * c.vis;
|
|
957
|
-
if (el.clef) el.clef.fgM.color.copy(E.statusColor(['MERGED', 'PASS'].includes(el.status) ? 'pass' : ['FAIL', 'ERROR', 'CONFLICT'].includes(el.status) ? 'fail' : 'pending'));
|
|
974
|
+
if (el.clef) el.clef.fgM.color.copy(E.statusColor(['MERGED', 'PASS', 'ACCEPTED'].includes(el.status) ? 'pass' : ['FAIL', 'ERROR', 'CONFLICT'].includes(el.status) ? 'fail' : 'pending'));
|
|
958
975
|
if (el.inner) {
|
|
959
976
|
const p = pr ? pr.p : 0, vis = p > .002;
|
|
960
977
|
if (!vis && el.inner.visible) { E.release(el.inner); }
|
|
@@ -1239,6 +1256,7 @@ export async function createStage(host, cb = {}) {
|
|
|
1239
1256
|
const lamps = { title: 'Lamps', rows: [
|
|
1240
1257
|
{ sw: { lamp: 'st-merged' }, label: 'merged · passed · accepted' }, { sw: { lamp: 'st-running' }, label: 'running · interrupted' },
|
|
1241
1258
|
{ sw: { lamp: 'st-fail' }, label: 'failed · conflict · infra · timeout' }, { sw: { lamp: 'st-pending' }, label: 'pending, not started' },
|
|
1259
|
+
{ sw: { lamp: 'st-outdated' }, label: 'outdated review' },
|
|
1242
1260
|
{ sw: { route: true }, label: st.view === 'atlas' ? 'chosen run' : 'chosen task' + (L.route ? ' and its route' : '') }] };
|
|
1243
1261
|
if (st.view === 'atlas') {
|
|
1244
1262
|
out.push({ title: 'Map', rows: [], note: 'Each block is a run, each district a repository. The lamps on a block are its tasks; the block stands taller the more tasks it has.' }, lamps);
|
|
@@ -1318,8 +1336,7 @@ export async function createStage(host, cb = {}) {
|
|
|
1318
1336
|
if (el.kind === 'task') {
|
|
1319
1337
|
const t = byId.get(el.data.task.id); if (!t) continue;
|
|
1320
1338
|
el.data.task = t; setStatus(el, taskStatus(t)); halo(el);
|
|
1321
|
-
const rows =
|
|
1322
|
-
if (st.replay === null && t.status === 'RUNNING') rows.push('RUNNING');
|
|
1339
|
+
const rows = attemptLamps(t);
|
|
1323
1340
|
if (rows.length > el.ledCount) { /* more tries than lamps: the newest show */ }
|
|
1324
1341
|
setLeds(el, rows.slice(-el.ledCount));
|
|
1325
1342
|
const sig = JSON.stringify([t.status, t.findings, t.gate, list(t.shadow).map(s => s.agreement), t.dwarf, t.accepted_by, el.status]);
|
|
@@ -1385,7 +1402,7 @@ export async function createStage(host, cb = {}) {
|
|
|
1385
1402
|
E.invalidate(100); // marks a replay reaches drop in
|
|
1386
1403
|
st.replayState = st.replay !== null ? RM.stateAt(st.run, st.replay) : null;
|
|
1387
1404
|
for (const el of EL.values()) {
|
|
1388
|
-
if (el.kind === 'task') setStatus(el, taskStatus(el.data.task));
|
|
1405
|
+
if (el.kind === 'task') { setStatus(el, taskStatus(el.data.task)); setLeds(el, attemptLamps(el.data.task).slice(-el.ledCount)); }
|
|
1389
1406
|
else if (el.kind === 'finish') setStatus(el, finishStatus(el.id, finishInfo(el.id)));
|
|
1390
1407
|
else if (el.data && el.data.item) setStatus(el, st.replay !== null ? replayItemStatus(el.data.item) : el.data.item.status);
|
|
1391
1408
|
else continue;
|
|
@@ -91,13 +91,14 @@
|
|
|
91
91
|
:root[data-theme=dark] { color-scheme: dark; }
|
|
92
92
|
|
|
93
93
|
/* status: the lamp behind each forge state. Five lamps, one meaning each (--st; --st-lit on the 3D plate);
|
|
94
|
-
a hollow lamp is that lamp's family but not its plain case: passed without a merge, stopped, failed outside the code
|
|
94
|
+
a hollow lamp is that lamp's family but not its plain case: passed without a merge, stopped, failed outside the code,
|
|
95
|
+
an outdated review */
|
|
95
96
|
.st-merged, .st-pass, .st-accepted, .st-nochanges { --st: var(--lamp-green); --st-lit: var(--lit-green); }
|
|
96
97
|
.st-running, .st-interrupted { --st: var(--lamp-amber); --st-lit: var(--lit-amber); }
|
|
97
|
-
.st-pending, .st-none, .st-unknown, .st-invalidated { --st: var(--lamp-off); --st-lit: var(--lamp-off); }
|
|
98
|
+
.st-pending, .st-none, .st-unknown, .st-invalidated, .st-outdated { --st: var(--lamp-off); --st-lit: var(--lamp-off); }
|
|
98
99
|
.st-fail, .st-error, .st-conflict, .st-infra, .st-timeout, .st-blocked { --st: var(--lamp-red); --st-lit: var(--lit-red); }
|
|
99
100
|
[class^="st-"], [class*=" st-"] { --st-fill: var(--st); --st-ring: var(--lamp-edge); }
|
|
100
|
-
.st-accepted, .st-nochanges, .st-interrupted, .st-infra, .st-timeout, .st-blocked { --st-fill: transparent; --st-ring: var(--st); }
|
|
101
|
+
.st-accepted, .st-nochanges, .st-interrupted, .st-infra, .st-timeout, .st-blocked, .st-outdated { --st-fill: transparent; --st-ring: var(--st); }
|
|
101
102
|
/* review severity is a shape (P0 spike, P1 diamond, P2 cone, P3 pin) on a ramp of its own, never a status lamp */
|
|
102
103
|
.sev-P0 { --sev: var(--sev-p0); --sev-shape: polygon(50% 0, 100% 100%, 0 100%); }
|
|
103
104
|
.sev-P1 { --sev: var(--sev-p1); --sev-shape: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
|
|
@@ -152,6 +153,7 @@ kbd { font-family: var(--mono); font-size: var(--t-xs); padding: 0 5px; border:
|
|
|
152
153
|
.chip { display: inline-flex; align-items: center; gap: 5px; padding: 1px 7px 0 6px; border: 1px solid var(--rule); border-radius: var(--r-small); background: var(--key); color: var(--ink); white-space: nowrap;
|
|
153
154
|
font-family: var(--font-plate); font-weight: 700; font-size: .857rem; letter-spacing: .05em; text-transform: uppercase; line-height: 1.5; font-variant-numeric: tabular-nums; }
|
|
154
155
|
.chip::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--st-fill, var(--lamp-off)); box-shadow: inset 0 0 0 1.5px var(--st-ring, transparent); flex: none; }
|
|
156
|
+
a.chip, a.chip:hover { color: inherit; text-decoration: none; }
|
|
155
157
|
.chips { display: flex; gap: var(--s-1); flex-wrap: wrap; }
|
|
156
158
|
.sev-chip { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: var(--t-xs); white-space: nowrap; }
|
|
157
159
|
.sev-chip::before { content: ''; width: 9px; height: 9px; clip-path: var(--sev-shape, circle(30%)); background: var(--sev, var(--muted)); flex: none; }
|
|
@@ -683,6 +685,7 @@ th:hover .th-sort, .th-sort.on { color: var(--ink); }
|
|
|
683
685
|
.fg-bar { fill: var(--st, var(--lamp-off)); }
|
|
684
686
|
.fg-verdict { stroke: var(--lamp-red); }
|
|
685
687
|
.fg-verdict.fg-pass { stroke: var(--st, var(--lamp-green)); }
|
|
688
|
+
.fg-node.st-accepted .fg-box { fill: transparent; }
|
|
686
689
|
.fg-empty { color: var(--muted); padding: var(--s-3) 0; }
|
|
687
690
|
.gantt, .strip { display: block; font-family: var(--mono); }
|
|
688
691
|
.gt-lane { fill: var(--module-2); }
|
|
@@ -693,6 +696,7 @@ th:hover .th-sort, .th-sort.on { color: var(--ink); }
|
|
|
693
696
|
.gt-tick-label { fill: var(--muted); font-size: 10px; }
|
|
694
697
|
.gt-item { cursor: pointer; }
|
|
695
698
|
.gt-box { fill: var(--st, var(--lamp-off)); fill-opacity: .2; stroke: var(--st, var(--lamp-off)); stroke-width: 1.2; }
|
|
699
|
+
.gt-item.st-accepted .gt-box { fill: transparent; }
|
|
696
700
|
.gt-item:hover .gt-box, .gt-item:focus .gt-box { fill-opacity: .35; }
|
|
697
701
|
.gt-item:focus { outline: none; }
|
|
698
702
|
.gt-item.gt-sel .gt-box { stroke: var(--route); stroke-width: 2.5; }
|