Skip to content

Randomized differential testing harness across the Swift and Rust engines - #158

Merged
germ-mark merged 17 commits into
mainfrom
llm/ger-2566-differential-harness
Sep 26, 2026
Merged

germ-mark merged 17 commits into
mainfrom
llm/ger-2566-differential-harness

Conversation

@germ-mark

Copy link
Copy Markdown
Contributor

Implements GER-2566 item 1: seeded randomized op sequences — send, drop, reorder, duplicate, crash-restore (legality-gated, with a behind-delivery negative op) — driven on Swift↔Rust session pairs in-process, checking convergence, re-staple healing, and ledger-matched known divergences.

  • New harness in Tests/TwoMLSPQMigrateTests/Differential/ (~1800 lines): op DSL + phase-biased seeded generator, two-engine facade with normalized outcome comparison, two-lane delivery scheduler, engine-attributed divergence ledger (JSON), env-gated test suite.
  • Differential target is the deployed pin (two-mls-pq@c501f9d, binding contract 33): just differential-deployed builds the pin xcframework + binding, swaps them in (TWOMLSPQ_PIN_BINDING excludes the pin-missing migrate target), runs only the differential filter, and restores. The suite skips without DIFFERENTIAL_DEPLOYED_PIN, so the standard build/test/lint are unaffected.
  • Every failure prints a repro line (seed + op-script blob + contract version + pin sha); replayScript runs the identical comparison path.
  • Two un-ledgered engine divergences intentionally fail rather than being suppressed: the offer-surfacing divergence (Swift queuedProposal non-optional vs Rust optional proposal, both mismatch directions, identical under pin and main bindings) and a post-restore frame-handling class (remoteCommitApplied). Root-causing is follow-up work; the ledger's c1 entry was deliberately trimmed so these surface.
  • The no-double-commit-in-flight check stays on as a guard but is structurally un-driveable in a two-party single-occupancy model (documented in the README) — positively exercising it would need a third party or engine fault hooks.

Deferred: full 250-seed sweep (verified at 12 seeds), born-dedicated topology, CI wiring for the pin run (local-only first cut per GER-2566 decision).

Test notes: standard swift test green with the differential suite skipping (4 tests); swift format lint --strict clean; pin run executes at 12 seeds — pure-pair oracles and replay pass, mixed legs red on the known divergences above (by design).

🤖 Generated with Claude Code

… engines

GER-2566 item 1: seeded op sequences (send, drop, reorder, duplicate,
crash-restore, behind-delivery negative) driven on Swift↔Rust pairs with
convergence, healing, and ledger-matched divergence checks. Runs against the
deployed pin (c501f9d, binding contract 33) via `just differential-deployed`,
which swaps the pin binding/xcframework in and restores after; the suite skips
without DIFFERENTIAL_DEPLOYED_PIN so the standard build is unaffected.

Known un-ledgered engine divergences (offer surfacing, post-restore frame
handling) intentionally fail — see the harness README.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 878d3d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

germ-mark and others added 12 commits September 25, 2026 20:37
Co-Authored-By: Claude Code <noreply@anthropic.com>
The first pin run's fold-legality rejections were the harness folding
non-live offers: records kept after nil-offer deliveries, offers parked
across the sender's own later commit, superseded rotation candidates,
and offers at the previous group epoch (swift-mls verifying rejects
wrongEpoch). Five liveness gates now precede every fold, keyed on the
engine's own still-verifies answer — fold rejections drop to 0 across
12 seeds, so no engine defect or rule-8 advert gap exists to ledger.

FoldGuardReplica re-runs the engine's guard sequence on dumped offers
under DIFFERENTIAL_DEBUG_GUARDS/_FOLDS/_ADVERTS for future root-causing.

Co-Authored-By: Claude Code <noreply@anthropic.com>
One row per engine divergence from the differential runs, evaluated
against: spec status, sender-legality, accept-legality, sender-fix
recoverability, and safe leniency (healing without accepting the bugged
behavior). Rows 1 and 4 closed/ledgered; rows 2, 3, 6 carry open cells
for the remaining investigations.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Duplicate delivery is expected protocol behavior; rejection rides on
application-message consumption (ratchet generation), not ciphertext-level
duplicate detection — so a true duplicate fails to decrypt in both engines
and neither re-surfaces its offer. Row 2's correlation is re-aimed at
classifying mismatch sites as duplicate-app, fresh-decrypt, or post-restore.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Per-frame (offerSurfaced, errClass) matches on every frame in both
directions: duplicate-app re-deliveries reject stale with no offer on
either engine (app-consumption model validated directly), all fresh
decrypts surface on both, zero fresh-decrypt failures. Residual call-count
findings are direction-level offer-presence differences from harness
engine-state gating, not offer handling.

