Skip to content

Make the simulator honest about serial tails: consume the engine gate chain, fix the probe:page ratio, add per-decision gate visibility - #69

Merged
sagiba merged 6 commits into
mainfrom
engine/serial-tail
Jul 28, 2026
Merged

Make the simulator honest about serial tails: consume the engine gate chain, fix the probe:page ratio, add per-decision gate visibility#69
sagiba merged 6 commits into
mainfrom
engine/serial-tail

Conversation

@sagiba

@sagiba sagiba commented Jul 28, 2026

Copy link
Copy Markdown
Member

Makes the simulator honest about serial tails, and gives both instruments per-decision gate visibility.

Why

The sensing port's live A/B left one divergence: the sim promised a full cure of nara-1950-census's ~5.5M-key serial tail (~850 owner children); the real engine — live and on the replay server — stalls at ~385 splits with the tail intact. Racing tail cures on a simulator that invents cures would produce validated-but-fake fixes, so this branch closes the honesty gap first.

What

  1. Flight-recorder gate visibility (--trace): two new JSONL events — owner_split_decision (terminal gate reason plus the inputs the chain read: est, pages-since-carve, outstanding, far-ahead fraction, density ratio) and victim_scan (per-scan skip tallies + refusal discriminator). Policies stay pure; executors emit. Goldens regenerated added-only; metrics-internals §7 updated.
  2. Sim consumes the engine gate chain directly: the mirrored EstimatorOwnerSplitPolicy/EstimatorStealPolicy are deleted; SimExecutor constructs the engine's own OwnerSplitGovernor/ThiefPolicy for every sensing arm through the estimator seam (null → WINDOW for CURRENT, bit-identical by construction). The mirror-drift class is structurally gone. Parity test rebuilt: per-arm battery determinism, CURRENT-vs-unsteered identity, and a per-arm structural-zero exposure pin at the observed-mass floor.
  3. Gate-inputs dump (swath.sim.gate-dump): opt-in per-run TSVs of every owner-split decision's inputs and every victim scan, for diffing sim runs against replay traces. Plus a property-driven single-leg run harness.
  4. Live call-class latency ratio: the real-listing instruments now run at the live store's measured profile (223 ms page / 121 ms pivot probe / 223 ms structure probe). The probe:page ratio (0.543 live vs the bench profile's 0.318) sets the steal-race window (~2 owner pages live); at the bench ratio the sim's thief won races the real engine loses 100% on wide-flat tails — the root of the fantasy cure. Synthetic benches keep their stated regime, with disclosure notes. Regression leg ProbeToPageRatioTailTest pins the honest-stall mechanism at all four protocol seeds and fails against the old profile.

Evidence chain

