Render-truth backbone: Rust fips-render producer + causal ledger in the per-event timeline
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 1
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
Render-truth backbone: move the render-frame producer to Rust + put the causal ledger in the per-event timeline
Summary
Today the versioned render stream (render-frame-v1alpha1) is produced client-side in Swift, and the exact per-cause causal ledger never enters the per-event timeline (it exists only on the final RunArtifact). This issue proposes the right long-term move:
- Move the render-frame producer into a pure Rust
fips-rendercrate — a deterministicproject(trace, fidelity, provenance) -> Vec<RenderFrame>fold over the ordered event trace — so render truth is derived from the same Rust source as everything else and cannot drift from the engine. - Put the causal ledger waterfall into the replayable per-event timeline (as per-event deltas), so a run can be scrubbed/seeked to an exact causal state without re-running, and the flame/stage views replay from the stream instead of a bolted-on artifact summary.
Both require a render-frame-v1alpha2 schema cut and a run-hash/golden regeneration. This is intentionally deferred behind a lean Swift-first visualization pass (see "Relationship to current work"); it is filed now so the rationale and investigation aren't lost.
Related: closed #56 (M6 artifact query/downsampling). Builds on the renderer-truth work already on main (feat: independently verify renderer semantics).
Background — what prompted this
We ran a visualization-coverage audit of the Wind Tunnel: the app's purpose is to visualize deterministic mesh-protocol simulations (observe exact causal propagation, congestion, protocol limits), but a lot of important simulation state the engine computes is invisible on the canvas or buried in panels. While scoping how to surface it, we mapped the render data path end to end and found two structural facts that motivate this issue.
What we investigated (findings)
The render-frame producer already exists — in Swift, not Rust. FIPSDPackage/Sources/FIPSDFeature/RenderFrame.swift folds raw SimulationState into the exact render-frame-v1alpha1 shape (nodes, edges, cohorts, reconciliation, violations), including world coordinates via a splitmix64 radial hash, and the workbench writes render-frames.v1.jsonl evidence. So render-frame-v1alpha1 is not an unproduced contract — but the producer is a Swift reimplementation that:
- can drift from Rust truth (two independent projections of the same semantics),
- sees only top-level event fields (it can't reach engine-internal state the events don't already carry),
- and is duplicated effort every time the render model evolves.
The causal ledger is computed exactly but never enters the timeline. The engine's add_ledger calls (scattered through the handlers, e.g. crates/fips-engine/src/engine_tree.rs) build a complete per-cause waterfall (requested → coalesced → constructed → transmitted → delivered/rejected). But it is assembled onto the final RunArtifact only (crates/fips-engine/src/engine_finish.rs); a naive "replay the event stream" viewer never sees it. Relatedly, LedgerEntry.causal_parent is currently always None, and partition/node-loss blast-radius counts (sessions-disrupted, caches-invalidated) are ledger-only, absent from the events that cause them.
Wire/const fidelity is high and pinned. Separately confirmed during the audit: wire sizes are pinned to fixtures generated by the real FIPS serializers, and the engine follows executable codecs over prose. This is why a Rust projector is attractive — render truth should be derived on the same trusted Rust side, gated by parity, rather than re-derived in Swift.
The long-term move (proposed design)
A. fips-render crate (kill drift)
- New crate
crates/fips-renderexposing pure deterministicproject(trace, fidelity, provenance) -> Vec<RenderFrame>folding the already-orderedEventRecordtrace. - Not in the engine hot loop (render truth is a separate concern/cadence); not left in Swift (that's the drift source).
- Port world_x/world_y layout into the projector so the evidence log is authoritative. Live-window fit stays client-side (
WorldViewport/RenderFrame.positions(in:)). Swift decodes frames into the existingRenderNode/RenderCohorttypes; client-side derivation is deleted once byte-parity is proven.
B. Causal ledger in the timeline (enable causal replay)
- Instrument
add_ledgerto capture per-event ledger mutations; flush them into a newEventRecord.ledger_deltafield (skip_serializing_ifempty so unrelated runs stay byte-identical). - The projector accumulates cumulative totals; seek = sum of deltas ≤ ordinal, giving the absolute causal waterfall at any point without replay.
- Populate
LedgerEntry.causal_parent(today alwaysNone) via an explicit primary-cause rule for multi-cause events — one mechanism feeding both the timeline and the end-of-run aggregate.
C. render-frame-v1alpha2 schema
v1alpha1 objects are additionalProperties:false + all-required, so any addition is breaking → clean version cut. The v2 additions also make room for the visualization signals the audit wants surfaced (these can land incrementally once the projector owns them):
- Congestion on
link:queue_occupancy_bytes,queue_capacity_bytes,congestion_ratio,bandwidth_bps,latency_ns,loss_ppm,mtu_bytes,shared_medium_group. - Cause-of-inactivity on node+link:
inactive_causeenum (node-death|partition|transport-class-failure|link-conditions),inactive_cause_event_id,intervention_id. - Bloom new directional
bloom_edges[]:from/to/edge_id,role,occupied_bits,size_bits,hash_count,fill_ratio,fpr_ppb,estimated_cardinality,saturated(null under Occupancy/SparseBits fidelity),fresh. - Adversarial on node:
sybil_descended(sticky),sybil_ordinal,attacker_operations,transport_class_faulted,fault_class. - Lookup new
lookups[]:flow_id,origin,attempt,ttl,path_len,outcomeenum (in-flight|succeeded|retry|ttl-exhausted|failed-discovery),retry_at_ns. - Causal ledger per-frame:
causal_ledger{ deltas[{causal_id, causal_parent, stage, delta, cumulative, event_id, evidence}], stage_order }. - Plus emit bloom
size_bits/hash_countfrom the engine (static filter params).
Rationale — why this is the right long-term move
- Single source of render truth. Deriving frames in Rust from the same trace the engine emits removes an entire class of Swift/Rust divergence bugs; the evidence log becomes the authority, gated by parity.
- Causal replay is the product thesis. The app exists to "observe exact causal propagation." Putting the ledger in the timeline is what makes seek/scrub/flame views replay faithfully rather than reconstruct from a summary.
- Extensibility. New render signals become projector fields validated against a versioned schema, instead of ad-hoc Swift reconstructions.
- It compounds with existing renderer-truth work already on
main.
Relationship to current work (why this is deferred)
We are shipping the user-visible goal first via a lean, all-Swift pass: surface congestion, cause-of-failure, adversarial lineage, lookup-failure, and a bloom overlay/inspector on the canvas using the existing Swift producer, deriving fields from the already-replayable event stream (e.g. bloom role from tree topology; size_bits/hash_count are constants). That path needs zero engine changes, no schema bump, and no artifact re-hashing, so it delivers value fast and low-risk.
This issue is the backbone those signals should eventually sit on. Doing the lean pass first does not burn any bridges: the projector and causal-timeline remain independently justified, and the Swift field-derivations become parity references for the Rust projector.
Risks / decisions
- Golden blast radius / run-hash break.
ledger_deltashifts trace bytes → changes the run hash (engine_finish.rs) → everyrun_id/artifact_idfor runs that exercise ledger deltas.skip_serializing_ifempty keeps current runs byte-identical, but this is inherent once the ledger is in the timeline. Decision (owner): accept and regenerate all goldens via the blessed generators (no fidelity-flag gating). - Cross-language layout parity. splitmix64 + cos/sin can diverge at the last ULP between Swift and Rust. Gate on a byte-parity test; prefer fixed/normalized space; if exact parity is unreachable, ratify a quantized comparison explicitly.
- Multi-cause events need an explicit primary-cause rule for
causal_parent. - Bloom
saturatedis only valid under ExactBits fidelity (null otherwise).
Phased plan
- P0 — engine enrichment.
EventRecord.ledger_delta(skip-if-empty) instrumented atadd_ledger; populateLedgerEntry.causal_parentvia the primary-cause rule; emit bloomsize_bits/hash_count. Regenerate goldens. - P1 —
fips-rendercrate + byte-parity. Port the Swift projection to Rust; gate on a byte-parity test vs the currentrender-frames.v1.jsonl. No new signals; Swift keeps self-producing in parallel until parity proven. - P2 —
v1alpha2signal fields. Add congestion + cause-of-inactivity + bloom_edges + adversarial + lookups + causal_ledger to the projector/schema. - P3 — Swift consumes. Swift decodes projector frames; delete client-side derivation; wire causal_ledger to the scrubber/flame panel.
Acceptance criteria (definition of done)
-
crates/fips-renderproduces frames byte-identical (or ratified-quantized) to the current Swiftrender-frames.v1.jsonl, enforced by a CI parity test. -
EventRecord.ledger_deltapresent andskip_serializing_ifempty; runs without ledger activity are byte-unchanged. -
LedgerEntry.causal_parentpopulated via a documented primary-cause rule. -
render-frame-v1alpha2schema published with the six field groups;v1alpha1retained/migrated. - Timeline seek reconstructs the exact causal waterfall from deltas without re-running.
- Swift renderer consumes Rust frames; client-side projection removed; all goldens regenerated the blessed way;
scripts/check.shgreen.
Filed from a visualization-coverage audit (2026-07-24). Full design specs (projector, visual-layer, coverage matrix) were produced during that audit and can be attached on request.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with crates/fips-engine/src/engine_tree.rs and engine_finish.rs to trace ledger mutations and artifact assembly, then read FIPSDPackage/Sources/FIPSDFeature/RenderFrame.swift for the existing projection. The work is done when crates/fips-render achieves the specified parity, timeline ledger deltas support seeking, the v1alpha2 schema and goldens are updated, Swift consumes Rust frames, and scripts/check.sh is green.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, swift
- Domain
- data-visualization, distributed-systems, networking, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100