Skip to content

Commit cf9c598

Browse files
V48 Gate 3 (specification-implementation): plural nav labels + selection always loads execution telemetry and resumed results
Nav: the route links now read 'Deposits' and 'Reads' (public nav + footer links), matching the plural routes; action verbs elsewhere (terminal actions, footer flow steps, docs prose) stay singular. Master-detail selection contract hardened on both pages: - Selecting ANY pipeline run adopts it into the Telemetry detail — a live run [re-]connects its stream, a historic run always replays its persisted events. Synthesis runs additionally resume synthesized options into review; other runs are a telemetry-only detail (kicker 'Pipeline run') that completes without an options fetch. - usePipelineExecution skips the SSE tail when the history row is already terminal (no more empty reconnect churn on historic rows). - Terminal rows adopt AT their terminal status instead of passing through a transient 'running' that relabeled/disabled the Synthesize button and animated the orb until hydration; the resume effect now also runs from 'complete' while options are unloaded. - An actively dispatched run owns the detail until the user actually changes selection (first liveRuns arrival can no longer re-point the detail at a historical row mid-dispatch). - /reads: a completed run's persisted output surfaces a 'Synthesized AssetPacks' summary (option ids, titles, covered-path counts) with a 'Review in Deposits' link into the /deposits review detail. Tests: telemetry-only adoption on /deposits, synthesized-packs resume on /reads, pluralized nav/footer assertions, URL-aware default history mocks echoing the requested run id. Spec: BITCODE_SPEC_V48_NOTES.md master-detail section extended with the selection-always-loads contract + plural nav-label law. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f51a7b2 commit cf9c598

10 files changed

Lines changed: 336 additions & 37 deletions

BITCODE_SPEC_V48_NOTES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,22 @@ become MASTER-DETAIL:
533533
fallback (completed rows without a completion event; interrupted rows via
534534
the orphan sweeper — 'interrupted' is now a terminal status for the SSE
535535
tail).
536+
- **Selection ALWAYS loads the execution's telemetry:** selecting ANY
537+
pipeline run adopts it into the detail — a live run [re-]connects its
538+
stream, a historic run replays its persisted events (the tail hook skips
539+
the SSE tail entirely when the history row is already terminal). A
540+
synthesis run additionally resumes its synthesized AssetPack options into
541+
review; any other run is a telemetry-only detail (kicker 'Pipeline run')
542+
that completes without an options fetch. Terminal rows adopt AT their
543+
terminal status — never through a transient 'running' (which would
544+
disable the dispatch button and animate the orb until hydration). An
545+
actively dispatched run owns the detail until the user actually changes
546+
selection. On /reads, a completed run's persisted output additionally
547+
surfaces a 'Synthesized AssetPacks' summary linking back to the /deposits
548+
review detail.
549+
- **Nav route labels are plural** ('Deposits', 'Reads') — matching the
550+
routes they open; action verbs elsewhere (terminal actions, footer flow
551+
steps, docs prose) stay singular.
536552
- **Known read-gate gap:** read-lens pipeline runs dispatched through the
537553
sandbox harness do not yet persist their own lens-stamped executions rows
538554
(structured mode); the /reads table lists what exists and the telemetry