Replay reproduction → per-decision tail diagnosis (owner blocked 5,326/5,326 by the reflect floor's zeroed window term; thief loses every evaluated pivot race) → mirror deletion (fantasy persisted ⇒ world model indicted) → per-decision input subtraction (density model exonerated) → attempt-anatomy measurement with a two-point latency-scale fit (probe:page ratio isolated as the sole gap; live window ≈ 2.0 owner pages) → ratio fix → acceptance sweep: promoted-arm rows now land at 373–429 splits against the engine truth of 383–399, ~99% proposal loss, serial ~0.95. CURRENT now collapses at 4/4 seeds, matching the live 4/4 persistence (the old 2/4 "coin flip" was an artifact of the wrong ratio).

Every commit was independently reviewed (SHIP each). Engine behavior is unchanged on all default paths: the swath-core changes are trace-only observability; goldens changed added-only.

Summary by CodeRabbit

  • New Features

    • Added victim_scan trace events with victim-selection counts, outcomes, and estimates.
    • Added owner_split_decision events with gate outcomes and decision metrics.
    • Added optional simulator TSV dumps for owner-split decisions and victim scans.
    • Non-finite trace values are now emitted as valid JSON null values.
  • Documentation

    • Expanded trace schema guidance, event semantics, versioning, and simulator instrumentation instructions.
  • Tests

    • Added coverage and updated golden traces for the new events, gate decisions, JSON serialization, and simulator dumps.

sagiba and others added 5 commits July 28, 2026 13:25
The aggregate OWNER_SPLIT.*/NO_VICTIM.* counters say a gate fired, never
which range it blocked or on what readings -- so a serial tail that never
splits cannot be attributed from a run's artifacts. Add two --trace event
kinds that carry exactly that, additive to the v:1 schema:

- owner_split_decision, once per OwnerSplitGovernor.decide() past the
  open-frontier early-out (blocked OR carved), with the terminal gate's
  reason plus est / pages_since_last_self_split / outstanding /
  worker_count / far_ahead_fraction / density_ratio / keys_emitted.
- victim_scan, once per ThiefPolicy.selectVictim() pass, with the
  per-cause skip tallies, the winning estimate and the refusal
  discriminator -- aggregate per scan, never per candidate.

Both policies stay pure: the readings ride back on the decision
(OwnerSplitGateInputs on Skip/Carve, VictimScan on Selected/NoVictim) and
the executors that already hold the TraceSink emit the events, behind the
same enabled() guard every other call site uses. The gate chain
short-circuits, so an input a lower gate would have read is reported as a
NaN not-computed sentinel, serialized -- like every non-finite number --
as JSON null so a trace line stays strictly parseable.

No gate logic, ordering or counter changes. The decision-trace goldens
gain one victim_scan entry per recorded steal attempt; no existing
recorded value moves.
…or mirrors

The simulator installed a sensing variant by reproducing victim selection and
the owner-split gate chain in this module with one substitution -- where the
estimate comes from -- because the engine's policies had no seam to install an
estimator through. They have had one since #68, so the mirrors were two copies
kept faithful by a test rather than a mechanism.

The sim's RemainingWorkEstimator now EXTENDS the engine's (the estRemaining
contract was already identical; what the simulator adds is the two degeneracy
probes its sensor counters ask), so a variant is handed straight to the engine's
own OwnerSplitGovernor and ThiefPolicy. Both construction paths collapse into
one: CURRENT passes null and leaves the engine on its own WINDOW default, which
is byte-for-byte the shipped path it already took. RateAnchoredArm delegates
classify() too, so a simulated run on the promoted arm emits the same
SENSING_OWNER.*/SENSING_STEAL.* rows a deployed one does.

The SENSING_ROUTE.* counters are re-keyed off whether an estimator was installed
rather than off which mirror class was: the question they answered dies with the
mirrors, and the one that replaces it -- which sensor steered this table's leg --
is what a race record needs.

The parity test's mirror-vs-engine comparisons are vacuous now, so the same
batteries drive the ENGINE chain under all eight arms instead: decisions
complete and stable per arm, CURRENT pinned against a chain nobody steered (the
no-regression pin on the collapse), and -- new -- every arm required to expose
the observed-mass floor's structural zero, a large estimate blocked because
min(1, densityRatio) <= f leaves no reachable child tail. That last check is the
class the mirror-parity test structurally could not make.
A counter says a gate fired n times; it cannot say which reading made it
fire, which is the only question worth asking when a simulated run and a
real one disagree. `-Dswath.sim.gate-dump=<path>` writes one TSV row per
`OwnerSplitGovernor.decide` past the open-frontier early-out, and one per
`ThiefPolicy.selectVictim` pass at `<path>.scans.tsv`.

The columns are the engine's own `OwnerSplitGateInputs` and `VictimScan`
payloads -- what it already emits as its `owner_split_decision` and
`victim_scan` trace events -- so the two sides diff row for row against a
replay-server trace of the same listing. Each decision row also carries
the decided range's `lo`/`cursor_to`/`hi`: sim node ids and a replay run's
node ids are different id spaces, so a tail range is matched by keys.

A write-only observer: a dumping run takes exactly the decisions it takes
without the dump, and with the property unset no row is formatted and no
key rendered. Instants come from the run's own virtual clock. Both files
are opened CREATE_NEW and every IO failure fails the run -- an artifact
that silently truncated would have its missing rows read as a finding.
… ratio

The simulator priced a one-key probe at 35 ms against a 110 ms page — 0.32 —
while the store those instruments answer questions about was measured at
121 ms against 223 ms (0.54, nara-1950-census live/replay profile,
2026-07-28). A thief spends a cascade of probes placing a pivot and loses if
the victim drained past it meanwhile, so that ratio times the probes an
attempt issues IS the race window in owner pages: about two at the live
ratio, under one at 0.32. Everything else in the world model was measured
faithful — parks, the fleet's single attempt slot, futility pacing, the
pivot cascade — so this one ratio is why a simulated thief kept winning
races the engine loses 100% of on a wide-flat tail, and why the sim reported
a real bucket carved into ~900 ranges where the engine stalls at ~385.

PolicyRunFixtures gains LIVE_S3_LATENCY (223 ms page / 121 ms pivot probe /
223 ms delimited probe, flat — the structure probe's measured 2 ms/cp term
needs a SimListingView change and is disclosed as a non-goal), and the three
instruments whose answers are quoted as "what the engine would do on this
bucket" run at it: RealListingRunTest, the corpus sweep, SingleLegRunTest.
The synthetic benches stay at MEASURED_TAIL_LATENCY, whose javadoc now says
what it is — a bench regime, not the live store's ratio — because their
pinned tables were taken there and a bench number states its regime.

ProbeToPageRatioTailTest pins the mechanism on the module's own nara-shaped
fixture (the leaf-concentrated bench at a full 1,000-key page, eight
workers, the promoted arm, four seeds): the heavy leaf drains as one range
that emits every one of its 400,000 keys itself, 668 steal attempts against
it produce one child that carries no keys, proposal loss reads 0.9938, the
run publishes 18 splits and spends 49% of itself serial. The same fixture,
seeds and arm at the bench ratio fail every one of those thresholds
(victim share 0.40-0.57, 1,001 keys stolen off it, loss 0.76-0.82, 39-61
splits, serial 0.09-0.21), which is the falsification the leg exists for.
E-11's window-in-pages threshold is reported as not-computable rather than
approximated: a run records steal.split for a race the thief wins and
nothing for the scan that opened an attempt, so there is no per-attempt
bracket to take a median over without new executor plumbing.

The occupancy walk that finds which range held the fleet moves to SimTrace,
shared with the real-listing decomposition rather than copied — two walks
over the same claim/complete stream that disagreed would be describing two
different runs.

The README's claim that race loss is a property of the keyspace and not of
the declared timings is corrected: the two regimes it generalised from both
price probes at 0.32, so what they held fixed was the ratio.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42e6e69e-ae1a-4038-9385-db2e941930b0

📥 Commits

Reviewing files that changed from the base of the PR and between d8b60f0 and f291235.

📒 Files selected for processing (4)
  • swath-core/src/main/java/io/varve/swath/observability/NoopTraceSink.java
  • swath-core/src/test/java/io/varve/swath/engine/WorkStealingScanConstructorDefaultsTest.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/SimGateDump.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SimGateDumpTest.java
📝 Walkthrough

Walkthrough

The change adds victim_scan and owner_split_decision tracing, propagates gate and scan data through engine policy results, adds optional simulator TSV dumps, updates estimator wiring and latency regimes, and expands tests, fixtures, and documentation.

Changes

Decision trace instrumentation

Layer / File(s) Summary
Owner-split gate telemetry
swath-core/src/main/java/io/varve/swath/engine/policy/*, swath-core/src/main/java/io/varve/swath/engine/OwnerSelfSplit.java, swath-core/src/main/java/io/varve/swath/engine/WorkStealingScan.java
Owner-split decisions now carry gate inputs and distinguish decision-only traces from published splits.
Victim-scan telemetry
swath-core/src/main/java/io/varve/swath/engine/policy/*, swath-core/src/main/java/io/varve/swath/engine/Thief.java
Victim selection now records scan counts, estimates, chosen nodes, and refusal reasons.
Trace sinks and fixtures
swath-core/src/main/java/io/varve/swath/observability/*, swath-core/src/test/java/io/varve/swath/observability/*, swath-core/src/test/resources/goldens/decision-trace/*
Trace sinks serialize the new events, non-finite values become JSON null, and golden traces include updated event sequences.

Simulator integration

Layer / File(s) Summary
Estimator wiring and gate dumps
swath-sim/src/main/java/io/varve/swath/sim/executor/*, swath-sim/build.gradle.kts
Simulator sensing variants use the engine estimator seam, and optional owner-decision and victim-scan TSV dumps are wired through execution and cleanup.
Simulation validation and regimes
swath-sim/src/test/java/io/varve/swath/sim/executor/*, swath-sim/README.md
Live latency profiles, parity checks, trace reconstruction, performance tests, single-leg execution, and gate-dump documentation are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • varveio/swath#32 — This change extends its policy-seam decision-trace infrastructure with victim-scan and owner-split decision events.
  • varveio/swath#34 — Both changes modify confetti probe-slot decision paths, while this change adds trace payloads around those paths.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main changes: engine gate-chain integration, probe:page ratio calibration, and added decision-level trace visibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch engine/serial-tail

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
swath-sim/src/main/java/io/varve/swath/sim/executor/SimExecutor.java (1)

398-415: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

gateDump.close() failure in finally can mask the run's real failure.

If kernel.run() throws and gateDump.close() (in the finally) also throws UncheckedIOException, the close failure replaces the original exception per Java finally semantics — the opposite of the comment's stated intent that a failing run's dump is "most worth reading" for diagnosis. The original failure reason is lost.

🐛 Proposed fix: preserve the primary failure, suppress the close failure
-        SimRunResult result;
-        try {
-            result = kernel.run();
-        } finally {
-            // Closed on the failing path too: a run that died holding half its dump is the run whose
-            // dump is most worth reading.
-            if (gateDump != null) {
-                gateDump.close();
-            }
-        }
+        SimRunResult result;
+        RuntimeException primary = null;
+        try {
+            result = kernel.run();
+        } catch (RuntimeException failure) {
+            primary = failure;
+            throw failure;
+        } finally {
+            // Closed on the failing path too: a run that died holding half its dump is the run whose
+            // dump is most worth reading. A close failure must not mask the run's own failure.
+            if (gateDump != null) {
+                try {
+                    gateDump.close();
+                } catch (RuntimeException closing) {
+                    if (primary != null) {
+                        primary.addSuppressed(closing);
+                    } else {
+                        throw closing;
+                    }
+                }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@swath-sim/src/main/java/io/varve/swath/sim/executor/SimExecutor.java` around
lines 398 - 415, Update execute() so a gateDump.close() failure cannot replace
the exception from kernel.run(). Preserve the primary failure and attach any
close-time UncheckedIOException as a suppressed exception; when kernel.run()
succeeds, continue propagating a close failure normally. Keep the existing
cleanup behavior and gateDump null check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@swath-core/src/main/java/io/varve/swath/observability/NoopTraceSink.java`:
- Around line 37-46: Add an in-method comment to both empty overrides,
ownerSplitDecision and victimScan, stating that TraceSink.NONE intentionally
discards all trace events; leave their no-op behavior unchanged.

In
`@swath-core/src/test/java/io/varve/swath/engine/WorkStealingScanConstructorDefaultsTest.java`:
- Around line 432-441: Document the intentional no-op implementations of
ownerSplitDecision and victimScan in the test sink, stating that it only
observes lifecycle events. Add the documentation directly to these overrides
without changing their signatures or behavior.

In `@swath-sim/src/main/java/io/varve/swath/sim/executor/SimGateDump.java`:
- Around line 176-193: Update appendKey to decode key bytes with a strict UTF-8
decoder that reports malformed or unmappable input instead of replacing it with
U+FFFD. Preserve the existing tab/newline rejection and successful text output
for valid UTF-8 keys, while propagating a clear failure for invalid byte
sequences consistent with SimGateDump’s fail-loudly contract.

---

Outside diff comments:
In `@swath-sim/src/main/java/io/varve/swath/sim/executor/SimExecutor.java`:
- Around line 398-415: Update execute() so a gateDump.close() failure cannot
replace the exception from kernel.run(). Preserve the primary failure and attach
any close-time UncheckedIOException as a suppressed exception; when kernel.run()
succeeds, continue propagating a close failure normally. Keep the existing
cleanup behavior and gateDump null check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e219e3ca-86e7-4800-b252-7c46f0c9f305

📥 Commits

Reviewing files that changed from the base of the PR and between 20b51e9 and d8b60f0.

📒 Files selected for processing (49)
  • docs/internals/metrics-internals.md
  • docs/ops/dev/decision-trace-goldens.md
  • swath-core/src/main/java/io/varve/swath/engine/OwnerSelfSplit.java
  • swath-core/src/main/java/io/varve/swath/engine/Thief.java
  • swath-core/src/main/java/io/varve/swath/engine/WorkStealingScan.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/Carve.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/NoVictim.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/OwnerSplitDecision.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/OwnerSplitGateInputs.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/OwnerSplitGovernor.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/Selected.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/Selection.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/Skip.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/ThiefPolicy.java
  • swath-core/src/main/java/io/varve/swath/engine/policy/VictimScan.java
  • swath-core/src/main/java/io/varve/swath/observability/JsonlTraceSink.java
  • swath-core/src/main/java/io/varve/swath/observability/NoopTraceSink.java
  • swath-core/src/main/java/io/varve/swath/observability/TraceSink.java
  • swath-core/src/test/java/io/varve/swath/engine/DecisionTraceGoldenTest.java
  • swath-core/src/test/java/io/varve/swath/engine/RecordingTraceSink.java
  • swath-core/src/test/java/io/varve/swath/engine/WorkStealingScanConstructorDefaultsTest.java
  • swath-core/src/test/java/io/varve/swath/engine/WorkStealingScanTraceTest.java
  • swath-core/src/test/java/io/varve/swath/engine/policy/OwnerSplitGovernorTest.java
  • swath-core/src/test/java/io/varve/swath/engine/policy/ThiefPolicySelectionTest.java
  • swath-core/src/test/java/io/varve/swath/observability/TraceSinkTest.java
  • swath-core/src/test/resources/goldens/decision-trace/deep-narrow.jsonl
  • swath-core/src/test/resources/goldens/decision-trace/explosion-1to1.jsonl
  • swath-core/src/test/resources/goldens/decision-trace/flat-wide.jsonl
  • swath-core/src/test/resources/goldens/decision-trace/partition-key-value.jsonl
  • swath-core/src/test/resources/goldens/decision-trace/thief-cascade-mechanisms.jsonl
  • swath-core/src/test/resources/goldens/decision-trace/thief-edge-cases.jsonl
  • swath-sim/README.md
  • swath-sim/build.gradle.kts
  • swath-sim/src/main/java/io/varve/swath/sim/executor/EstimatorOwnerSplitPolicy.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/EstimatorStealPolicy.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/RateAnchoredArm.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/RemainingWorkEstimator.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/SimExecutor.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/SimGateDump.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/CorpusSweep.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/PolicyRunFixtures.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/ProbeToPageRatioTailTest.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/RealListingRunTest.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SensingEstimatorTest.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SensingRaceProtocol.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SensingVariantParityTest.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SimGateDumpTest.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SimTrace.java
  • swath-sim/src/test/java/io/varve/swath/sim/executor/SingleLegRunTest.java
💤 Files with no reviewable changes (2)
  • swath-sim/src/main/java/io/varve/swath/sim/executor/EstimatorOwnerSplitPolicy.java
  • swath-sim/src/main/java/io/varve/swath/sim/executor/EstimatorStealPolicy.java

…8 keys in the gate dump

Sonar's new-code quality gate flagged the two newly added TraceSink no-op
overrides (ownerSplitDecision/victimScan) in NoopTraceSink and the constructor-
defaults test's RecordingTraceSink, since new empty method bodies need an
in-method justification comment even though the surrounding pre-existing
no-ops (predating the gate) already establish this pattern implicitly via a
class-level comment. Add the same in-method comment style to both.

SimGateDump.appendKey used new String(key, UTF_8), which silently substitutes
U+FFFD for malformed byte sequences instead of failing -- undermining the
dump's stated purpose of byte-exact diffing against a replay trace, and
inconsistent with the class's fail-loudly-or-not-at-all contract (it already
refuses tab/CR/LF for exactly this reason). Decode with a strict
CharsetDecoder that reports malformed/unmappable input and throw an
IllegalStateException with the hex-encoded key instead. Add a regression test
covering a malformed-UTF-8 key.
@sagiba
sagiba merged commit 87f6c46 into main Jul 28, 2026
15 checks passed
@sagiba
sagiba deleted the engine/serial-tail branch July 28, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant