packages/cultmesh-ts/test/realtime-quic-native.test.ts: the FFI smoke against
CultMeshQuicNativeRuntime (open/close, a listener with a real PKCS12
credential, a full loopback connection delivering one raw frame end to end),
plus the negative grep that .async( appears only on nextEvent and no
koffi.register exists.
packages/cultmesh-ts/test/realtime-quic-consumer.test.ts: the TypeScript
consumer against a raw listener opened through the native runtime (connect
with the correct pin; refuse a wrong pin; refuse a missing pin with no
validator; a caller validator observes the target and the DER bytes; a late
older generation is dropped), mirroring
CultMeshNativeQuicRealtimeTransportTests.cs and
CultMeshQuicRealtimeTransportTests.cs. Then the five trust negatives through
CultMeshQuicRealtimeSessionManager with a static lookup source and routes
signed with node:crypto (P-256, IEEE P1363 via dsaEncoding): (a) expired
certificate, (b) unsigned remote route, (c) untrusted Odin root, (d) a route
that verifies but pins the wrong TLS certificate, refused by the connector
before isVerifiedFor is ever asked, and (e) a stale/replayed generation,
proven with a stub connector whose transport fails isVerifiedFor, asserting
the session manager disposes it and rejects the connect. There is no
session_open nonce on the QUIC plane (route generation plus the TLS handshake
is its freshness), so (e) is this shape rather than a literal nonce replay,
as the cut map records.
test/fixtures/quic-test.p12 and quic-test-cert.der: a self-signed ECDSA P-256
PKCS12 test credential (OpenSSL, empty password, 1088 bytes — the same shape
as the native bridge's own embedded test PKCS12) and its DER certificate, used
to open real raw listeners and compute the expected cert-sha256 pin at test
time rather than hand-transcribing it.
Discrepancy kept, reported: the C# reference's receive-side inbox also
coalesces a backlog of not-yet-drained latest-only frames of the same key to
just the newest; this transport applies only the generation-drop filter the
cut map names (":538-560") and queues every frame that passes it. The interop
lane's frames are far enough apart that this never matters in practice, but a
consumer that fell behind would see more history here than the C# reference
delivers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
packages/cultmesh-ts/src/realtime-quic.ts adds, over the native binding:
- parseQuicRealtimeEndpoint, mirroring
CultMeshQuicRealtimeTransportConnector.TryParseEndpoint (:98-109).
- CultMeshQuicRealtimeTransport: sendFrame/receiveFrame/dispose/isVerifiedFor,
the stream-kind-vs-delivery consistency check (:519-524) and the
per-(channel,body) generation filter that drops older (producerEpoch,
sequence) (:538-560), both pinned against CultMeshQuicRealtimeTransport.cs.
Reliable sends reuse one persistent outbound stream behind a gate;
latest-only opens a fresh stream per send; unreliable fails closed. The
bridge owns wire framing both ways, so this file never length-prefixes
anything by hand.
- CultMeshQuicRealtimeConnector: the certificate decision on event 3 (a
caller validator, else SHA-256-of-DER against the endpoint's cert-sha256,
else reject), mirroring MatchesAdvertisedCertificatePin (:111-119).
- CultMeshQuicRealtimeSessionManager and CultMeshStaticRealtimeLookupSource:
binds candidates from authorityRoutes carrying cultmesh.realtime_state.v1,
verifies each with cultnet-ts's verifyAuthorityRoute before any connector is
asked, orders by connector then route priority, races at most
maxRacedCandidates per tier, requires isVerifiedFor after connect and
disposes a transport that fails it — the realtime path of
CultMeshSessions.cs's ConnectRealtimeAsync (:833-896), including its
session key verseId + "\u001f" + authorityRuntimeId (:46).
No Odin WebSocket rendezvous lives here (Q7, ruled A): only the
ICultMeshRealtimeLookupSource port and a static source.
index.ts re-exports the module and gains three CultMesh statics beside the
RUDP ones: parseQuicRealtimeEndpoint, createQuicRealtimeConnector,
createQuicRealtimeSessionManager.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds packages/cultmesh-ts/src/realtime-quic-native.ts: a lazy koffi binding
over the twelve v2 queue exports plus cultmesh_quic_last_status
(native/GameCult.Mesh.Quic.Native/include/cultmesh_quic_native.h). koffi is
required only inside loadBindings(), so a consumer that never touches QUIC
loads no native module. CultMeshQuicNativeRuntime is a process-wide,
reference-counted singleton running one event pump (a loop awaiting
nextEvent.async with a 250ms timeout, no koffi callbacks) that dispatches
decoded events to registered listener/connection handlers by id.
Also: add the koffi 3.3.0 dependency to cultmesh-ts's package.json, and switch
its tsconfig from classic Node resolution to Node16 so `cultnet-ts/contracts`
(a package.json `exports` subpath) actually resolves — a gap Cut 1 recorded
and left for this cut ("until Cut 4 changes it").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Running QUIC Cut 3's Windows leg exposed the same class of defect Linux
had at 5533c34: CULTMESH_QUIC_BUILD_TESTS has never had its own packaging
step on Windows, and build-quic-native.ps1's copy of msquic.dll only
lands in the final packaged output directory, never beside the test
binary in the CMake build tree.
Unlike the Linux failure, this one did not fail loudly. gamecult_mesh_quic_native.dll
imports msquic.dll by name, and Windows DLL search falls through to PATH
when the importing binary's own directory doesn't have it. This
workstation has an unrelated msquic.dll on PATH from the Windows
Performance Toolkit (529,480 bytes, unversioned) that is not the pinned
Microsoft.Native.Quic.MsQuic.OpenSSL 2.5.9 build (4,181,856 bytes). Every
asserts-build scenario ran clean against that wrong binary before this
fix, with no error to say so.
Mirrors the Linux POST_BUILD fix: CMake already receives MSQUIC_ROOT, so
the test target now copies msquic.dll from MSQUIC_ROOT/bin/x64 beside
itself instead of leaving the loader to guess.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
They shipped without ever being added to the public surface list.
Scalar-only is noted explicitly: every other scalar in math.cs has
float2/3/4 overloads, erf/erfinv do not, and that stays a separate
scope decision since the only consumer (Aetheria fire control) is
scalar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The round-trip test (ErfinvInvertsErf) was the only defense of
erfinv's accuracy, and it is blind by construction: it can only prove
erf and erfinv agree with each other, not that either is correct. A
hand mutation of a single tail coefficient degraded erfinv's worst
error 300-400x (5.0e-7 to 2.0e-4) and every committed test still
passed. Added ErfinvMatchesReferenceValues, pinned against reference
values from bisecting the platform's double-precision erf (Python's
math.erf, glibc-backed) rather than this package's own erf, out to
|y| = 0.9999, at the same 2e-6 absolute bar erf's own reference test
carries. Expected values are computed for the actual float32-rounded
input, not the decimal literal, because erfinv's derivative diverges
near +-1 and that rounding alone moves the true answer by more than
the error bar if ignored.
Verified against three hand mutations, each restored byte-exactly
afterward:
- erfinv tail leading coefficient 2.83297682f -> 2.83317682f: killed
(4 assertions failed).
- erfinv tail second coefficient 1.00167406f -> 1.00187406f: killed
(4 assertions failed).
- erf's `p` coefficient 0.3275911f -> 0.3275930f: survives. Soul
measured this one 2.3x worse but still under the 2e-6 bar the spec
pins; this is not a fixture gap, it is the mutant staying inside the
accuracy envelope the tests are supposed to allow. Recorded here
rather than tightening the bar past what the spec asked for.
Added ErfinvAtDomainEdgesAndBeyond, pinning the domain and edge
behavior from the matching source comment: erfinv(1) = +infinity,
erfinv(-1) = -infinity, and NaN outside [-1, 1] including +-infinity
and NaN input.
Deleted ErfinvIsMonotonicallyIncreasing: it asserted strict
monotonicity, which erfinv does not have. Scanning every representable
float32 in [0, 0.999999] finds 9662 backward steps, first at
y = 0.00022214651, worst drop 5.960464e-8 (one ULP at that scale) --
invisible only because the old fixture stepped by 0.001. erf has the
same shape: 100 backward steps over [0, 4.5], worst drop 1.192093e-7.
Replaced it with two tests: ErfinvIsMonotonicWithinFloatingPointTolerance,
pinning monotonicity to within 1e-6 (comfortably above the measured
noise floor), and ErfCellMassesStayNonNegativeAcrossRealisticGeometries,
the consumer-level property that actually matters for fire control:
differences of adjacent erf values (cell probability mass) stay
non-negative across 16/64/256/1024/4096 cells over +-4 sigma, plus a
tight 1 sigma spread and a very tight 0.25 sigma spread.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.
Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.
Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README's Linux test build has never worked: both the bridge and the
test executable carry INSTALL_RPATH "$ORIGIN" and expect libmsquic.so.2
beside them, but only scripts/build-quic-native.sh's packaged output
directory ever got that copy. A test build produced a binary that died
at once with "cannot open shared object file". Fix it where the
knowledge already lives instead of adding a README workaround: CMake
already receives MSQUIC_LIB_DIR, so the test target now copies
libmsquic.so.2 beside itself in a POST_BUILD step.
While verifying the README's own worked example end to end, found a
second, independent break in the same docker block: MSQUIC_INCLUDE_DIR
and MSQUIC_LIB_DIR were passed as paths relative to the repository
root, but CMake resolves a relative cache-variable path against the
CMakeLists.txt that consumes it, not the invocation directory, so the
compile failed on a missing msquic.h before it ever reached the
loader. Made both paths absolute with $(pwd) and documented why.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All ten scenarios pass under asserts and all four required under release, on
Yggdrasil, in the committed debian:13 image. Worst overshoot anywhere was 4 ms
against a 400 ms tolerance. F1 is closed on Linux: the build that ships now
runs the timeout rule.
Running the README's own worked example is what found the second thing. It
cannot work: the test binary carries an $ORIGIN rpath and nothing puts
libmsquic beside it for a test build. That example was written in the same
commit that deleted the harness, so the working path was removed and the
broken one published together.
Two things stay open. Windows still owes its release configuration for the
same reason Linux did. And F5's stress wants sixteen CPU burners on the
platform where burners are now forbidden, which is the operator's call rather
than mine to route around.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pass ran against 52082ec. Its nine hand probes, including the Rust
both-hops probe that proved S-3 on a matched pair, sit unpushed on soul-probe
in a scratch clone. Probes are throwaway by doctrine, so the pointer says both
where they are and that the findings recorded in this map are the durable
copy. Fix batch 4's tests have to reproduce the behaviour independently
anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My previous commit rewrote the batch-8 rulings as if the batch were still
owed. It was not: it landed at 5646ff7 and the harness went at 796433f, both
on hands/cultmath-erf. The map was behind the code, not the other way round.
What that changes: the release configuration and the seven mutation entries
landed inside the harness and left with it two commits later, so the release
build is owed again as a plain build. The scenario runner and every seam
survived untouched and now run as direct arguments to the binary.
Cut 3's only outstanding work is the Linux leg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four of the six rulings for the eighth fix batch named harness entries. The
operator retired the hand-written mutation harness the same day, with no
fallback, so those clauses are dead text. What each ruling protected is not:
the release configuration, the seam wrapping the whole duration expression,
pollhammer against a runtime holding live objects, the zero and negative
timeout bounds, and closerace surviving starvation all stand as behaviour.
The bridge is C++ and no ecosystem mutation tool is wired here, so it gets no
committed mutation evidence at all. Its rules are pinned by scenarios at the
layer that decides them; a mutant is something Soul tries by hand and throws
away.
F5's 16 burners now run on Yggdrasil. Those burners are what froze the
operator's workstation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The triage landed on the fifth attempt. Most of the 85 survivors are message
strings; about a dozen are real, and they sit on the rules the cut exists to
enforce: the alias-numeric refusal degrades to "only if every schema agrees",
a half-blank cites.target stops being refused, the index-alias role naming is
untested at all four sites, the row tiebreak and EdgesFor's ordering are
pinned by nothing, and ComputeDigest's hop branches have no coverage at all.
The equivalent mutants are named so nobody chases them twice.
R-AA's triage half is done; what it now owes is a behavioural test per live
survivor.
Soul also withdrew its own claim that C#'s cursor digest does not sort its
lists. It does, at CultNetSelectionEvaluator.cs:637.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 1 may not merge. Row identity becomes (schemaId, recordKey), the hop
behaves identically in both runtimes, the error message obeys its own schema,
and cultnet-rs becomes mutable by cargo-mutants.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the pinned accuracy bar against tabulated erf values (0, +-0.5,
+-1, +-2, +-3, within 2e-6), odd symmetry, the +-1 limits, erfinv's
round trip through erf over y in [-0.999, 0.999], and erfinv monotonicity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fire control's Gaussian scatter needs erf for a cell's probability mass
and erfinv to draw the lateral offset, both called per shot. Abramowitz-
Stegun 7.1.26 for erf (stated max abs error 1.5e-7), Giles 2010 (GPU
Computing Gems) for erfinv. Neither is an HLSL intrinsic, so there is no
shader-parity obligation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove scripts/mutate-cultmesh.mjs
(1451 lines) and its Node dependency from the Linux QUIC dev image.
The behavioural coverage stays: native/GameCult.Mesh.Quic.Native's
scenario runner and its seams (waitseam, pollhammer, holdtimeout,
zerotimeout, closerace, and the rest) are untouched. The README and
the dev Dockerfile now document building with
-DCULTMESH_QUIC_BUILD_TESTS=ON -DCULTMESH_QUIC_DEBUG_ASSERTS=ON and
running each scenario as a direct argument to the built binary,
instead of through the harness.
docs/cultnet-selection-cut.md and docs/typescript-quic-realtime-cut.md
still name the harness; they are Self's campaign history and stay as
written.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self's ruling of 2026-09-22 named six findings against Soul's eighth pass.
This lands the code side of all six:
- The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole
duration handed to `wait_for`, not the raw `timeout_ms` before it is
converted to milliseconds, so arithmetic applied after that conversion can
no longer live outside what the seam records. Its "unset" sentinel moves to
a separate `debug_wait_recorded` flag so a recorded -1 could never be
confused with nothing recorded.
- `pollhammer` also runs against a runtime holding a live connection to an
unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a
real (never-connected) listener, closing the gap where the wait's predicate
was only ever exercised on an empty runtime.
- A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within
50 ms, probing 0, -1 and INT32_MIN.
- `closerace`'s settle no longer sleeps a guessed duration: where the
development seam exists it waits for `debug_peak_calls` to reach the poller
count (reset before any poller thread exists, so nothing can zero a count a
poller already raised) before a short fixed margin closes the last gap
between being counted and being inside the condition wait.
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock
scenarios can catch arithmetic applied outside the seam's own argument,
which `waitseam`'s equality check structurally cannot see.
scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both
platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running
`polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new
mutation entries: R1 (the release macro divides the duration by 10), W1-W3
(600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the
seam's own argument), P1 (the idle predicate also ends on a live connection),
and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two
stale comments are corrected: the scenario list above `assertScenarios` now
names `pollhammer` among the clock-sensitive entries, and a historical "1300"
probe value that no longer matches the test file is removed. The header's
section 7 intro now says the seam serves both the quiesce and the wait
recording, not quiesce alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Refusals become wire messages, cursors are keyed, one id reaches the
authorizer, and v0 lowering preserves v0's pre-cut answers exactly,
correcting the earlier ruling that lowered an empty list to no filter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C# reference never serves v1, cited returns the wrong edges in both
runtimes, and the runtimes disagree on floats, schema aliases and ordering.
Floats render as their exact decimal expansion, flagged for the operator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cultmesh_quic_native.cpp's wait_for callsite now reads
std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)) instead
of std::chrono::milliseconds(timeout_ms), so every existing entry anchored on
the old spelling is repointed at the new one, with the loosening's derivation
placed inside the macro's argument so the seam's record and the wait's own
duration stay the same evaluation.
Soul's seventh pass named eight derivations that were each the identity at
every wall-clock probe the sixth pass's tolerances could afford: a floor at 50
with no early check, a 16 ms and a 40 ms quantum, a wait that doubles past a
runtime's twelfth poll, a clamp at 7275, an offset of 25, a floor of 40, and a
6% scale. All eight are new entries here, killed by waitseam's exact mismatch
rather than by timing.
The seam itself gets a revert (it never records, so waitseam reads -1
forever) and a loosening (the record moves to an unconditional statement
ahead of the branch, reading the host's raw argument on every call instead of
only what the bridge hands its wait — indistinguishable at every value that
reaches a real wait, and caught only at timeout_ms <= 0, where the bridge's
own guard never waits and the honest seam must stay untouched).
S1's two predicate survivors get names and entries: K1 gives up after 64
wakes, K2 ends the wait on any wake once runtime->error is non-empty. Both
are new pollhammer targets; the existing "no predicate" revert is S1's
"returns on any wake" and needed no new entry, only its anchor fixed. S5 gets
a second hold-guard entry at the stated 1500 ms ceiling, alongside the
existing 1000 ms one.
assertScenarios gains waitseam and pollhammer.
Every added/changed entry was hand-verified against a manual win32-x64 build
before this commit: waitseam killed the seam's revert, its loosening, and a
representative +25 ms offset by exact mismatch; holdtimeout killed the S5
1500 ms guard by a 1.4 s overshoot; pollhammer killed K1, K2 and the
no-predicate revert within tens of milliseconds. Each mutation was applied by
hand, run, and hand-reverted with a diff against the working tree before the
next.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Self's ruling of 2026-09-22 called it after seven Soul passes: a wall-clock
probe against cultmesh_quic_next_event's timeout can never tell the host's
argument from a clamp, floor, round, scale or later-poll fault that happens to
be the identity at the probe's own value, because there is always another
function that matches. Move the observation to where the rule is decided.
Add cultmesh_quic_debug_last_wait_ms / _reset_last_wait_ms, built exactly like
the existing hold seam: gated behind CULTMESH_QUIC_DEBUG_ASSERTS, absent from a
shipped build. CULTMESH_QUIC_DEBUG_RECORD_WAIT wraps the literal timeout_ms
handed to the condition wait as a pass-through, so what is recorded and what is
actually waited on are the same evaluation and cannot diverge except by
changing what the macro wraps.
Two scenarios:
- waitseam asserts the recorded wait equals the host's own argument, exactly,
over 0, 1, 15, 16, 39, 40, 999, 1000, 1001, 7300 and INT32_MAX (the header
states no narrower maximum), and over 40 polls on one runtime past the point
every prior scenario stopped asking. timeout_ms <= 0 never reaches the wait
at all, so its probe checks the honest converse — the seam stays untouched —
which is what catches a seam that records the raw argument on every call
instead of only the value it actually hands its wait.
- pollhammer runs one thread hammering every gate-touching entry point that
cannot itself queue an event, in a yield loop, against another thread's idle
poll, once on a clean runtime and once after a refused listener_open has
left runtime->error non-empty. The idle poll must still stay for its
timeout.
polltimeout, pollbusy and holdtimeout stay as generous-margin proof that the
recorded wait is really waited, not as the precision instrument they used to
have to be: the fastest-of-ten mechanism, the 15 ms probe's tight tolerance,
and the stated-limits table they rested on are gone, because waitseam now
kills every clamp, floor, round, scale, offset and later-poll mutant by
equality regardless of timing. The Windows priority raise these three
scenarios carried is gone with them: a flat 800 ms late tolerance already
covers the worst overshoot the old table recorded (127 ms at normal priority
under load), so raising the process above the load bought nothing the new
margin does not already have. holdtimeout's long probe moves to 1600 ms, above
the stated 1500 ms ceiling on the hold guard, with the hold kept armed to
3000 ms so a guard that parks a naturally-timed-out call anyway overshoots by
well more than the margin.
Verified locally on win32-x64: waitseam and pollhammer pass against the
unmutated bridge; a seam-loosening mutant (recording the raw argument outside
the wait's own branch) fails waitseam's 0 ms probe; an inline +25 ms offset on
the wait's argument fails waitseam by exact mismatch; the S5 hold-guard mutant
(timeout_ms > 1500) fails holdtimeout by 1.4 s overshoot; K1 (++wakes > 64), K2
(!error.empty()) and a no-predicate revert all fail pollhammer's idle poll
within tens of milliseconds. All were hand-reverted and diffed clean against
the working tree before this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seven passes on one rule, each killing the last Soul's functions and each
followed by another that is identity at every probe. Wall-clock observation
from outside cannot converge on an arithmetic rule. A dev-only seam records
the computed wait, and equality kills the whole family.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reconcile tests lost what they proved, the shard snapshot still loops on
its own, and the public options type silently drops every term v0 cannot
carry. Empty lists are refused at the one door, Mesh refuses what it cannot
send, and the dead identity code is deleted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator ruled decimal strings on 2026-09-17 and the map recorded it in
section 16, but section 2 and D8 still said float64 and nothing specified the
canonical form or the comparison. Commit 1 built what section 2 said. The Rust
Hands stopped on the conflict before writing a line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul's sixth pass (N4): `(woken || timeout_ms > 1000) && HELD` survived both
targets. The two existing hold entries only rearrange the guard's syntax,
and `holdtimeout`'s single 150 ms poll sits below any threshold such a guard
would carry.
`holdtimeout` now runs two idle polls under the armed hold, 150 ms and
1300 ms, and holds for 2200 ms. The new entry is that guard. Compiled in on
both targets, the 1300 ms poll is parked until the release and returns at
about 2200 ms, and every other scenario stays green. The harness kills it
on both targets.
With the two polls running together, the first poll's exit also wakes the
second. That gives the asserts build a second multi-threaded witness for the
predicate: the N1 revert and read-what-woke mutants die here as well as on
`pollbusy`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul's sixth pass (N1): every CallScope destructor wakes every waiter, so
without the predicate a poll leaves whenever another host thread makes any
call. Every scenario had a single-threaded host, and dropping the predicate
survived the whole matrix on both targets.
`pollbusy` is a public-API scenario and needs no development seam. A second
host thread calls `cultmesh_quic_last_error` every 20 ms for twice the
poll's timeout while an idle `poll(1000)` waits. It checks the timing first.
It then checks the other thread's call count, so a poll that passed without
ever being woken reads as the fixture failing.
Three harness entries:
- revert: no predicate, `woken` taken from `cv_status`;
- loosening: the wait reads what woke it and does not wait again;
- loosening: a hand-written loop that restarts the whole timeout on every
wake. Its lateness is a function of the input.
Each was compiled in and run through every scenario on both targets:
- revert and read-what-woke: `pollbusy` returns at 0 ms;
- re-arm: `pollbusy` returns at about 3000 ms.
The earlier scenarios all stay green under all three. The harness kills all
three on both targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul's sixth pass (N2): with probes at 150, 200 and 5000 ms, `min(t,5000)`,
`min(t,4990)`, `t*11/10` and `t+70` survived both targets, `max(t,220)`
survived linux-x64, and `max(t,250)` and `t+80` died by 22 and 2 ms. "5000 is
above any plausible ceiling" was wrong: 5000 is a round ceiling.
- A 15 ms probe, asked ten times with the fastest checked against 30 ms. It
is the only place a floor or an added constant shows. Scheduler delay only
adds, so noise has to delay all ten attempts to fail the bridge.
- The long probe is 7300 ms, which is not a round number.
- A single poll gets 60 ms plus a sixteenth of the timeout.
- Each timed scenario prints its worst overshoot per probe.
- Three harness entries: a round clamp `min(t,5000)`, a floor
`max(t,100)` and an offset `t+100`.
Stressed with two busy threads per logical CPU (16 on 8), five rounds of
every timed scenario on both targets. At normal priority, Windows put a
single poll up to 127 ms late and the fastest of ten 15 ms attempts up to
87 ms late, and a 45 ms tolerance failed the unmutated bridge in four rounds
of five. The timed scenarios therefore raise their own process to
HIGH_PRIORITY_CLASS on win32-x64, which needs no privilege. With that, the
worst single-poll overshoot was 15 ms on win32-x64 and 41 ms on linux-x64,
at normal priority there. The fastest 15 ms attempt was 0 ms late on both.
The limit in source is measured, not asserted. Each boundary mutant was
compiled in and run on both targets:
- `t+25` survived and `t+40` died;
- `max(t,40)` survived and `max(t,50)` died, on linux-x64 by 5 ms;
- `t*106/100` survived and `t*108/100` died;
- `min(t,7285)` survived and `min(t,7275)` died.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`*.sh text eol=lf` reaches new clones only. A clone checked out before the
rule keeps scripts/build-quic-native.sh with carriage returns after pulling,
because the blob did not change: git neither rewrites the file nor reports it
modified, and the container still dies on the interpreter `bash\r`.
The README now gives the repair where a newcomer meets the container path,
and .gitattributes points to it. Verified on clones made at 367c9bc and moved
to c25de5a, from Git Bash and from Windows PowerShell 5.1: 109 CR bytes
before, none after, status clean. The line uses `;` so it runs in both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>