uapi/app/deposits/DepositPageClient.tsx

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ export default function DepositPageClient() {
155155
const [protectedIpExclusionsText, setProtectedIpExclusionsText] = useState("");
156156
const [optionsRequested, setOptionsRequested] = useState(false);
157157
const [synthesisRunId, setSynthesisRunId] = useState<string | null>(null);
158+
// Whether the attached run is an option synthesis (its completed output
159+
// carries synthesized AssetPack options to resume into review) or any
160+
// other pipeline execution (telemetry-only detail).
161+
const [synthesisRunExpectsOptions, setSynthesisRunExpectsOptions] =
162+
useState(true);
158163
// Master-detail pipelines table: lens-preset filters + pagination for the
159164
// Deposits run table (selection itself lives in the URL transactionId).
160165
const [pipelineFilters, setPipelineFilters] = useState<TransactionFilters>({
@@ -616,7 +621,16 @@ export default function DepositPageClient() {
616621
// streamed run completes, read the persisted synthesis from the execution row
617622
// output and surface the reviewable options; a streamed error fails the run.
618623
useEffect(() => {
619-
if (synthesisStatus !== "running" || !synthesisRunId || realSynthesis) return;
624+
// Runs while a run is attached and its results are not resumed yet:
625+
// 'running' covers live tails, 'complete' covers adopted terminal rows
626+
// whose persisted output still needs loading (realSynthesis guards
627+
// against re-fetch loops once the options are in).
628+
if (
629+
(synthesisStatus !== "running" && synthesisStatus !== "complete") ||
630+
!synthesisRunId ||
631+
realSynthesis
632+
)
633+
return;
620634
// A hook-level fetch error (history unavailable) fails the run outright;
621635
// an event-derived error counts only once the events are attributed to
622636
// THIS run (execution match) — otherwise a previously selected run's
@@ -635,6 +649,12 @@ export default function DepositPageClient() {
635649
String((synthesisExecution as { status?: string } | null)?.status || "").toLowerCase() ===
636650
"completed";
637651
if (!synthesisActivity.isStreamingComplete && !rowCompleted) return;
652+
if (!synthesisRunExpectsOptions) {
653+
// Telemetry-only run (not an option synthesis): the replayed history
654+
// IS the resumed result — freeze the clock, nothing to load.
655+
setSynthesisStatus("complete");
656+
return;
657+
}
638658
let cancelled = false;
639659
void (async () => {
640660
try {
@@ -674,6 +694,7 @@ export default function DepositPageClient() {
674694
}, [
675695
synthesisStatus,
676696
synthesisRunId,
697+
synthesisRunExpectsOptions,
677698
realSynthesis,
678699
synthesisActivity.isStreamingComplete,
679700
synthesisActivity.error,
@@ -701,27 +722,36 @@ export default function DepositPageClient() {
701722
}
702723
}, [liveRuns, synthesisRunId, synthesisStatus]);
703724

704-
// Master-detail adoption: selecting a synthesis pipeline run in the table
705-
// connects the Telemetry + Options detail to it. A RUNNING run reattaches
706-
// its live stream (usePipelineExecution tails any runId); a COMPLETED run
707-
// resumes its persisted results through the same completion effect (its
708-
// history already carries the terminal event, so the effect resolves
709-
// immediately); a failed/interrupted run surfaces its terminal state with
710-
// the historical log attached. Adoption fires only on SELECTION-ID
711-
// transitions: a fresh dispatch changes synthesisRunId while the URL
712-
// selection is still the previous run, and re-adopting that stale
713-
// selection would clobber the just-dispatched run.
725+
// Master-detail adoption: selecting ANY pipeline run in the table connects
726+
// the Telemetry detail to it. A RUNNING run reattaches its live stream
727+
// (usePipelineExecution tails any runId); a COMPLETED run replays its
728+
// persisted history, and a synthesis run additionally resumes its
729+
// synthesized options into review through the completion effect; a
730+
// failed/interrupted run surfaces its terminal state with the historical
731+
// log attached. Adoption fires only on SELECTION-ID transitions: a fresh
732+
// dispatch changes synthesisRunId while the URL selection is still the
733+
// previous run, and re-adopting that stale selection would clobber the
734+
// just-dispatched run.
714735
const lastAdoptedSelectionIdRef = useRef<string | null>(null);
715736
useEffect(() => {
716737
const run = selectedRun;
717-
if (!run?.id || run.contextSource !== "deposit-option-synthesis") {
718-
lastAdoptedSelectionIdRef.current = run?.id ?? null;
738+
if (!run?.id) {
739+
lastAdoptedSelectionIdRef.current = null;
719740
return;
720741
}
721742
if (lastAdoptedSelectionIdRef.current === run.id) return;
722743
lastAdoptedSelectionIdRef.current = run.id;
723744
if (run.id === synthesisRunId) return;
745+
// An actively dispatched run owns the detail until the user actually
746+
// changes selection — the FIRST liveRuns arrival after a fast dispatch
747+
// must not re-point the detail at a historical row.
748+
if (synthesisDispatchedAtMs !== null && synthesisStatus === "running") {
749+
return;
750+
}
724751
setSynthesisRunId(run.id);
752+
setSynthesisRunExpectsOptions(
753+
run.contextSource === "deposit-option-synthesis",
754+
);
725755
setSynthesisDispatchedAtMs(null);
726756
setSynthesisLogScrolled(false);
727757
setRealSynthesis(null);
@@ -733,10 +763,16 @@ export default function DepositPageClient() {
733763
setSynthesisError(
734764
run.summary ? `Run ${status}${run.summary}` : `Run ${status}.`,
735765
);
766+
} else if (status === "completed") {
767+
// Adopt a completed row AT its terminal status — never pass through a
768+
// transient 'running' (it would relabel/disable the dispatch button
769+
// and animate the orb until the tail hydrates). The completion effect
770+
// also resumes from 'complete' while options are not loaded yet.
771+
setSynthesisStatus("complete");
736772
} else {
737773
setSynthesisStatus("running");
738774
}
739-
}, [selectedRun, synthesisRunId]);
775+
}, [selectedRun, synthesisRunId, synthesisDispatchedAtMs, synthesisStatus]);
740776

741777
const sessionRows = [
742778
{
@@ -885,6 +921,7 @@ export default function DepositPageClient() {
885921
? crypto.randomUUID()
886922
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
887923
setSynthesisRunId(runId);
924+
setSynthesisRunExpectsOptions(true);
888925
setSynthesisDispatchedAtMs(Date.now());
889926
setSynthesisLogScrolled(false);
890927

@@ -955,13 +992,16 @@ export default function DepositPageClient() {
955992
synthesizeOptionsRef.current = handleSynthesizeOptions;
956993
}, [handleSynthesizeOptions]);
957994

995+
// Auto-scroll to Telemetry only for DISPATCHED runs (dispatch stamps the
996+
// timestamp; table adoption clears it) — adopting a row on page load or a
997+
// table click must not yank the viewport away from where the user is.
958998
useEffect(() => {
959-
if (!synthesisRunId) return;
999+
if (!synthesisRunId || synthesisDispatchedAtMs === null) return;
9601000
synthesisTelemetryRef.current?.scrollIntoView?.({
9611001
behavior: "smooth",
9621002
block: "start",
9631003
});
964-
}, [synthesisRunId]);
1004+
}, [synthesisRunId, synthesisDispatchedAtMs]);
9651005

9661006
// Secondary per-option actions only: Archive (rejected-by-depositor) and
9671007
// Resynthesize. Approval/deposit is a single batch call (handleDepositSelected).
@@ -1450,7 +1490,9 @@ export default function DepositPageClient() {
14501490
<div className="flex flex-wrap items-center justify-between gap-3">
14511491
<div className="min-w-0">
14521492
<p className="text-[0.68rem] uppercase tracking-[0.22em] text-emerald-200/80">
1453-
Asset Pack Synthesis
1493+
{synthesisRunExpectsOptions
1494+
? "Asset Pack Synthesis"
1495+
: "Pipeline run"}
14541496
</p>
14551497
<h2 className="mt-2 flex items-center gap-2 text-lg font-semibold text-white">
14561498
<span>Telemetry</span>

uapi/app/reads/ReadPageClient.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ export default function ReadPageClient() {
109109
// Telemetry error dismissal is per selected run.
110110
const [dismissedTelemetryErrorRunId, setDismissedTelemetryErrorRunId] =
111111
useState<string | null>(null);
112+
// Resumed results for a COMPLETED selected run: its persisted output may
113+
// carry synthesized AssetPack options (a synthesis run selected here).
114+
const [selectedRunPacks, setSelectedRunPacks] = useState<{
115+
runId: string;
116+
options: Array<{
117+
optionId: string;
118+
title: string;
119+
coveredSourcePathCount: number;
120+
}>;
121+
} | null>(null);
112122

113123
const readCurrentSearchParams = useCallback(
114124
() =>
@@ -227,6 +237,61 @@ export default function ReadPageClient() {
227237
return Number.isFinite(parsed) ? parsed : null;
228238
}, [readRunEvents, readRunIsProcessing]);
229239

240+
// Resume a completed run's synthesized AssetPacks: load the persisted
241+
// output and summarize the options alongside the replayed telemetry (the
242+
// full review/admission detail lives on /deposits). Best-effort — a run
243+
// without synthesized options simply shows telemetry only.
244+
useEffect(() => {
245+
setSelectedRunPacks(null);
246+
if (!selectedPipelineRunId || selectedRun?.status !== "completed") return;
247+
let cancelled = false;
248+
void (async () => {
249+
try {
250+
const res = await fetch(
251+
`/api/executions/history/${selectedPipelineRunId}`,
252+
);
253+
const data = await res.json().catch(() => null);
254+
if (cancelled || !res?.ok) return;
255+
const output = data?.run?.output as {
256+
depositOptionSynthesis?: {
257+
options?: Array<{ optionId?: string; title?: string }>;
258+
};
259+
reviewProjections?: Array<{
260+
optionId?: string;
261+
title?: string;
262+
coveredSourcePaths?: string[];
263+
}>;
264+
} | null;
265+
const projections = Array.isArray(output?.reviewProjections)
266+
? output.reviewProjections
267+
: [];
268+
const fallback = Array.isArray(output?.depositOptionSynthesis?.options)
269+
? output.depositOptionSynthesis.options
270+
: [];
271+
const options = (projections.length > 0 ? projections : fallback)
272+
.map((option) => ({
273+
optionId: String(option?.optionId || ""),
274+
title: String(option?.title || "Untitled AssetPack option"),
275+
coveredSourcePathCount: Array.isArray(
276+
(option as { coveredSourcePaths?: string[] })
277+
?.coveredSourcePaths,
278+
)
279+
? (option as { coveredSourcePaths: string[] })
280+
.coveredSourcePaths.length
281+
: 0,
282+
}))
283+
.filter((option) => option.optionId);
284+
if (cancelled || options.length === 0) return;
285+
setSelectedRunPacks({ runId: selectedPipelineRunId, options });
286+
} catch {
287+
// Telemetry replay stands on its own; the packs summary is additive.
288+
}
289+
})();
290+
return () => {
291+
cancelled = true;
292+
};
293+
}, [selectedPipelineRunId, selectedRun?.status]);
294+
230295
const depositedSourceRevision =
231296
useMemo<TerminalDepositedSourceRevision | null>(() => {
232297
const selectedRepository = repositoryContext?.selectedRepository || null;
@@ -743,6 +808,40 @@ export default function ReadPageClient() {
743808
}}
744809
/>
745810
</div>
811+
{selectedRunPacks?.runId === selectedPipelineRunId ? (
812+
<div
813+
data-testid="reads-synthesized-packs"
814+
className="mt-4 border border-emerald-300/15 bg-emerald-300/[0.05] px-3 py-3"
815+
>
816+
<p className="font-mono text-[0.62rem] uppercase tracking-[0.16em] text-emerald-200/80">
817+
Synthesized AssetPacks · {selectedRunPacks.options.length}
818+
</p>
819+
<ul className="mt-2 space-y-1 text-sm leading-6 text-neutral-200">
820+
{selectedRunPacks.options.map((option) => (
821+
<li
822+
key={option.optionId}
823+
className="flex flex-wrap items-baseline gap-x-3 gap-y-1"
824+
>
825+
<span className="font-mono text-[0.68rem] text-neutral-500">
826+
{option.optionId}
827+
</span>
828+
<span>{option.title}</span>
829+
{option.coveredSourcePathCount > 0 ? (
830+
<span className="text-xs text-neutral-500">
831+
{option.coveredSourcePathCount} source paths
832+
</span>
833+
) : null}
834+
</li>
835+
))}
836+
</ul>
837+
<Link
838+
href={`/deposits?transactionId=${encodeURIComponent(selectedRunPacks.runId)}&depositStage=review-options`}
839+
className="mt-3 inline-flex items-center border border-emerald-300/20 bg-emerald-300/10 px-3 py-2 text-xs font-medium text-emerald-100 transition hover:border-emerald-200/40 hover:bg-emerald-300/15"
840+
>
841+
Review in Deposits
842+
</Link>
843+
</div>
844+
) : null}
746845
</section>
747846
) : null}
748847

uapi/components/base/bitcode/layout/bitcode-public-copy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ export const BITCODE_PUBLIC_COPY = {
105105
userCta: 'Open Auxillaries',
106106
links: {
107107
network: 'Packs',
108-
deposit: 'Deposit',
109-
read: 'Read',
108+
deposit: 'Deposits',
109+
read: 'Reads',
110110
transactions: 'Packs',
111111
docs: 'Docs',
112112
github: 'Bitcode on GitHub',
@@ -115,8 +115,8 @@ export const BITCODE_PUBLIC_COPY = {
115115
publicNav: {
116116
links: [
117117
{ href: '/packs', label: 'Packs' },
118-
{ href: '/deposits', label: 'Deposit' },
119-
{ href: '/reads', label: 'Read' },
118+
{ href: '/deposits', label: 'Deposits' },
119+
{ href: '/reads', label: 'Reads' },
120120
{ href: '/docs', label: 'Docs' },
121121
],
122122
guestPrimaryCta: 'Open Auxillaries',

uapi/hooks/usePipelineExecution.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ export function usePipelineExecution(runId: string | null): UsePipelineExecution
9595
// initial history hydration, not the tail's lifetime.
9696
setIsLoading(false);
9797

98+
// A terminal row's history IS the complete telemetry: replay it
99+
// without opening the live tail (the run can produce no further
100+
// events, so the tail would only burn empty reconnects).
101+
const rowStatus = String((data.run as { status?: string } | null)?.status || '').toLowerCase();
102+
if (
103+
rowStatus === 'completed' ||
104+
rowStatus === 'failed' ||
105+
rowStatus === 'interrupted' ||
106+
rowStatus === 'cancelled'
107+
) {
108+
return;
109+
}
110+
98111
try {
99112
const last = (data.events || []).slice(-1)[0];
100113
let cursorTs = last?.created_at || '';

uapi/tests/depositPageClient.test.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,28 @@ describe("DepositPageClient", () => {
171171
items: [],
172172
},
173173
]);
174-
global.fetch = jest.fn();
174+
// Selecting any pipeline run adopts it into the Telemetry detail, whose
175+
// tail hydrates from the history endpoint — the default mock answers
176+
// those with a bare completed row (echoing the requested id) so tests
177+
// that never dispatch still settle cleanly.
178+
global.fetch = jest.fn((input: unknown) => {
179+
const url = String(input);
180+
if (url.includes("/api/executions/history/")) {
181+
return Promise.resolve({
182+
ok: true,
183+
status: 200,
184+
json: async () => ({
185+
run: { id: url.split("/").pop(), status: "completed", output: {} },
186+
events: [],
187+
}),
188+
});
189+
}
190+
return Promise.resolve({
191+
ok: false,
192+
status: 404,
193+
json: async () => null,
194+
});
195+
}) as unknown as typeof fetch;
175196
});
176197

177198
afterEach(() => {
@@ -897,4 +918,23 @@ describe("DepositPageClient", () => {
897918
const alert = await screen.findByRole("alert");
898919
expect(alert).toHaveTextContent("Repository ownership check failed.");
899920
});
921+
922+
it("adopts a selected non-synthesis pipeline run as a telemetry-only detail (no options, no failure)", async () => {
923+
// Default fixtures: selection deposit-1, a COMPLETED composer run (not an
924+
// option synthesis). Selecting it must attach its replayed telemetry at
925+
// its terminal status without attempting an options resume.
926+
render(<DepositPageClient />);
927+
928+
const telemetry = await screen.findByTestId("deposit-synthesis-telemetry");
929+
expect(telemetry).toHaveTextContent("Pipeline run");
930+
expect(telemetry).not.toHaveTextContent("Asset Pack Synthesis");
931+
expect(telemetry).toHaveTextContent("deposit-1");
932+
await waitFor(() =>
933+
expect(screen.getByTestId("pipeline-execution-log")).toHaveAttribute(
934+
"data-processing",
935+
"false",
936+
),
937+
);
938+
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
939+
});
900940
});

0 commit comments

Comments
 (0)