Skip to content

Commit 39f9f86

Browse files
V48 (specification-implementation): Extract deposit synthesis lifecycle hook
Move resume/adopt/dispatch/reconcile synthesis effects into use-deposit-synthesis-lifecycle so DepositPageClient is orchestration-only (~660 LOC). Restore source-selection funnel analytics; document in NOTES.
1 parent f9f497c commit 39f9f86

4 files changed

Lines changed: 428 additions & 275 deletions

File tree

BITCODE_SPEC_V48_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,7 @@ activity hooks); option cards still nested in `DepositAssetPackOptions`.
13671367
| VCS inventory hook | `DepositSourceSelection/hooks/use-deposit-source-vcs.ts` |
13681368
| Option review/admission | `DepositPageClient/hooks/use-deposit-option-actions.ts` |
13691369
| Activity ledger record/anchor | `DepositPageClient/hooks/use-deposit-activity-recording.ts` |
1370+
| Synthesis lifecycle (resume/adopt/dispatch) | `DepositPageClient/hooks/use-deposit-synthesis-lifecycle.ts` |
13701371

13711372
### Phase 6 landing (executions corridor)
13721373

uapi/components/deposits/DepositPageClient/DepositPageClient.tsx

Lines changed: 30 additions & 275 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { useDepositNetworkDepositoryCount } from "./hooks/use-deposit-network-de
2626
import { useDepositUrlNavigation } from "./hooks/use-deposit-url-navigation";
2727
import { useDepositOptionActions } from "./hooks/use-deposit-option-actions";
2828
import { useDepositActivityRecording } from "./hooks/use-deposit-activity-recording";
29+
import { useDepositSynthesisLifecycle } from "./hooks/use-deposit-synthesis-lifecycle";
2930
import { DepositRouteStateAside } from "@/components/deposits/DepositRouteStateAside/DepositRouteStateAside";
3031
import { DepositPipelinesMaster } from "@/components/deposits/DepositPipelinesMaster/DepositPipelinesMaster";
3132
import { DepositSynthesisTelemetry } from "@/components/deposits/DepositSynthesisTelemetry/DepositSynthesisTelemetry";
@@ -54,7 +55,6 @@ import {
5455
import { BitcodeShellBridgeProvider } from "@/components/bitcode/layout/BitcodeShellBridge/BitcodeShellBridge";
5556
import {
5657
buildDepositRouteSession,
57-
writeDepositRouteStage,
5858
} from "@/components/deposits/models/deposit-route-model";
5959
import {
6060
DEPOSIT_HEADER_METRIC_EXPLAINERS,
@@ -81,10 +81,6 @@ import {
8181
import { buildDepositSourceCriticalitySignals } from "@/components/deposits/models/deposit-source-criticality";
8282
import { buildDepositRouteInput } from "@/components/deposits/models/deposit-route-input-builder";
8383
import { resolvePreferredSignerAddress } from "@/components/deposits/models/deposit-preferred-signer";
84-
import {
85-
adoptSelectionStatusFromRun,
86-
synthesisStatusFromRunRow,
87-
} from "@/components/deposits/models/deposit-run-status";
8884

8985
export default function DepositPageClient() {
9086
const { user } = useAuth();
@@ -179,7 +175,6 @@ export default function DepositPageClient() {
179175

180176
const synthesizeOptionsRef = useRef<(() => Promise<void>) | null>(null);
181177
const synthesisTelemetryRef = useRef<HTMLElement | null>(null);
182-
const lastAdoptedSelectionIdRef = useRef<string | null>(null);
183178
const lastTrackedSourceRef = useRef<string | null>(null);
184179

185180
const closePipelineDetail = useCallback(() => {
@@ -356,178 +351,43 @@ export default function DepositPageClient() {
356351
refreshLiveRuns,
357352
});
358353

359-
// Resume synthesized options when a dispatched/adopted run completes.
360-
useEffect(() => {
361-
if (
362-
(synthesisStatus !== "running" && synthesisStatus !== "complete") ||
363-
!synthesisRunId ||
364-
realSynthesis
365-
) {
366-
return;
367-
}
368-
if (
369-
synthesisActivity.error &&
370-
(synthesisExecutionMatchesRun || synthesisStreamError)
371-
) {
372-
setSynthesisStatus("failed");
373-
setSynthesisError(synthesisActivity.error);
374-
if (synthesisDispatchedAtMs !== null) {
375-
trackProductEvent({
376-
name: "deposit_synthesis_failed",
377-
data: {
378-
stage: "run",
379-
durationMs: Date.now() - synthesisDispatchedAtMs,
380-
},
381-
});
382-
}
383-
return;
384-
}
385-
if (!synthesisExecutionMatchesRun) return;
386-
const rowCompleted =
387-
String(
388-
(synthesisExecution as { status?: string } | null)?.status || "",
389-
).toLowerCase() === "completed";
390-
if (!synthesisActivity.isStreamingComplete && !rowCompleted) return;
391-
if (!synthesisRunExpectsOptions) {
392-
setSynthesisStatus("complete");
393-
return;
394-
}
395-
let cancelled = false;
396-
void (async () => {
397-
try {
398-
const res = await fetch(`/api/executions/history/${synthesisRunId}`);
399-
const data = await res.json().catch(() => null);
400-
const output = data?.run?.output as
401-
| { depositOptionSynthesis?: unknown; reviewProjections?: unknown }
402-
| undefined;
403-
const synthesis = output?.depositOptionSynthesis;
404-
if (!res.ok || !synthesis) {
405-
throw new Error("Synthesized options were not found for this run.");
406-
}
407-
if (cancelled) return;
408-
setRealSynthesis({
409-
synthesis: synthesis as NonNullable<typeof realSynthesis>["synthesis"],
410-
reviewProjections: Array.isArray(output?.reviewProjections)
411-
? (output!.reviewProjections as NonNullable<
412-
typeof realSynthesis
413-
>["reviewProjections"])
414-
: [],
415-
});
416-
setOptionsRequested(true);
417-
setSynthesisStatus("complete");
418-
if (synthesisDispatchedAtMs !== null) {
419-
const options = (synthesis as { options?: unknown[] }).options;
420-
trackProductEvent({
421-
name: "deposit_synthesis_completed",
422-
data: {
423-
optionCount: Array.isArray(options) ? options.length : 0,
424-
durationMs: Date.now() - synthesisDispatchedAtMs,
425-
},
426-
});
427-
}
428-
replaceDepositSearchParams(
429-
writeDepositRouteStage(readCurrentSearchParams(), "review-options"),
430-
);
431-
void refreshLiveRuns();
432-
} catch (error) {
433-
if (cancelled) return;
434-
setSynthesisStatus("failed");
435-
setSynthesisError(
436-
error instanceof Error
437-
? error.message
438-
: "Synthesis result not found.",
439-
);
440-
if (synthesisDispatchedAtMs !== null) {
441-
trackProductEvent({
442-
name: "deposit_synthesis_failed",
443-
data: {
444-
stage: "resume",
445-
durationMs: Date.now() - synthesisDispatchedAtMs,
446-
},
447-
});
448-
}
449-
}
450-
})();
451-
return () => {
452-
cancelled = true;
453-
};
454-
}, [
354+
const { handleSynthesizeOptions } = useDepositSynthesisLifecycle({
455355
synthesisStatus,
356+
setSynthesisStatus,
456357
synthesisRunId,
457-
synthesisRunExpectsOptions,
358+
setSynthesisRunId,
458359
realSynthesis,
459-
synthesisActivity.isStreamingComplete,
460-
synthesisActivity.error,
461-
synthesisDispatchedAtMs,
462-
synthesisExecution,
360+
setRealSynthesis,
361+
synthesisActivity,
463362
synthesisExecutionMatchesRun,
464363
synthesisStreamError,
364+
synthesisExecution,
365+
synthesisDispatchedAtMs,
366+
setSynthesisDispatchedAtMs,
367+
setSynthesisError,
368+
synthesisRunExpectsOptions,
369+
setSynthesisRunExpectsOptions,
370+
setOptionsRequested,
371+
setSynthesisLogScrolled,
372+
liveRuns,
373+
selectedRun,
465374
readCurrentSearchParams,
466-
refreshLiveRuns,
467375
replaceDepositSearchParams,
468-
]);
469-
470-
// Row-status reconciliation when SSE is quiet but the row is terminal.
471-
useEffect(() => {
472-
if (synthesisStatus !== "running" || !synthesisRunId) return;
473-
const run = liveRuns.find((candidate) => candidate.id === synthesisRunId);
474-
if (!run) return;
475-
const mapped = synthesisStatusFromRunRow(run);
476-
if (mapped.status === "running") return;
477-
setSynthesisStatus(mapped.status);
478-
setSynthesisError(mapped.error);
479-
if (synthesisDispatchedAtMs === null) return;
480-
if (mapped.status === "cancelled") {
481-
trackProductEvent({
482-
name: "deposit_synthesis_cancelled",
483-
data: { durationMs: Date.now() - synthesisDispatchedAtMs },
484-
});
485-
} else if (mapped.status === "failed") {
486-
trackProductEvent({
487-
name: "deposit_synthesis_failed",
488-
data: {
489-
stage: "run",
490-
durationMs: Date.now() - synthesisDispatchedAtMs,
491-
},
492-
});
493-
}
494-
}, [liveRuns, synthesisDispatchedAtMs, synthesisRunId, synthesisStatus]);
495-
496-
useEffect(() => {
497-
if (synthesisStatus !== "running" || !synthesisRunId) return;
498-
const interval = window.setInterval(() => {
499-
void refreshLiveRuns();
500-
}, 15_000);
501-
return () => window.clearInterval(interval);
502-
}, [refreshLiveRuns, synthesisRunId, synthesisStatus]);
503-
504-
// Master-detail adoption from URL selection.
505-
useEffect(() => {
506-
const run = selectedRun;
507-
if (!run?.id) {
508-
lastAdoptedSelectionIdRef.current = null;
509-
return;
510-
}
511-
if (lastAdoptedSelectionIdRef.current === run.id) return;
512-
lastAdoptedSelectionIdRef.current = run.id;
513-
if (run.id === synthesisRunId) return;
514-
if (synthesisDispatchedAtMs !== null && synthesisStatus === "running") {
515-
return;
516-
}
517-
setSynthesisRunId(run.id);
518-
setSynthesisRunExpectsOptions(
519-
run.contextSource === "deposit-option-synthesis",
520-
);
521-
setSynthesisDispatchedAtMs(null);
522-
setSynthesisLogScrolled(false);
523-
setRealSynthesis(null);
524-
setSynthesisError(null);
525-
setOptionsRequested(false);
526-
const mapped = adoptSelectionStatusFromRun(run);
527-
setSynthesisStatus(mapped.status);
528-
setSynthesisError(mapped.error);
529-
}, [selectedRun, synthesisRunId, synthesisDispatchedAtMs, synthesisStatus]);
376+
replaceDepositRouteTransaction,
377+
refreshLiveRuns,
378+
obfuscations,
379+
forcedInclusions,
380+
forcedExclusions,
381+
repositoryContext,
382+
depositoryDemandSignals: depositRouteInput.depositoryDemandSignals,
383+
readingDemandSignals: depositRouteInput.readingDemandSignals,
384+
existingDepositorySignals: depositRouteInput.existingDepositorySignals,
385+
synthesizeOptionsRef,
386+
synthesisTelemetryRef,
387+
});
530388

389+
// Funnel analytics: one source-safe event per distinct repository selection
390+
// per mount — provider + pin shape only, never the repository name.
531391
useEffect(() => {
532392
const fullName = repositoryContext?.selectedRepository?.fullName || null;
533393
if (!fullName || lastTrackedSourceRef.current === fullName) return;
@@ -571,111 +431,6 @@ export default function DepositPageClient() {
571431
setIsObfuscationsAnchorPopoverOpen,
572432
});
573433

574-
const handleSynthesizeOptions = useCallback(
575-
async (instructionsOverride?: string) => {
576-
const effectiveInstructions =
577-
typeof instructionsOverride === "string" && instructionsOverride.trim()
578-
? instructionsOverride
579-
: obfuscations;
580-
setSynthesisStatus("running");
581-
setSynthesisError(null);
582-
setRealSynthesis(null);
583-
const runId =
584-
typeof crypto !== "undefined" && "randomUUID" in crypto
585-
? crypto.randomUUID()
586-
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
587-
setSynthesisRunId(runId);
588-
setSynthesisRunExpectsOptions(true);
589-
setSynthesisDispatchedAtMs(Date.now());
590-
setSynthesisLogScrolled(false);
591-
592-
try {
593-
const response = await fetch("/api/deposit/synthesize-options", {
594-
method: "POST",
595-
headers: { "Content-Type": "application/json" },
596-
body: JSON.stringify({
597-
runId,
598-
repositoryFullName:
599-
repositoryContext?.selectedRepository?.fullName || null,
600-
sourceBranch: repositoryContext?.selectedBranch || null,
601-
sourceCommit: repositoryContext?.selectedCommit || null,
602-
obfuscations: effectiveInstructions,
603-
forcedInclusions,
604-
forcedExclusions,
605-
demandContext: [
606-
...depositRouteInput.depositoryDemandSignals.map(
607-
(signal) => signal.label,
608-
),
609-
...depositRouteInput.readingDemandSignals.map(
610-
(signal) => signal.label,
611-
),
612-
],
613-
depositoryDemandSignals: depositRouteInput.depositoryDemandSignals,
614-
readingDemandSignals: depositRouteInput.readingDemandSignals,
615-
existingDepositorySignals:
616-
depositRouteInput.existingDepositorySignals,
617-
}),
618-
});
619-
const payload = await response.json().catch(() => null);
620-
if (!response.ok || !payload?.ok) {
621-
throw new Error(
622-
typeof payload?.error === "string"
623-
? payload.error
624-
: "Deposit option synthesis failed.",
625-
);
626-
}
627-
trackProductEvent({
628-
name: "deposit_synthesis_dispatched",
629-
data: {
630-
hasObfuscations: Boolean(effectiveInstructions.trim()),
631-
forcedInclusionCount: forcedInclusions.length,
632-
forcedExclusionCount: forcedExclusions.length,
633-
demandSignalCount:
634-
depositRouteInput.depositoryDemandSignals.length +
635-
depositRouteInput.readingDemandSignals.length,
636-
},
637-
});
638-
void refreshLiveRuns().then(() => {
639-
replaceDepositRouteTransaction(runId);
640-
});
641-
} catch (error) {
642-
setSynthesisStatus("failed");
643-
setSynthesisError(
644-
error instanceof Error
645-
? error.message
646-
: "Deposit option synthesis failed.",
647-
);
648-
trackProductEvent({
649-
name: "deposit_synthesis_failed",
650-
data: { stage: "dispatch", durationMs: null },
651-
});
652-
}
653-
},
654-
[
655-
obfuscations,
656-
depositRouteInput.depositoryDemandSignals,
657-
depositRouteInput.existingDepositorySignals,
658-
depositRouteInput.readingDemandSignals,
659-
forcedExclusions,
660-
refreshLiveRuns,
661-
replaceDepositRouteTransaction,
662-
repositoryContext,
663-
forcedInclusions,
664-
],
665-
);
666-
667-
useEffect(() => {
668-
synthesizeOptionsRef.current = handleSynthesizeOptions;
669-
}, [handleSynthesizeOptions]);
670-
671-
useEffect(() => {
672-
if (!synthesisRunId || synthesisDispatchedAtMs === null) return;
673-
synthesisTelemetryRef.current?.scrollIntoView?.({
674-
behavior: "smooth",
675-
block: "start",
676-
});
677-
}, [synthesisRunId, synthesisDispatchedAtMs]);
678-
679434
const {
680435
handleOptionReviewDecision,
681436
handleToggleSelect,

0 commit comments

Comments
 (0)