Co-Authored-By: Claude Code <noreply@anthropic.com>
A fold/deliver call-count difference is by construction a presence
difference — the op acts on whatever is live — so cross-direction
offer/frame-presence divergence is now classified offer-presence-divergence:
counted in a per-run summary line, not failed. Per-frame handling,
payloads, remoteCommitApplied, errorClass and probe invariants still fail;
presence comparison stays in the pure-pair oracles. The 12-seed pin red
set collapses to the post-restore class (matrix row 3) — 189 finding
lines. Engine-state-independent scheduling filed as follow-up (GER-2583).

Co-Authored-By: Claude Code <noreply@anthropic.com>
The matrix and findings are work product tracked in Linear (linked from
GER-2583), not repo content; tool documentation stays in the README.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Every wedging run carries a restoreBehindDelivery to establishment-era
state, and a pure-Rust pair wedges identically — the designed fail-closed
consequence of restoring behind delivered state, not an engine
divergence. 15 runs heal fully by replay. One non-restore wedge (seed 1,
side-band phase, pre-restore) remains open for triage. The probe
invariant now fails when quiescent round-trips fail both directions —
both-sides-full symmetry previously masked wedges.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Per-op HANDOUT/DELIVER dumps with leg first-byte/len/srcOp/lane/openKind
and facade state lines. Seed 1's non-restore wedge triaged to the
side-band driver never discharging a round: the responder's leg stays
pending after bind, is handed out again, and the peer applies a second
ratchetCT for the same round. Engine-side acceptance of a superseded
ratchetCT stays an open question until rounds are driven correctly.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The generator now emits the full bind-discharge committing round (offer,
fold, commit, apply) after each A.4 ratchet round as ordinary
scheduler-composed ops, so the PQ turn passes and legs are never re-handed.
The restore-legality gate now tracks the receiver's own lastStateSeq at
delivery time instead of the emitter's dependsOnSeq watermark (incommensurable
counters — restores silently no-op'd before); skips are dumped with a reason.

12-seed rerun: the non-restore wedge is gone, no double-apply signature
survives, every remaining wedge is restoreBehindDelivery-driven (the
designed fail-closed outcome, pure-Rust pairs included), and restores now
fire with visible skip accounting.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The restoreBehindDelivery negative op now asserts clean classification
rather than probe convergence: findings whose op postdates a fired
behind-restore are excluded as designed-wedge (counted per run) unless
either side's outcome is unclassified, which always fails. Attribution
is structural, so divergence in restore-free windows still surfaces.
The 12-seed rerun leaves 216 unexcused findings: a pre-restore
remoteCommitApplied divergence and 82 behind-restore outcomes failing
clean classification — both recorded as engine-side questions in the
Linear evaluation matrix.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The error-class table now equates Swift aeadOpenFailed with Rust
DecryptionFailed (the same AEAD rejection) and treats unsupportedFrameTag
as a classified clean rejection, so unclassified means only .other. The
designed-wedge exclusion grows 58->104 and the failing set shrinks
216->170. First-divergent-op analysis shows the pre-restore
remoteCommitApplied class is NOT a ledgered cascade: three of four
exemplars diverge in rotation-free, restore-free, plain-messaging
windows — a genuine per-frame handling divergence, recorded in the
Linear evaluation matrix. Residue: four post-restore .other sites
(truncatedSection, messageFromUnretainedEpoch) pending owner
classification.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@germ-mark
germ-mark force-pushed the llm/ger-2566-differential-harness branch from d9df6ee to 94e2e8f Compare September 26, 2026 06:57
germ-mark and others added 4 commits September 26, 2026 00:05
The restore-detail dump now runs after the restore attempt (FIRED=),
matching the attribution rule's fired-op set exactly; the per-seed
summary line prints behindOps so analysis and rule can't drift again.
True 12-seed failing set: 169 (presence 303, designed-wedge 104,
ledger-cascade 0).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Currently inert (dumps nil at the read site - a normalize-path
mismatch to diagnose next round). Re-ride ruled out at every
remoteCommitApplied divergence site: all divergent deliveries are
first deliveries of those bytes; the 9 duplicate deliveries classify
stale symmetrically on both engines.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The success-path debugDeliver call never passed epoch: (a silently
unmatched call-site edit), so every line printed nil - now 216
delivery lines carry epoch=N on both engines. The per-op state line
adds recvEpoch (Swift via @testable groupEpochs(of: recvGroup); Rust
per-delivery via the decrypted message epoch).

Co-Authored-By: Claude Code <noreply@anthropic.com>
The remoteCommitApplied divergence verdict: no rule-5 engine defect.
At every divergent site the two directions hold different receiver
epochs, and each engine's report matches its own epoch position (the
seed 11 control - matching epochs, both engines true - confirms it).
The class reclassifies as state divergence; the underlying
commit-cadence divergence under plain messaging (recv epochs split 1
vs 2 by op 15/28, pre-rotation) is a new undiagnosed lead, recorded
in the Linear evaluation matrix.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@germ-mark
germ-mark merged commit 95e7fc0 into main Sep 26, 2026
11 checks passed
@germ-mark
germ-mark deleted the llm/ger-2566-differential-harness branch September 26, 2026 07:31
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