| 1 | /** |
| 2 | * Observability probe for every imperative scroll write against the |
| 3 | * transcript viewport. The single-writer contract is enforced statically by |
| 4 | * scripts/check-single-scroll-writer.mjs; this probe is the runtime mirror: |
| 5 | * tests and diagnostics can observe who wrote, what kind of write, and where |
| 6 | * it landed, without intercepting the DOM. |
| 7 | */ |
| 8 | import { isFrontendDiagnosticsBuild } from "./frontendDiagnosticsBuild"; |
| 9 | import { recordFrontendDiagnostic } from "./frontendDiagnosticBridge"; |
| 10 | |
| 11 | export type TranscriptScrollWriteRecord = { |
| 12 | session?: string; |
| 13 | transaction?: number; |
| 14 | owner: string; |
| 15 | intent?: string; |
| 16 | requestedOffset?: number; |
| 17 | acceptedOffset?: number; |
| 18 | outcome?: string; |
| 19 | kind: "scrollTo" | "scrollBy" | "scrollToIndex" | "pinTail"; |
| 20 | top?: number; |
| 21 | index?: number | "LAST"; |
| 22 | source?: string; |
| 23 | phase?: "mount-anchor" | "correct-offset" | "initial" | "settle"; |
| 24 | scrollTop?: number; |
| 25 | scrollHeight?: number; |
| 26 | clientHeight?: number; |
| 27 | bottomDistance?: number; |
| 28 | mode?: string; |
| 29 | sequence?: number; |
| 30 | generation?: number; |
| 31 | ownershipEpoch?: number; |
| 32 | geometryRevision?: number; |
| 33 | transactionId?: number; |
| 34 | rejectedReason?: string; |
| 35 | settleFrame?: number; |
| 36 | offBottomFrames?: number; |
| 37 | stagnantFrames?: number; |
| 38 | }; |
| 39 | |
| 40 | type DiagnosticSink = (type: string, fields: Record<string, unknown>) => void; |
| 41 | let diagnosticSink: DiagnosticSink | undefined; |
| 42 | const CAPTURE_SCROLL_DIAGNOSTIC_DETAILS = isFrontendDiagnosticsBuild( |
| 43 | typeof __BUILD_CHANNEL__ === "string" ? __BUILD_CHANNEL__ : "development", |
| 44 | Boolean(import.meta.env?.DEV), |
| 45 | ); |
| 46 | |
| 47 | export function isTranscriptScrollDiagnosticsBuild(channel: string, development: boolean): boolean { |
| 48 | return isFrontendDiagnosticsBuild(channel, development); |
| 49 | } |
| 50 | |
| 51 | export function setTranscriptScrollDiagnosticSink(sink: DiagnosticSink): void { |
| 52 | diagnosticSink = sink; |
| 53 | } |
| 54 | |
| 55 | export function recordTranscriptScrollDiagnostic(type: string, fields: Record<string, unknown> = {}): void { |
| 56 | diagnosticSink?.(type, fields); |
| 57 | // Forward the same content-free geometry into the broader frontend timeline. |
| 58 | recordFrontendDiagnostic("transcript", `transcript.${type}`, fields); |
| 59 | // The bench harness (desktop/frontend/bench) installs this page-side hook to |
| 60 | // attach the diagnostic stream to replay failure output. |
| 61 | if (typeof window !== "undefined") window.__REASONIX_TRANSCRIPT_SCROLL_DIAGNOSTIC__?.(type, fields); |
| 62 | } |
| 63 | |
| 64 | declare global { |
| 65 | interface Window { |
| 66 | __REASONIX_TRANSCRIPT_SCROLL_WRITE__?: (write: TranscriptScrollWriteRecord) => void; |
| 67 | __REASONIX_TRANSCRIPT_SCROLL_DIAGNOSTIC__?: (type: string, fields: Record<string, unknown>) => void; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | export function noteTranscriptScrollWrite(write: TranscriptScrollWriteRecord): void { |
| 72 | if (CAPTURE_SCROLL_DIAGNOSTIC_DETAILS) { |
| 73 | recordTranscriptScrollDiagnostic("scroll-write", { |
| 74 | session: write.session, |
| 75 | transaction: write.transaction, |
| 76 | owner: write.owner, |
| 77 | intent: write.intent, |
| 78 | requestedOffset: write.requestedOffset, |
| 79 | acceptedOffset: write.acceptedOffset, |
| 80 | outcome: write.outcome, |
| 81 | writeKind: write.kind, |
| 82 | targetTop: write.top, |
| 83 | targetIndex: write.index, |
| 84 | source: write.source, |
| 85 | phase: write.phase, |
| 86 | scrollTop: write.scrollTop, |
| 87 | scrollHeight: write.scrollHeight, |
| 88 | clientHeight: write.clientHeight, |
| 89 | bottomDistance: write.bottomDistance, |
| 90 | mode: write.mode, |
| 91 | sequence: write.sequence, |
| 92 | generation: write.generation, |
| 93 | ownershipEpoch: write.ownershipEpoch, |
| 94 | geometryRevision: write.geometryRevision, |
| 95 | transactionId: write.transactionId, |
| 96 | rejectedReason: write.rejectedReason, |
| 97 | settleFrame: write.settleFrame, |
| 98 | offBottomFrames: write.offBottomFrames, |
| 99 | stagnantFrames: write.stagnantFrames, |
| 100 | }); |
| 101 | } |
| 102 | window.__REASONIX_TRANSCRIPT_SCROLL_WRITE__?.(write); |
| 103 | } |
| 104 |