You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A replay produces two execution graphs, and today the system constructs both and consults neither: the verdict classifies over flat call streams (divergence/mod.rs:2295 — "the verdict below is unaffected"), the dashboard rebuilds a tree from ledger span-path strings because a name-keyed merge of the raw graph forests pairs nothing (web/src/lib/spine.ts header), every run re-derives per-correlation structure from the raw tape, a run pulls the entire session (361 MB) to drive 3 correlations, and the kernel drives sequentially in record order although correlations are independent test cases by construction.
#60 (docs/design/graph-aligned-scorer.md) defines the comparison half: classification as alignment of two activation forests. This issue is the construction, storage, and delivery half, plus the full plan to land both without regressing the running system. The claim:
The activation forest is the recording's core artifact. The lowest unit of storage, selection, replay, and scoring is a single correlation: compaction materializes one forest per correlation with its boundary events attached, a run pulls exactly the correlations it drives, the kernel drives them in parallel in any order, and the scorer aligns forest against forest.
Target design
Graph as core artifact. Compaction materializes per-correlation activation forests — graph nodes, boundary events attached by (recording_run_id, graph_node_id), a named unattached annex, per-node subtree-event-count rollups — with balanced accounting asserted: every event attached or in the annex, every node in exactly one correlation forest or the ambient forest.
Per-correlation storage.sessions/v1/{id}/correlations/{corr}/{meta.json, tape.ndjsonl.zst, forest.json}, an ambient/ sibling (uncorrelated traffic, always in scope), an index/ of concatenated meta heads for one-GET listing, and a manifest that accretes correlation entries with per-correlation content hashes, written last. Meta heads carry the request vocabulary already captured on root nodes (EXECUTION_GRAPH_SELECTED_FIELDS: flow, connector, payment method, merchant, tenant, status) plus counts, gseq range, completeness, and seedability — the picker filters on metadata instead of matching ids.
Additive, early compaction. A tailing compactor seals correlations as they complete (ingress finalizer + quiesce window), while the session is still recording. Late arrivals reseal that one correlation loudly (meta.completeness); correlations overlapping drop-gaps carry possible_gap: true.
Runtime substrate first. Graph nodes state their task lineage; the tape's two sequence spaces are named and drop accounting becomes kind-aware; the record-only deja::http_incoming ingress span asymmetry — a live bug demoting rank-2 lookups on every replay today — is fixed at the producer; replay-side nodes carry run identity.
Parallel driving. Bounded concurrency across correlations, record order within one; KERNEL_PARALLELISM from the environment, default 1. The standing invariant is order-invariance: the same scope at K=1 recorded order and K=8 shuffled must produce identical verdicts and per-correlation outcomes.
Explicitly out of scope. Serving identity stays the lookup ladder; the landing tier stays flat; no cross-correlation alignment; no old-tape backfill (old tapes take the declared flat tier; re-record rather than migrate).
The tracks
Sizes are S/M/L. Stacks go up with gh stack. Every PR leaves the system green on its own; a PR whose behavior flips a default sits behind an environment gate with a declared fallback.
Track A — runtime substrate
A1 (M) feat(graph): stamp task lineage onto execution-graph nodes at close — every tape-carried node states its own bucket_id/fork_seq, read at span close so layer registration order cannot race it; v8→v9, serde-additive. After G2 pins the v8 fixture.
A2 (M) fix(writer): name the tape's sequence spaces and make drop accounting kind-aware — the boundary and graph counters stop sharing one drop ledger; markers gain per-space truth additively (ranges stays boundary-only and byte-compatible; graph_ranges is a new key), closing the doc/code contradiction in the compatible direction. After G2.
A3 (M) fix(ingress): replay drives requests under the same deja::http_incoming span as record — kills the systematic rank-2→rank-3 demotion and gives the replay forest the same ingress root the record forest has. Vendor-fork commit. Gate includes a before/after resolved_by_rank diff on the same tape, and a check that the oldest tape any environment still replays was recorded with the prefix (pre-prefix tapes are re-record-only, stated in the PR body).
A4 (S) fix(replay): stamp run identity onto replay-side graph nodes — symmetric attach identity for the forest builder; consumed defensively (see C5).
A5 (S) vendor pin bump — the explicit deployment step that activates A1/A2/A4 in candidate images. Owned here, gated on demo/run-self-check.sh 9/9 and the A3 rank histogram check. Without this item the plan can be fully green in CI while real graph-tier runs misalign — nothing else in any track owns it.
Track B — shared forest builder + per-correlation compaction
B1 (M) refactor(forest): extract activation-forest resolution primitives into a new deja-forest crate — parent map, root walking with dangling-parent promotion and the cycle guard, anchor closure, capture-vs-delivery gap classification; one shared home for compactor and orchestrator, without weakening the ScopedRecording/TapeSlot seam (the scope_invariant test survives).
B2 (M) feat(forest): shared builder — (nodes, events) → per-correlation forests, annex, rollups, balanced accounting — the pure function both sides call; construction fails on imbalance. The golden fixture pins the join contract: on a single-run stream, attach keys on graph_node_id alone, with run identity as a loud assertion rather than a join column (so the replay side works both before and after A5).
B3 (L) feat(compactor): per-correlation sealed layout (manifest v2) — correlation objects, meta heads, index, manifest last; v1 sessions stay readable as the declared flat tier. Ports G4's tier-equivalence test to the v2 path as part of the deletion of seal_session. Decides index shape (additive fields on CorrelationSummary rows) before F4's type mirror lands.
B4 (L) feat(compactor): tailing incremental compaction — finalizer + quiesce, late-arrival reseal, possible_gap, conditional-PUT manifest accretion. Pins the demo MinIO image tag; the startup self-test distinguishes "store ignores preconditions" (refuse) from "client never sent one" (config bug).
B5 (M) feat(s3): read the per-correlation sealed form — the v2 pull path plus the readers Track C needs. The in-process seal fallback goes behind DEJA_SEAL_LAYOUT (default v1) so B5 does not silently become the fleet's first v2 writer; an unknown manifest_version degrades to a declared landing-rescan fallback, never a hard parse error — otherwise a v2-sealed recording is bricked for every pre-B5 reader and rollback is one-way. The one-way-rollback property is stated in the PR body.
B6 (M) ops(compactor): compaction job image, single-writer contract, IAM — the tailer finally runs somewhere with the write permission the replay job was deliberately denied in fix(replay): stop a replay writing the tape store it is judged against #32 (s3:PutObject on sessions/v1/*); demo overlay runs it locally.
Runtime shape: an incremental job over the S3 landing tier — deliberately not a Kafka consumer. Vector stays the only Kafka consumer; the landing objects are the compactor's input queue and the S3 list is its poll. Three properties force this:
Landing is the accounting boundary. Ingest accounting requires an input that can be enumerated and re-read so every line becomes an output or a named drop; S3 objects are re-readable forever, Kafka offsets age out with retention.
Seals are content-addressed and must be reproducible.seal_id hashes the raw input lines — re-sealing after a compactor fix only works if the input still exists byte-for-byte.
A second consumer group is a second truth. Vector-with-acks defines what was durably recorded; a Kafka-consuming compactor could seal lines that never landed (or miss ones that did), diverging from the landing tier every fallback path rescans — the producer/consumer split this codebase's rules exist to kill.
The latency argument for Kafka is illusory: Vector batches to S3 on its own cadence, and the quiesce window for late-closing spans dominates end-to-end freshness anyway; the target is "replayable minutes after recorded," which polling meets. Deployment: a Kubernetes CronJob (1–2 min, concurrencyPolicy: Forbid as belt-and-braces — the conditional-PUT manifest write is the real single-writer guard) with its own service account; the compose demo runs the same binary as a looping service since compose has no cron; the existing CLI batch mode remains the degenerate one-pass case for ended sessions. Each pass is a pure function of S3 state (no offsets, no local state), so a crash before the manifest accretion leaves only idempotently-rewritable orphan objects, and promoting to a long-running single-replica tailer later is a deployment change, not a code change.
Track C — orchestrator consumption
C1 (S) fix(lifecycle): an empty record graph writes the note the scorecard reads, not stderr — standalone, lands immediately.
C2 (M) refactor(lifecycle): choose the run's correlations from the sealed index, tape scan as the declared fallback — one GET instead of a full tape scan; precondition for C3.
C3 (L) feat(orchestrator): pull only the scoped correlation objects + ambient — deps corrected to B3 + B5 + C2. Scoped pulls materialize into scope-keyed slots (recordings/{id}/scoped/{scope_hash}/…), never the shared canonical events.jsonl — a scoped tape in the shared slot is silently mistaken for the whole session by older readers and turns the today-benign concurrent re-pull race into a corrupting one. TapeSlot owns the new path so the seam grep still holds; the coverage sidecar is an attestation, not the guard.
C4 (M) feat(orchestrator): ScopedRecording::forests() — the record graph becomes a slice of the seal — extract_record_graph consumes the sealed slice; on-the-fly extraction stays as the declared flat tier. Owns migration 0007 (both record_forest and replay_forest artifact kinds) so C5 cannot collide on the migration number. Gate includes the demo smoke (lifecycle change), not just verify alone.
C5 (M) feat(lifecycle): materialize the replay-side forest from observed.jsonl through the shared builder — same code path as the record side; publishes the aligner's second input. Join per the B2 contract (node-id-only, run identity asserted), so it is correct during the A5 pin-lag window instead of annexing every event fleet-wide.
C6 — folded into F6, which owns the /graph response schema; C-track only produces and hydrates the artifacts.
D1 (L) feat(scorer): pure graph aligner over activation forests, with golden alignment tests — skeleton pruning via stored rollups, k-th same-named-child alignment, IdentitySkew reconciliation through source_event_global_sequence, suspected-rename shape flag, asserted balanced accounting. Goldens built from the shared synthetic generator (G0): wall correlation, clean correlation, request-only 400, the same-named loop where serving FIFO and alignment k disagree (asserts the skew is reported, not scored).
D2 (S) feat(scorer): load both execution graphs into RunArtifacts — record graph from the run's scoped artifact, replay graph split out of observed.jsonl in the same pass that yields observed calls.
D3 (L) feat(scorer): per-correlation graph-aligned scoring tier with declared scoring_mode — deps gain E4 (the order-invariance test must precede it) and a defensive rule: a one-sided ingress root demotes that correlation to flat with reason ingress_root_asymmetry, so D3 is correct even before A3/A5 reach the candidate image. All existing counters kept as projections; counter_disagreements() still balances. Gate: demo smoke with E2E_EXPECT_SCORING_MODE=graph.
D5 (M) feat(scorer): port rules A/B and race evidence onto graph facts — verdict-affecting: gate includes the orchestrator smoke, not just verify alone.
Track E — kernel parallel driving
E1 (S) feat(kernel): declared drive-order contract — recorded default, seeded shuffle for adversarial runs; boot-env docs tell the truth.
E2 (M) test(kernel): concurrent-drive integration harness — K=1 recorded vs K=8 shuffled yield identical per-correlation diff sets, in bare cargo test. The fixture must include null-correlation side-effect calls interleaved across correlations: uncorrelated traffic shares one global occurrence bucket, and the kernel's own comment says record order matters for it. The test adjudicates that case explicitly — either serving is fixed (shared-bucket occurrences consumed in source_event_global_sequence order) or the honest contract is pinned: K>1 refuses or loudly warns on tapes with uncorrelated side-effect traffic. K=1 stays the default until the K=2 smoke passes on a demo tape that demonstrably carries ambient traffic.
E3 (S) fix(runtime): FileObservedSink counts every dropped line and fails flush loudly — the let _ = write_all(...) swallow is a silent drop that would surface as fabricated omissions; fixed and pinned line-atomic under concurrent writers. Owns the sink test (absorbs the overlapping safety-net item).
E4 (M) test(orchestrator): scorer order-invariance golden — the single order_invariance.rs; detect() invariant to cross-correlation interleavings, with the within-correlation-order negative control. Merges before any D PR and before E2/E5.
E5 (M) feat(orchestrator): kernel parallelism and drive order are run parameters — resolved at creation, passed explicitly, stamped in run.params; re-POSTing a row re-runs identically.
Track F — API + dashboard
F1 (S) ci(web): vitest harness + first spine characterization tests — stacked on G1's web job (single owner of the first web CI job).
F2 (S) fix(dashboard): mirror HttpDiff.transport_error and the missing scorecard counters — live gap: the UI heuristically sniffs status 0 today.
F3 (M) fix(dashboard): demoted and inconclusive ledger kinds render as what they are, never as Matched — live bug: markOf() returns null for demoted kinds and the detail pane falls through to "values agree".
F7 (M) feat(replay): /graph carries alignment classifications and scoring_mode, read from the persisted artifact, never recomputed — deps: D3 (+D4).
F8 (L) feat(dashboard): the spine consumes forests and alignment; the ledger-string tree becomes the declared flat tier.
F9 (M) feat(dashboard): the report leads with subtree findings and names its scoring mode — the design doc's "two facts, not two thousand" claim, made real in the UI.
F10 (S) feat(dashboard): show a run's kernel parallelism from run.params — deps: E5.
Track G — safety net, CI, gates
G0 (M) test-support: shared synthetic tape/forest generator — new, land first among test infra: a dev-only crate generating tagged-NDJSON tapes and node/event sets with forks, same-named sibling loops, seeded interleavings, drop markers, and late-closing spans. Without it, five tracks each build private fixture builders that go stale independently when A1/A2 change the wire — the exact producer/consumer split this codebase's rules exist to kill. Run-0810-class evidence lives in sandbox S3; synthetic shapes are the only goldens hosted CI can run. Consumed by D1, E2, E4, G2, B2.
G1 (S) ci: web typecheck/build job + just targets — first web gate; F1 stacks on it.
G2 (M) test: golden v8 tape, scorecard-v1, and manifest fixtures pin the decode contract — must merge before A1/A2 or the fixture pins nothing; generated from G0 with a hand-audited field check, plus one small non-regeneratable demo-sourced line set.
G3 (S) test: compile the web Scorecard/ledger mirror against the golden fixtures — one fixture, two enforced copies (a byte-identity check keeps the web copy honest).
G4 (M) test: sealed fast path and landing rescan read back the same recording — written against compact_session (the surviving path), since B3 deletes seal_session; B3 ports it to v2-vs-landing equivalence as part of the deletion diff.
G5 — dissolved: the sink half is E3's, the scorer-permutation half is E4's. One owner per invariant.
G6 (M) ci(smoke): the demo compose run becomes the declared end-to-end gate with asserted facts — jq-checked assertions and named extension flags (E2E_EXPECT_RANK2, E2E_EXPECT_CONDITIONAL_PUT, E2E_EXPECT_SCORING_MODE) instead of human-read logs. Pulled forward to land before the B5/C3/D3 merge wave; smoke output pasted in PR bodies from then on. The smoke needs compose + a live Stripe key and cannot run in hosted CI — that is a named property of the gate, not a surprise.
G7 (S) docs: compatibility matrix, per-track merge gates, rollout/rollback contract — checked into the repo. One correction already known: the scorecard lives both as the FS artifact and as replay_runs.scorecard JSONB read back through the run row, so scorecard additivity must be pinned for both.
Merge order
Every wave leaves main releasable; the two behavior flips (storage layout, kernel parallelism) sit behind env flags with declared fallbacks.
The unowned deployment step was the biggest hole in the original breakdown: A1/A2/A4 activate only after the vendor pin bump, and D3/C5/B4 silently depended on it. A5 now owns it, and D3/C5 are written to be correct during the lag window (defensive ingress-root demotion; node-id-only join).
Storage cutover is one-way if unguarded. A v2 manifest written by an ungated fallback bricks the recording for every older reader. DEJA_SEAL_LAYOUT default v1 + tolerant unknown-version handling makes the failure mode a slow declared tier instead.
Scoped tapes must be structurally invisible to old readers — scope-keyed slots, not a sidecar contract that only new readers honor. Several sessions share DEJA_RECORDING_ROOT on the dev box; the mixed-version hazard is real there, not just in rolling deploys.
Parallel driving's stated premise has a known exception: null-correlation traffic is order-sensitive at K>1. The order-invariance fixture encodes it; the default stays K=1 until the contract is pinned and the K=2 smoke passes on a tape with ambient traffic.
The safety net must not be built twice. Single owners: E3 (sink), E4 (order invariance), G1+F1 (web CI), F6 (/graph schema), C4 (migration 0007), G0 (fixtures).
Open decisions
Whether serving fixes the shared null-correlation occurrence bucket (consume in source_event_global_sequence order) or the K>1 contract excludes uncorrelated-traffic tapes — settled by E2's adjudication.
Meta-head index shape: additive fields on CorrelationSummary rows (current lean) vs a replacement index — B3 decides, in writing, before F4 merges.
Packing threshold for very large sessions (individual correlation objects vs packed parts with ranged GETs) — ship individual files, state the threshold in the design doc.
Why
A replay produces two execution graphs, and today the system constructs both and consults neither: the verdict classifies over flat call streams (
divergence/mod.rs:2295— "the verdict below is unaffected"), the dashboard rebuilds a tree from ledger span-path strings because a name-keyed merge of the raw graph forests pairs nothing (web/src/lib/spine.tsheader), every run re-derives per-correlation structure from the raw tape, a run pulls the entire session (361 MB) to drive 3 correlations, and the kernel drives sequentially in record order although correlations are independent test cases by construction.#60 (
docs/design/graph-aligned-scorer.md) defines the comparison half: classification as alignment of two activation forests. This issue is the construction, storage, and delivery half, plus the full plan to land both without regressing the running system. The claim:The activation forest is the recording's core artifact. The lowest unit of storage, selection, replay, and scoring is a single correlation: compaction materializes one forest per correlation with its boundary events attached, a run pulls exactly the correlations it drives, the kernel drives them in parallel in any order, and the scorer aligns forest against forest.
Target design
(recording_run_id, graph_node_id), a named unattached annex, per-node subtree-event-count rollups — with balanced accounting asserted: every event attached or in the annex, every node in exactly one correlation forest or the ambient forest.sessions/v1/{id}/correlations/{corr}/{meta.json, tape.ndjsonl.zst, forest.json}, anambient/sibling (uncorrelated traffic, always in scope), anindex/of concatenated meta heads for one-GET listing, and a manifest that accretes correlation entries with per-correlation content hashes, written last. Meta heads carry the request vocabulary already captured on root nodes (EXECUTION_GRAPH_SELECTED_FIELDS: flow, connector, payment method, merchant, tenant, status) plus counts, gseq range, completeness, and seedability — the picker filters on metadata instead of matching ids.meta.completeness); correlations overlapping drop-gaps carrypossible_gap: true.deja::http_incomingingress span asymmetry — a live bug demoting rank-2 lookups on every replay today — is fixed at the producer; replay-side nodes carry run identity.detect()switches per correlation: graph-aligned when both forests exist, the span-scoped flat scorer otherwise, withscoring_modedeclared per correlation. The call ledger regenerates from the alignment, retiring the pre-fix(scorer): pairing is span-scoped and runs after every resolution is known #57 pairing identity and the "may be a mis-pairing" UI caveat.KERNEL_PARALLELISMfrom the environment, default 1. The standing invariant is order-invariance: the same scope at K=1 recorded order and K=8 shuffled must produce identical verdicts and per-correlation outcomes.The tracks
Sizes are S/M/L. Stacks go up with
gh stack. Every PR leaves the system green on its own; a PR whose behavior flips a default sits behind an environment gate with a declared fallback.Track A — runtime substrate
feat(graph): stamp task lineage onto execution-graph nodes at close— every tape-carried node states its ownbucket_id/fork_seq, read at span close so layer registration order cannot race it; v8→v9, serde-additive. After G2 pins the v8 fixture.fix(writer): name the tape's sequence spaces and make drop accounting kind-aware— the boundary and graph counters stop sharing one drop ledger; markers gain per-space truth additively (rangesstays boundary-only and byte-compatible;graph_rangesis a new key), closing the doc/code contradiction in the compatible direction. After G2.fix(ingress): replay drives requests under the same deja::http_incoming span as record— kills the systematic rank-2→rank-3 demotion and gives the replay forest the same ingress root the record forest has. Vendor-fork commit. Gate includes a before/afterresolved_by_rankdiff on the same tape, and a check that the oldest tape any environment still replays was recorded with the prefix (pre-prefix tapes are re-record-only, stated in the PR body).fix(replay): stamp run identity onto replay-side graph nodes— symmetric attach identity for the forest builder; consumed defensively (see C5).demo/run-self-check.sh9/9 and the A3 rank histogram check. Without this item the plan can be fully green in CI while real graph-tier runs misalign — nothing else in any track owns it.Track B — shared forest builder + per-correlation compaction
B1 (M)
refactor(forest): extract activation-forest resolution primitives into a new deja-forest crate— parent map, root walking with dangling-parent promotion and the cycle guard, anchor closure, capture-vs-delivery gap classification; one shared home for compactor and orchestrator, without weakening theScopedRecording/TapeSlotseam (thescope_invarianttest survives).B2 (M)
feat(forest): shared builder — (nodes, events) → per-correlation forests, annex, rollups, balanced accounting— the pure function both sides call; construction fails on imbalance. The golden fixture pins the join contract: on a single-run stream, attach keys ongraph_node_idalone, with run identity as a loud assertion rather than a join column (so the replay side works both before and after A5).B3 (L)
feat(compactor): per-correlation sealed layout (manifest v2)— correlation objects, meta heads, index, manifest last; v1 sessions stay readable as the declared flat tier. Ports G4's tier-equivalence test to the v2 path as part of the deletion ofseal_session. Decides index shape (additive fields onCorrelationSummaryrows) before F4's type mirror lands.B4 (L)
feat(compactor): tailing incremental compaction— finalizer + quiesce, late-arrival reseal,possible_gap, conditional-PUT manifest accretion. Pins the demo MinIO image tag; the startup self-test distinguishes "store ignores preconditions" (refuse) from "client never sent one" (config bug).B5 (M)
feat(s3): read the per-correlation sealed form— the v2 pull path plus the readers Track C needs. The in-process seal fallback goes behindDEJA_SEAL_LAYOUT(default v1) so B5 does not silently become the fleet's first v2 writer; an unknownmanifest_versiondegrades to a declared landing-rescan fallback, never a hard parse error — otherwise a v2-sealed recording is bricked for every pre-B5 reader and rollback is one-way. The one-way-rollback property is stated in the PR body.B6 (M)
ops(compactor): compaction job image, single-writer contract, IAM— the tailer finally runs somewhere with the write permission the replay job was deliberately denied in fix(replay): stop a replay writing the tape store it is judged against #32 (s3:PutObjectonsessions/v1/*); demo overlay runs it locally.Runtime shape: an incremental job over the S3 landing tier — deliberately not a Kafka consumer. Vector stays the only Kafka consumer; the landing objects are the compactor's input queue and the S3 list is its poll. Three properties force this:
seal_idhashes the raw input lines — re-sealing after a compactor fix only works if the input still exists byte-for-byte.The latency argument for Kafka is illusory: Vector batches to S3 on its own cadence, and the quiesce window for late-closing spans dominates end-to-end freshness anyway; the target is "replayable minutes after recorded," which polling meets. Deployment: a Kubernetes CronJob (1–2 min,
concurrencyPolicy: Forbidas belt-and-braces — the conditional-PUT manifest write is the real single-writer guard) with its own service account; the compose demo runs the same binary as a looping service since compose has no cron; the existing CLI batch mode remains the degenerate one-pass case for ended sessions. Each pass is a pure function of S3 state (no offsets, no local state), so a crash before the manifest accretion leaves only idempotently-rewritable orphan objects, and promoting to a long-running single-replica tailer later is a deployment change, not a code change.Track C — orchestrator consumption
fix(lifecycle): an empty record graph writes the note the scorecard reads, not stderr— standalone, lands immediately.refactor(lifecycle): choose the run's correlations from the sealed index, tape scan as the declared fallback— one GET instead of a full tape scan; precondition for C3.feat(orchestrator): pull only the scoped correlation objects + ambient— deps corrected to B3 + B5 + C2. Scoped pulls materialize into scope-keyed slots (recordings/{id}/scoped/{scope_hash}/…), never the shared canonicalevents.jsonl— a scoped tape in the shared slot is silently mistaken for the whole session by older readers and turns the today-benign concurrent re-pull race into a corrupting one.TapeSlotowns the new path so the seam grep still holds; the coverage sidecar is an attestation, not the guard.feat(orchestrator): ScopedRecording::forests() — the record graph becomes a slice of the seal—extract_record_graphconsumes the sealed slice; on-the-fly extraction stays as the declared flat tier. Owns migration 0007 (bothrecord_forestandreplay_forestartifact kinds) so C5 cannot collide on the migration number. Gate includes the demo smoke (lifecycle change), notjust verifyalone.feat(lifecycle): materialize the replay-side forest from observed.jsonl through the shared builder— same code path as the record side; publishes the aligner's second input. Join per the B2 contract (node-id-only, run identity asserted), so it is correct during the A5 pin-lag window instead of annexing every event fleet-wide.C6— folded into F6, which owns the/graphresponse schema; C-track only produces and hydrates the artifacts.Track D — the graph-aligned scorer (#60)
feat(scorer): pure graph aligner over activation forests, with golden alignment tests— skeleton pruning via stored rollups, k-th same-named-child alignment,IdentitySkewreconciliation throughsource_event_global_sequence, suspected-rename shape flag, asserted balanced accounting. Goldens built from the shared synthetic generator (G0): wall correlation, clean correlation, request-only 400, the same-named loop where serving FIFO and alignment k disagree (asserts the skew is reported, not scored).feat(scorer): load both execution graphs into RunArtifacts— record graph from the run's scoped artifact, replay graph split out ofobserved.jsonlin the same pass that yields observed calls.feat(scorer): per-correlation graph-aligned scoring tier with declared scoring_mode— deps gain E4 (the order-invariance test must precede it) and a defensive rule: a one-sided ingress root demotes that correlation to flat with reasoningress_root_asymmetry, so D3 is correct even before A3/A5 reach the candidate image. All existing counters kept as projections;counter_disagreements()still balances. Gate: demo smoke withE2E_EXPECT_SCORING_MODE=graph.feat(ledger): regenerate the call ledger from the alignment— retires the pre-fix(scorer): pairing is span-scoped and runs after every resolution is known #57(correlation, boundary, method)pairing and the UnifiedView caveat it necessitated.feat(scorer): port rules A/B and race evidence onto graph facts— verdict-affecting: gate includes the orchestrator smoke, notjust verifyalone.Track E — kernel parallel driving
feat(kernel): declared drive-order contract— recorded default, seeded shuffle for adversarial runs; boot-env docs tell the truth.test(kernel): concurrent-drive integration harness— K=1 recorded vs K=8 shuffled yield identical per-correlation diff sets, in barecargo test. The fixture must include null-correlation side-effect calls interleaved across correlations: uncorrelated traffic shares one global occurrence bucket, and the kernel's own comment says record order matters for it. The test adjudicates that case explicitly — either serving is fixed (shared-bucket occurrences consumed insource_event_global_sequenceorder) or the honest contract is pinned: K>1 refuses or loudly warns on tapes with uncorrelated side-effect traffic. K=1 stays the default until the K=2 smoke passes on a demo tape that demonstrably carries ambient traffic.fix(runtime): FileObservedSink counts every dropped line and fails flush loudly— thelet _ = write_all(...)swallow is a silent drop that would surface as fabricated omissions; fixed and pinned line-atomic under concurrent writers. Owns the sink test (absorbs the overlapping safety-net item).test(orchestrator): scorer order-invariance golden— the singleorder_invariance.rs;detect()invariant to cross-correlation interleavings, with the within-correlation-order negative control. Merges before any D PR and before E2/E5.feat(orchestrator): kernel parallelism and drive order are run parameters— resolved at creation, passed explicitly, stamped inrun.params; re-POSTing a row re-runs identically.Track F — API + dashboard
ci(web): vitest harness + first spine characterization tests— stacked on G1's web job (single owner of the first web CI job).fix(dashboard): mirror HttpDiff.transport_error and the missing scorecard counters— live gap: the UI heuristically sniffs status 0 today.fix(dashboard): demoted and inconclusive ledger kinds render as what they are, never as Matched— live bug:markOf()returns null for demoted kinds and the detail pane falls through to "values agree".fix(dashboard): the correlations client parses CorrelationSummary rows, not strings— live bug since feat(replay): seal a recording once, and bound a run to what it can list #28; lands immediately, after B3's index-shape decision is confirmed in writing so the row type is written once.feat(replay): correlations endpoint serves meta heads; picker filters by flow/connector/status— deps: B3 + B5 + F4.feat(replay): /runs/{id}/graph serves per-correlation forests + ambient, flat tier declared per correlation— deps: C4 + C5. Owns the response schema.feat(replay): /graph carries alignment classifications and scoring_mode, read from the persisted artifact, never recomputed— deps: D3 (+D4).feat(dashboard): the spine consumes forests and alignment; the ledger-string tree becomes the declared flat tier.feat(dashboard): the report leads with subtree findings and names its scoring mode— the design doc's "two facts, not two thousand" claim, made real in the UI.feat(dashboard): show a run's kernel parallelism from run.params— deps: E5.Track G — safety net, CI, gates
test-support: shared synthetic tape/forest generator— new, land first among test infra: a dev-only crate generating tagged-NDJSON tapes and node/event sets with forks, same-named sibling loops, seeded interleavings, drop markers, and late-closing spans. Without it, five tracks each build private fixture builders that go stale independently when A1/A2 change the wire — the exact producer/consumer split this codebase's rules exist to kill. Run-0810-class evidence lives in sandbox S3; synthetic shapes are the only goldens hosted CI can run. Consumed by D1, E2, E4, G2, B2.ci: web typecheck/build job + just targets— first web gate; F1 stacks on it.test: golden v8 tape, scorecard-v1, and manifest fixtures pin the decode contract— must merge before A1/A2 or the fixture pins nothing; generated from G0 with a hand-audited field check, plus one small non-regeneratable demo-sourced line set.test: compile the web Scorecard/ledger mirror against the golden fixtures— one fixture, two enforced copies (a byte-identity check keeps the web copy honest).test: sealed fast path and landing rescan read back the same recording— written againstcompact_session(the surviving path), since B3 deletesseal_session; B3 ports it to v2-vs-landing equivalence as part of the deletion diff.G5— dissolved: the sink half is E3's, the scorer-permutation half is E4's. One owner per invariant.ci(smoke): the demo compose run becomes the declared end-to-end gate with asserted facts—jq-checked assertions and named extension flags (E2E_EXPECT_RANK2,E2E_EXPECT_CONDITIONAL_PUT,E2E_EXPECT_SCORING_MODE) instead of human-read logs. Pulled forward to land before the B5/C3/D3 merge wave; smoke output pasted in PR bodies from then on. The smoke needs compose + a live Stripe key and cannot run in hosted CI — that is a named property of the gate, not a surprise.docs: compatibility matrix, per-track merge gates, rollout/rollback contract— checked into the repo. One correction already known: the scorecard lives both as the FS artifact and asreplay_runs.scorecardJSONB read back through the run row, so scorecard additivity must be pinned for both.Merge order
Every wave leaves main releasable; the two behavior flips (storage layout, kernel parallelism) sit behind env flags with declared fallbacks.
Risk register
DEJA_SEAL_LAYOUTdefault v1 + tolerant unknown-version handling makes the failure mode a slow declared tier instead.DEJA_RECORDING_ROOTon the dev box; the mixed-version hazard is real there, not just in rolling deploys./graphschema), C4 (migration 0007), G0 (fixtures).Open decisions
source_event_global_sequenceorder) or the K>1 contract excludes uncorrelated-traffic tapes — settled by E2's adjudication.CorrelationSummaryrows (current lean) vs a replacement index — B3 decides, in writing, before F4 merges.docs/design/unified-execution-graph.md, the construction/storage companion docs: graph-aligned scoring — classification as alignment of two execution trees #60's migration step 4 references) distills from this issue and lands with or before Wave 1.