From 9597891f782f643b41434f4a35a10e4860bea6fd Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Fri, 31 Jul 2026 19:11:44 +0530 Subject: [PATCH 1/4] Run Workspace: timeline falls back to events.jsonl, labeled (#496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timeline section read only the derived collections (run.timeline / run.activityTimeline, populated for index entries since #296), so legacy runs, fixture-shaped runs, and index entries predating the field declared "no recorded timeline events" while events.jsonl sat populated in the same snapshot — the Live Feed rendered those exact events one page over. readableTimeline now derives a minimal timeline from events when the derived collections are empty (ts/type/task_id/stage_id, time-ordered, capped to the most recent 120 — the index's own timeline cap), and the section carries its source. The page labels the fallback ("Derived from events.jsonl — this run has no persisted timeline"), matching the 3D Studio's "events.jsonl projection" honesty convention. Derived timelines keep precedence (no event double-merge) and the honest empty state remains when both sources are empty — all pinned, verified failing first. Closes #496. Co-Authored-By: Claude Fable 5 --- .../dashboard/state/run-workspace.js | 21 +++++- .../dashboard/ui/pages/run-workspace.js | 7 +- tests/run-workspace-timeline-496.test.js | 71 +++++++++++++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/run-workspace-timeline-496.test.js diff --git a/src/observability/dashboard/state/run-workspace.js b/src/observability/dashboard/state/run-workspace.js index 6996b387..04dc6505 100644 --- a/src/observability/dashboard/state/run-workspace.js +++ b/src/observability/dashboard/state/run-workspace.js @@ -3,9 +3,26 @@ import { buildOverviewProjection } from './overview.js'; // owner: RStack developed by Richardson Gunde function readableTimeline(run) { - return [...(run.timeline ?? []), ...(run.activityTimeline ?? [])] + const derived = [...(run.timeline ?? []), ...(run.activityTimeline ?? [])] .filter((item) => item && item.ts) .sort((a, b) => String(a.ts).localeCompare(String(b.ts))); + if (derived.length) return { items: derived, source: 'persisted' }; + // #496: legacy/fixture runs and index entries predating the derived + // timeline fields still carry raw events — a populated events.jsonl must + // never read as "no recorded timeline events". Minimal projection, capped + // to the most recent entries, labeled with its source (the same honesty + // convention as the 3D Studio's "events.jsonl projection" timeline). + const events = (run.events ?? []) + .filter((event) => event && event.ts) + .map((event) => ({ + ts: event.ts, + type: event.type ?? 'event', + task_id: event.task_id ?? null, + stage_id: event.stage_id ?? null, + })) + .sort((a, b) => String(a.ts).localeCompare(String(b.ts))) + .slice(-120); + return { items: events, source: events.length ? 'events' : 'none' }; } function workItems(run) { @@ -113,7 +130,7 @@ export function buildRunWorkspace(run, context = {}) { sections: { summary: { available: true }, work: { available: work.length > 0, items: work }, - timeline: { available: timeline.length > 0, items: timeline }, + timeline: { available: timeline.items.length > 0, items: timeline.items, source: timeline.source }, artifacts: { available: artifacts.length > 0, items: artifacts, stageReports: run.stageReports ?? [] }, metrics: { available: metricsProvenance !== 'unavailable', diff --git a/src/observability/dashboard/ui/pages/run-workspace.js b/src/observability/dashboard/ui/pages/run-workspace.js index 8ee15bff..2136127a 100644 --- a/src/observability/dashboard/ui/pages/run-workspace.js +++ b/src/observability/dashboard/ui/pages/run-workspace.js @@ -90,7 +90,12 @@ function renderRunWorkspaceTimeline(section) { setHTML('run-workspace-timeline', emptyHtml('Timeline unavailable', 'This run has no recorded timeline events.')); return; } - setHTML('run-workspace-timeline', '
' + section.items.map(function(item) { + // #496: an events-derived timeline names its source — same convention as + // the 3D Studio's "events.jsonl projection" label. + var sourceNote = section.source === 'events' + ? '
Derived from events.jsonl — this run has no persisted timeline.
' + : ''; + setHTML('run-workspace-timeline', sourceNote + '
' + section.items.map(function(item) { return '
' + '
' + esc(String(item.type || 'event').replaceAll('_', ' ')) + '' + '' + esc(item.task_id || item.stage_id || item.detail || 'Run event') + '
'; diff --git a/tests/run-workspace-timeline-496.test.js b/tests/run-workspace-timeline-496.test.js new file mode 100644 index 00000000..52003299 --- /dev/null +++ b/tests/run-workspace-timeline-496.test.js @@ -0,0 +1,71 @@ +/** + * #496 — the Run Workspace timeline falls back to events.jsonl. + * + * The timeline section read only the derived timeline collections + * (run.timeline / run.activityTimeline, populated for index-served runs + * since #296). Runs without them — legacy runs, fixture-shaped runs, index + * entries predating the field — declared "no recorded timeline events" while + * events.jsonl sat populated in the same snapshot. The projection now + * derives a minimal timeline from events when the derived collections are + * empty, labeled with its source; the honest empty state remains only when + * both sources are empty. + * + * owner: RStack developed by Richardson Gunde + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildRunWorkspace } from '../src/observability/dashboard/state/run-workspace.js'; +import { commandCenterScript } from '../src/observability/dashboard/ui/pages/command-center.js'; +import { runWorkspaceScript } from '../src/observability/dashboard/ui/pages/run-workspace.js'; + +function baseRun(overrides = {}) { + return { + runId: 'run-fx-blocked', + projectRoot: '/tmp/p', + manifest: { goal: 'Blocked fixture', status: 'IN_PROGRESS' }, + tasks: [{ id: '07-code', title: 'Code', status: 'BLOCKED', stage_id: '07-code' }], + events: [ + { ts: '2026-07-01T01:00:00.000Z', type: 'task_started', task_id: '07-code' }, + { ts: '2026-07-01T02:00:00.000Z', type: 'task_validated', task_id: '07-code', status: 'FAIL' }, + { ts: '2026-07-01T03:00:00.000Z', type: 'task_started', task_id: '07-code' }, + { ts: '2026-07-01T04:00:00.000Z', type: 'guardrail_triggered', task_id: '07-code' }, + { ts: '2026-07-01T04:00:00.000Z', type: 'approval_gate_blocked', task_id: '07-code' }, + ], + approvals: [], + ...overrides, + }; +} + +test('a run with events but no derived timeline gets an events-sourced timeline (#496)', () => { + const workspace = buildRunWorkspace(baseRun()); + const timeline = workspace.sections.timeline; + assert.equal(timeline.available, true, 'the timeline is available'); + assert.equal(timeline.items.length, 5, 'every event becomes an entry'); + assert.equal(timeline.source, 'events', 'and the section names its fallback source'); + assert.equal(timeline.items[0].ts, '2026-07-01T01:00:00.000Z', 'entries stay time-ordered'); + assert.equal(timeline.items[0].type, 'task_started'); + assert.equal(timeline.items[0].task_id, '07-code'); +}); + +test('a derived timeline keeps precedence — no event double-merge (#496)', () => { + const workspace = buildRunWorkspace(baseRun({ + timeline: [{ ts: '2026-07-01T05:00:00.000Z', type: 'stage_completed', stage_id: '07-code' }], + })); + const timeline = workspace.sections.timeline; + assert.equal(timeline.available, true); + assert.equal(timeline.items.length, 1, 'derived entries only — events never merge in beside them'); + assert.equal(timeline.source, 'persisted'); +}); + +test('a run with neither derived timeline nor events keeps the honest empty state (#496)', () => { + const workspace = buildRunWorkspace(baseRun({ events: [] })); + const timeline = workspace.sections.timeline; + assert.equal(timeline.available, false, 'no invented timeline'); +}); + +test('the workspace page renders the events-fallback source label (#496)', () => { + const bundle = commandCenterScript + runWorkspaceScript; + assert.match(bundle, /events\.jsonl/, 'the renderer names the fallback source in the timeline section'); +}); From 388e956a9bea5324139cc71a8bf6832c9ad93dba Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Fri, 31 Jul 2026 19:12:01 +0530 Subject: [PATCH 2/4] Diagnostics: integrity signal is never a false zero (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildDiagnostics aggregated integrity detail over fully-parsed runs only; index-served runs carry the has_integrity_errors boolean (persisted at INDEX_VERSION 5 by the #316 lite-full parity fix) but not the integrity[] detail array — so a damaged run served from the rollup index contributed nothing, and Diagnostics read "Data integrity errors: 0" with the damaged run in plain view (observed live in the 2026-07-30 tour). The #316 class one level deeper: the badge boolean made it into the index, the detail array feeding Diagnostics did not. Index-served damaged runs now count and appear in the problem list naming their run, with an honest entry that per-file detail requires a full parse ("rollup index entry — run reports damaged files"). Fully-parsed detail is unchanged and never double-counts via its own boolean. Pinned by unit cases plus a real two-build round-trip (first build parses fully and persists the index, the second serves the damaged run from it — the exact path that reported zero), verified failing first. Persisting the compact detail into index entries rides the next INDEX_VERSION bump, noted on the issue. Closes #497. Co-Authored-By: Claude Fable 5 --- src/observability/dashboard/state/layers.js | 20 +++++- tests/diagnostics-integrity-497.test.js | 70 +++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/diagnostics-integrity-497.test.js diff --git a/src/observability/dashboard/state/layers.js b/src/observability/dashboard/state/layers.js index 69aa815e..c1b98091 100644 --- a/src/observability/dashboard/state/layers.js +++ b/src/observability/dashboard/state/layers.js @@ -88,8 +88,24 @@ export function buildDiagnostics(runs, roots, indexMeta = null) { missingValidationCount: fullyParsedTasks.filter((task) => !task.validation).length, // Data integrity (#82): damaged run files recorded during the state build, // one entry per corrupt file so operators see WHAT is broken, not zeros. - integrity: fullyParsed.flatMap((run) => (run.integrity ?? []).map((issue) => ({ runId: run.runId, ...issue }))), - integrityErrorCount: fullyParsed.reduce((total, run) => total + (run.integrity?.length ?? 0), 0), + // #497: index-served runs carry only the has_integrity_errors boolean + // (INDEX_VERSION 5, the #316 parity fix) — the detail array is not + // persisted, so a damaged run served from the rollup index used to + // contribute nothing and Diagnostics read a false zero. Count and name + // such runs; per-file detail honestly states it needs a full parse. + // Persisting the detail rides the next INDEX_VERSION bump (#497). + integrity: [ + ...fullyParsed.flatMap((run) => (run.integrity ?? []).map((issue) => ({ runId: run.runId, ...issue }))), + ...(runs ?? []) + .filter((run) => run.fromIndex && run.hasIntegrityErrors) + .map((run) => ({ + runId: run.runId, + file: 'rollup index entry', + error: 'run reports damaged files — open the run for per-file detail (not persisted to the index)', + })), + ], + integrityErrorCount: fullyParsed.reduce((total, run) => total + (run.integrity?.length ?? 0), 0) + + (runs ?? []).filter((run) => run.fromIndex && run.hasIntegrityErrors).length, // Rollup index freshness + retention visibility for the Diagnostics page. index: indexMeta, }; diff --git a/tests/diagnostics-integrity-497.test.js b/tests/diagnostics-integrity-497.test.js new file mode 100644 index 00000000..06bfa10b --- /dev/null +++ b/tests/diagnostics-integrity-497.test.js @@ -0,0 +1,70 @@ +/** + * #497 — the Diagnostics integrity signal is never a false zero. + * + * buildDiagnostics aggregated integrity detail over fully-parsed runs only; + * index-served runs carry the has_integrity_errors boolean (persisted at + * INDEX_VERSION 5 by the #316 parity fix) but not the integrity[] detail + * array, so a damaged run served from the rollup index contributed nothing — + * "Data integrity errors: 0" with a damaged run in plain view. Index-served + * damage now counts and names its run; per-file detail honestly states it + * needs a full parse. Full detail persistence rides the next INDEX_VERSION + * bump (noted on the issue). + * + * owner: RStack developed by Richardson Gunde + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, realpathSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { buildDiagnostics } from '../src/observability/dashboard/state/layers.js'; +import { buildFullState } from '../src/observability/dashboard/state/index.js'; +import { fixtureMalformedRun } from './helpers/dashboard-fixtures.js'; + +test('index-served damaged runs count and are named — never a false zero (#497)', () => { + const diagnostics = buildDiagnostics([ + { runId: 'run-parsed-clean', fromIndex: false, tasks: [], events: [], integrity: [] }, + { runId: 'run-parsed-damaged', fromIndex: false, tasks: [], events: [], integrity: [{ file: 'decisions.json', error: 'invalid JSON' }] }, + { runId: 'run-indexed-damaged', fromIndex: true, hasIntegrityErrors: true, tasks: [], events: [] }, + { runId: 'run-indexed-clean', fromIndex: true, hasIntegrityErrors: false, tasks: [], events: [] }, + ], ['/tmp/root']); + + assert.equal(diagnostics.integrityErrorCount, 2, + 'one parsed detail entry + one index-served damaged run'); + const indexed = diagnostics.integrity.find((issue) => issue.runId === 'run-indexed-damaged'); + assert.ok(indexed, 'the index-served damaged run is named in the problem list'); + assert.match(indexed.error, /full parse|detail/i, + 'the entry says honestly that per-file detail needs a full parse'); + const parsed = diagnostics.integrity.find((issue) => issue.runId === 'run-parsed-damaged'); + assert.equal(parsed.file, 'decisions.json', 'fully-parsed detail is unchanged'); +}); + +test('fully-parsed damaged runs never double-count via their boolean (#497)', () => { + const diagnostics = buildDiagnostics([ + { runId: 'run-parsed-damaged', fromIndex: false, hasIntegrityErrors: true, tasks: [], events: [], integrity: [{ file: 'events.jsonl', error: 'truncated' }] }, + ], ['/tmp/root']); + assert.equal(diagnostics.integrityErrorCount, 1, 'the detail entry is the only count'); +}); + +test('a damaged run stays visible in Diagnostics when served from the rollup index (#497)', async (t) => { + const root = realpathSync(mkdtempSync(join(tmpdir(), 'rstack-integrity-497-'))); + t.after(() => rmSync(root, { recursive: true, force: true })); + await fixtureMalformedRun(root); + + // First build parses fully and persists the rollup index; the second build + // serves the (stale, inactive) damaged run from that index — the exact + // path that reported zero. + const first = await buildFullState(root, { includeRegistry: false }); + assert.ok(first.diagnostics.integrityErrorCount >= 1, 'full parse sees the damage'); + + const second = await buildFullState(root, { includeRegistry: false }); + const served = second.runs.find((run) => run.runId === 'run-fx-damaged'); + assert.ok(served, 'the damaged run is in scope'); + if (!served.fromIndex) t.skip('run was fully parsed again — index path not exercised in this environment'); + assert.ok(second.diagnostics.integrityErrorCount >= 1, + 'index-served damage still counts — zero would be a false green'); + assert.ok(second.diagnostics.integrity.some((issue) => issue.runId === 'run-fx-damaged'), + 'and the run is named'); +}); From 5047f8931bc77d97d24099a3014981a86bf1613b Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Fri, 31 Jul 2026 19:12:14 +0530 Subject: [PATCH 3/4] Command Center: the Proof Rail names and follows its run (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At all-runs scope the hero cited one run's readiness verdict ("blocked, coverage 85%") while the Proof Rail silently rendered a DIFFERENT run's stages — 15 unqualified "Not Started" beside a non-zero coverage verdict read as the dashboard disagreeing with itself (observed live in the 2026-07-30 tour: the verdict cited run-fx-blocked, the rail showed run-fx-stale). Two server-owned changes in the overview projection: focusRun now prefers the run the verdict's first blocker cites (when that run is in scope), so the rail reflects the run the hero is talking about; and the projection exposes stagesSource {runId, goal} so the rail labels which run it shows ("Stage proof from · ") — pages render the label, they never re-derive it. No-blocker verdicts keep the existing focus selection, citations outside the scope fall back safely, and no-runs scopes invent nothing — all pinned, verified failing first. Live-verified at all-runs scope: verdict, goal chip, and rail now speak about the same run, with the source label under the rail title. Closes #498. Co-Authored-By: Claude Fable 5 --- src/observability/dashboard/state/overview.js | 15 +++- .../dashboard/ui/pages/command-center.js | 12 +++ src/observability/dashboard/ui/pages/index.js | 1 + tests/overview-proof-rail-498.test.js | 87 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tests/overview-proof-rail-498.test.js diff --git a/src/observability/dashboard/state/overview.js b/src/observability/dashboard/state/overview.js index 238a9a21..d17397b4 100644 --- a/src/observability/dashboard/state/overview.js +++ b/src/observability/dashboard/state/overview.js @@ -14,8 +14,15 @@ const STATUS = { QUEUED: 'not_started', }; -function focusRun(runs) { +function focusRun(runs, readiness) { const candidates = runs ?? []; + // #498: the hero cites the readiness verdict, so when its first blocker + // names a run in scope, that run drives the stage rail — otherwise the + // rail can render a DIFFERENT run's stages beside the verdict and read as + // the dashboard disagreeing with itself. + const citedRunId = readiness?.blockers?.[0]?.sourceRef?.runId ?? null; + const cited = citedRunId ? candidates.find((run) => run.runId === citedRunId) : null; + if (cited) return cited; return candidates.find((run) => run.derivedStatus === 'active') ?? candidates.find((run) => run.pipelineRollup) ?? candidates[0] @@ -132,8 +139,8 @@ function actionCount(state) { * Readiness remains server-owned; this projection never derives a verdict. */ export function buildOverviewProjection(state) { - const run = focusRun(state.runs); const readiness = state.readiness ?? { status: 'unknown', coverage: {}, blockers: [] }; + const run = focusRun(state.runs, readiness); const stale = Boolean(run?.pipelineRollup?.stale); return { @@ -143,6 +150,10 @@ export function buildOverviewProjection(state) { title: run ? (readiness.summary ?? 'Delivery outcome is not available.') : 'No delivery run has been evaluated.', nextAction: pipelineAction(run, { ...readiness, actions: state.actions ?? [] }), stages: run ? CANONICAL_SDLC_STAGES.map((stage) => stageView(run, stage)) : [], + // #498: the rail names the run whose stages it shows — at aggregate + // scope the verdict and the rail otherwise read as two different runs + // with no explanation. Pages render this label, they never re-derive it. + stagesSource: run ? { runId: run.runId, goal: run.manifest?.goal ?? run.goal ?? run.runId } : null, actionCount: state.actions ? state.actions.filter((action) => !['approved', 'rejected', 'consumed', 'resolved', 'expired'].includes(action.status)).length : actionCount(state), diff --git a/src/observability/dashboard/ui/pages/command-center.js b/src/observability/dashboard/ui/pages/command-center.js index 881ce8e6..ad1bea58 100644 --- a/src/observability/dashboard/ui/pages/command-center.js +++ b/src/observability/dashboard/ui/pages/command-center.js @@ -133,6 +133,18 @@ function overviewStageIcon(state) { function renderOverviewProofRail(s) { var stages = (s.overview && s.overview.stages) || []; + // #498: the rail names the run whose stages it shows — server-owned via + // overview.stagesSource, so an aggregate scope can never render one run's + // stages beside another run's verdict without saying so. + var stagesSource = (s.overview && s.overview.stagesSource) || null; + var sourceEl = document.getElementById('overview-proof-source'); + if (sourceEl) { + var label = stagesSource + ? 'Stage proof from ' + String(stagesSource.goal || stagesSource.runId).slice(0, 54) + ' · ' + stagesSource.runId + : ''; + setText('overview-proof-source', label); + sourceEl.hidden = !label; + } if (!stages.length) { setHTML('overview-proof-rail', '
  • No stage proof yet.Start a run and canonical stage evidence will appear here.
  • '); return; diff --git a/src/observability/dashboard/ui/pages/index.js b/src/observability/dashboard/ui/pages/index.js index 9614e849..7c42fcf4 100644 --- a/src/observability/dashboard/ui/pages/index.js +++ b/src/observability/dashboard/ui/pages/index.js @@ -66,6 +66,7 @@ function pageBody(id) {
    Goal → stage → proof → action

    Proof Rail

    +
    diff --git a/tests/overview-proof-rail-498.test.js b/tests/overview-proof-rail-498.test.js new file mode 100644 index 00000000..bbc9595a --- /dev/null +++ b/tests/overview-proof-rail-498.test.js @@ -0,0 +1,87 @@ +/** + * #498 — the Proof Rail names the run it reflects and follows the verdict. + * + * At all-runs scope the Command Center hero cited one run's readiness + * verdict ("blocked, coverage 85%") while the Proof Rail silently rendered a + * DIFFERENT run's stages — 15 unqualified "Not Started" beside a non-zero + * coverage verdict read as the dashboard disagreeing with itself. The + * overview projection now (a) prefers the run the verdict's first blocker + * cites when choosing the rail's focus run, and (b) exposes stagesSource so + * the rail labels which run it shows. Single-run scope keeps its selection. + * + * owner: RStack developed by Richardson Gunde + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildOverviewProjection } from '../src/observability/dashboard/state/overview.js'; +import { commandCenterScript } from '../src/observability/dashboard/ui/pages/command-center.js'; + +function run(runId, goal, extras = {}) { + return { + runId, + projectRoot: '/tmp/p', + manifest: { goal }, + tasks: [], + ...extras, + }; +} + +test('the rail follows the run the verdict blocker cites at aggregate scope (#498)', () => { + const projection = buildOverviewProjection({ + runs: [ + run('run-stale', 'Stale fixture', { pipelineRollup: { stale: false } }), + run('run-blocked', 'Blocked fixture'), + ], + readiness: { + status: 'blocked', + summary: 'Release is blocked by 6 source-backed conditions.', + blockers: [{ + type: 'approval', + label: 'Pending approvals', + detail: 'guardrail-override:07-code awaits a manager', + sourceRef: { kind: 'approval_audit', path: '.rstack/runs/run-blocked/approvals.json', runId: 'run-blocked' }, + }], + coverage: { percent: 85 }, + }, + }); + assert.equal(projection.focusRunId, 'run-blocked', + 'the rail reflects the run the verdict is talking about'); + assert.deepEqual(projection.stagesSource, { runId: 'run-blocked', goal: 'Blocked fixture' }, + 'and the projection names the rail’s source run for the label'); +}); + +test('without blockers the existing focus selection is preserved (#498)', () => { + const projection = buildOverviewProjection({ + runs: [ + run('run-active', 'Active work', { derivedStatus: 'active' }), + run('run-other', 'Other'), + ], + readiness: { status: 'ready', summary: 'All checks passed.', blockers: [], coverage: { percent: 100 } }, + }); + assert.equal(projection.focusRunId, 'run-active', 'active-run preference unchanged'); + assert.equal(projection.stagesSource.runId, 'run-active'); +}); + +test('a blocker citing an out-of-scope run falls back to the existing selection (#498)', () => { + const projection = buildOverviewProjection({ + runs: [run('run-only', 'Only run')], + readiness: { + status: 'blocked', + summary: 'Blocked.', + blockers: [{ type: 'approval', label: 'x', detail: 'y', sourceRef: { runId: 'run-vanished' } }], + }, + }); + assert.equal(projection.focusRunId, 'run-only', 'never follows a citation outside the scope'); +}); + +test('no runs means no stages source — nothing invented (#498)', () => { + const projection = buildOverviewProjection({ runs: [], readiness: { status: 'unknown', blockers: [] } }); + assert.equal(projection.stagesSource, null); +}); + +test('the Command Center renders the rail source label from the projection (#498)', () => { + assert.match(commandCenterScript, /stagesSource/, + 'the rail header consumes the server-owned stagesSource label'); +}); From 8022258d7bce76550e2324a4c46dc844f8919659 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Fri, 31 Jul 2026 19:18:29 +0530 Subject: [PATCH 4/4] Review fix: name the timeline and label truncation caps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qodo (convention, PR #490 precedent): the events-timeline depth cap and the rail-label goal length are named constants (EVENTS_TIMELINE_CAP — matching the rollup index's own timeline depth — and PROOF_SOURCE_GOAL_CHARS). Refs #496 #498 (PR #511 review follow-up). Co-Authored-By: Claude Fable 5 --- src/observability/dashboard/state/run-workspace.js | 6 +++++- src/observability/dashboard/ui/pages/command-center.js | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/observability/dashboard/state/run-workspace.js b/src/observability/dashboard/state/run-workspace.js index 04dc6505..5c1caa0a 100644 --- a/src/observability/dashboard/state/run-workspace.js +++ b/src/observability/dashboard/state/run-workspace.js @@ -2,6 +2,10 @@ import { buildOverviewProjection } from './overview.js'; // owner: RStack developed by Richardson Gunde +// #496: events-derived timelines cap at the same depth the rollup index +// keeps for persisted timelines (rollup-index.js slices to 120). +const EVENTS_TIMELINE_CAP = 120; + function readableTimeline(run) { const derived = [...(run.timeline ?? []), ...(run.activityTimeline ?? [])] .filter((item) => item && item.ts) @@ -21,7 +25,7 @@ function readableTimeline(run) { stage_id: event.stage_id ?? null, })) .sort((a, b) => String(a.ts).localeCompare(String(b.ts))) - .slice(-120); + .slice(-EVENTS_TIMELINE_CAP); return { items: events, source: events.length ? 'events' : 'none' }; } diff --git a/src/observability/dashboard/ui/pages/command-center.js b/src/observability/dashboard/ui/pages/command-center.js index ad1bea58..f441eb9f 100644 --- a/src/observability/dashboard/ui/pages/command-center.js +++ b/src/observability/dashboard/ui/pages/command-center.js @@ -131,6 +131,9 @@ function overviewStageIcon(state) { return ({ passed: '✓', failed: '!', blocked: '×', in_progress: '→', not_started: '○', unknown: '?' })[state] || '?'; } +// #498: rail source label keeps the goal to one readable line. +var PROOF_SOURCE_GOAL_CHARS = 54; + function renderOverviewProofRail(s) { var stages = (s.overview && s.overview.stages) || []; // #498: the rail names the run whose stages it shows — server-owned via @@ -140,7 +143,7 @@ function renderOverviewProofRail(s) { var sourceEl = document.getElementById('overview-proof-source'); if (sourceEl) { var label = stagesSource - ? 'Stage proof from ' + String(stagesSource.goal || stagesSource.runId).slice(0, 54) + ' · ' + stagesSource.runId + ? 'Stage proof from ' + String(stagesSource.goal || stagesSource.runId).slice(0, PROOF_SOURCE_GOAL_CHARS) + ' · ' + stagesSource.runId : ''; setText('overview-proof-source', label); sourceEl.hidden = !label;