All three surviving comparator components now die in both runtimes, the claim
finally has the home the map said it had, and the whole-message vector is
green at 376 bytes. The thin evidence is no longer thin: a populated tags
array and a populated payload both come out byte-identical, which was the
named risk and did not materialise.
Hands also found that Soul's counterexample, taken verbatim, does not kill the
mutation it was built for. It survives in both runtimes because select's push
order happens to match alphabetical sort for that naming, and the two runtimes
push in opposite orders. Not an unreachable branch: a coincidence, the third
of these in three days, and this one was caught while executing rather than by
a later pass.
My byte counts in the landing note were wrong, 86 and 99 against the real 228
and 246. The committed constants matched the real bytes all along; only the
prose beside them was wrong, which is the same shape as the two corrections
before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul built the counterexample. The claim that no construction gives two edges
with tied From and Role and differing To reasoned about each hop separately,
and a selection carrying both hops mixes their edges into one bucket. A row
that self-references through the same role it uses to cite a peer produces
exactly that pair. It runs in both runtimes. Three comparator survivors the
last gate named are still alive, and batch 6 added no Rust row-order test at
all.
Two of my own words were wrong and both are corrected here. Nested wire-type
byte parity overstated evidence gathered at a single all-optionals-absent
value. And calling whole-message parity blocked on an ownership move ran two
independent things together: the encoders agreeing on bytes needs no ownership
move, and Soul wrote it this afternoon at 376 bytes identical. Calling it
blocked made the cheapest available evidence look expensive, which is how it
slipped six passes while sitting there for the taking.
The claim also had no home. The file section 9 names as carrying it contains
no mention of typed selection and never did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The brief named ygg-verify.sh without its path where every earlier brief gave
it in full. Hands could not find the script, verified the whole batch on the
workstation instead, said so plainly rather than claiming the bar was met, and
left the branch unpushed so nothing looked merge-ready. This batch still owes
its Yggdrasil run.
The regression is backed out and the real fix went to the owner: registration
now refuses a data member whose bare name collides with a reference member's
alias, which is the collision D10 never checked. Writing R-AM's test found a
second defect, the backstop logging DynamicInvoke's wrapper instead of the
fault it was meant to surface.
R-AP's encoding turned out to be C#'s existing behaviour, with the whole gap
on the Rust side omitting keys for absent optionals. Two nested types are now
byte-identical against real C# hex. The half that would move page assembly
into the evaluator was not built, and Hands stopped and explained why rather
than reporting the ruling closed. That half is its own cut, because it changes
two public signatures, every caller and the harness.
So the cut's honest claim is nested wire-type byte parity plus semantic parity
over the named subset. Whether that is enough to merge is the operator's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-AK was mine. I called a guard duplicate authority and ordered it deleted;
Hands verified, got it wrong, and left a comment claiming an alias uniqueness
property that the source contradicts. A legal document shape now takes every
hop path into an untyped exception, the peer gets no typed refusal, and the
batch's own new dispatch backstop swallows it without a log. Restoring the
guard leaves the suite green, which is the other half of the problem.
That is the second redundancy ruling of mine to be wrong. The first was caught
by Hands; this one was not. A deletion argued from a property has to be probed
at the shape that would break it.
R-AF is genuinely settled and Soul confirmed it harder than the probe did: the
reference type has nowhere to put a schema id, so no formatter or version can
introduce one. Both runtimes now agree on every shape that split them,
including the role-filter reordering no ruling covered.
R-AH has slipped five passes and cannot be built as I ruled it, because the
ruling never fixed map encoding, field order, or whether an absent optional is
nil or omitted, and because C# has no page type at the layer the vectors
test. This batch decides all four. Until it lands, the map now claims semantic
parity over a named field subset rather than byte parity, because byte parity
is not true today and the smaller honest claim is worth more.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
canonical_number went from 29 missed to 4, all four hand-traced as equivalent
rather than assumed. is_canonical, the Q-J door whose every branch mutation
was surviving, had never been called directly by any test; it is now.
The finding that outlives the batch is infrastructural. The same commit gave
different missed and timeout sets across three -j4 runs, while every -j1 run
reproduced the same result, most likely the shared cargo target directory
under concurrent mutant builds. That makes every concurrent mutation figure in
this campaign provisional, Stryker's --concurrency 4 runs included. Killed
mutants stay killed; survivor lists from concurrent runs cannot be trusted as
complete, so the rulings they informed rest on the hand-confirmed evidence
beside them rather than on the counts.
A third test in two days passed by coincidence: render_f32's sign-bit mutant
survived because 1.0 and 2.0 happen to have bit 0 matching their sign bit.
contracts.rs and schema_discovery.rs remain unmeasured. A partial run showed
real misses across the validate family before it was killed; that signal is
recorded so the next pass starts from it. Queued rather than run, because the
merge gate is a Soul pass on batch 5, not a sweep of an adjacent file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-AF was decided by dumping real bytes rather than by preferring an
implementation. A reference serializes as a bare msgpack string with no schema
id anywhere, so C#'s rule was right and Rust adopted it. The shared-key vector
that never existed now covers both directions, including that row order does
not change the answer.
Two findings came out of the work itself. Rust resolved and target-checked
every declared reference before filtering by the requested role, where C#
filters first: a live divergence no ruling had named. And the existing
row-tiebreak test was degenerate, passing with the tiebreak reverted and
swapped, purely because of which real schema hash happened to sort first. It
now derives its keys from the ids discovered at runtime.
Both cursor overflows are fixed, each runtime fed the other's proven input,
with a backstop catch so nothing untyped reaches the poll loop.
Two things are owed rather than done. The diff-scoped Stryker run failed on a
tooling bug that was worked around instead of root-caused, so 47 untriaged
survivors are inherited. The dispatch backstop has no integration test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
S-1 is not closed. R-V became two different rules: C# resolves an edge's
target by record key plus the declared leaf set, Rust by an exact schema-and-
key lookup. They give opposite answers on the exact shared-key shape the
ruling was written for, and no vector covers it because the fixture has no
duplicate keys at all. That is why it survived four passes.
Both runtimes fault on an attacker-supplied cursor, on mirror-image inputs.
C# overflows an int and lets an untyped exception reach the server's poll
loop; Rust overflows a usize and panics. Each holds exactly the hole the other
closed, which is what happens when a rule is hardened twice independently and
neither run is fed garbage.
R-Z was neither done nor reported as outstanding. I recorded batch 4 as
closing its rulings without checking that one, so the tracking failure is
mine. It also cannot be built as written: the two runtimes have no common
wire type for a page, so the comparison needs a shape decision first. I have
ruled one and flagged it for review rather than letting the vectors harden
around an unexamined choice.
Two further findings worth the pass. The Rust vocabulary has 59 live mutants
nobody has looked at, and about 35 of them sit on canonical_number, whose door
survives every branch mutation: the byte-for-byte parity rule is pinned by
nothing in Rust while the C# side was blocked at 66% over the same class. And
Soul settled the out-of-bounds read I had ruled most urgent: it is not a
defect, the mutant was a coverage gap.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-AB worked and cost two fixes: the named include_str! escape, then a second
one in the fixture loader that kept the crate unmutable even after the first.
cultnet-rs can now be mutated at all for the first time. Scoped to the diff:
23 tested, 23 caught, nothing missed.
The price is that twenty-two schemas now exist twice, guarded by a drift test
rather than by structure. It drifted within hours: the rebase onto the C#
side's R-X changed a schema and the vendored copy did not see it. The drift
test caught it. The cleaner shape, a build script staging into OUT_DIR, fails
for exactly the reason the include_str! did, because cargo-mutants copies the
package tree. Publishing the contracts as their own crate would fix it and is
out of this cut. So the duplicate is accepted on the record, in the follow-ups
rather than the win column.
S-2 turned out to be C#-only. Rust already checked the declared target before
filtering by key, so the divergence was one-sided.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Score 66% to 77.62%, every named survivor group killed, no production code
touched. The score is the least interesting part.
Two of the mutants Soul called live are equivalent: within one hop direction
the anchor and role already determine the target, so the pair cannot diverge.
They are recorded as equivalent so nobody pays for them a third time. And a
sixth mutant nobody had named survived in the same method on R-B's primary
ordering key, which means the rule R-B exists to protect had its first
component unguarded and the triage missed it.
The digest needed a golden value rather than a comparison. A same-versus-
different test cannot catch a mutant that swaps which case gets which flag,
because the two digests still differ, just mislabelled. The golden was
reconstructed from the method's documented contract rather than its own
helpers, which is what makes it evidence.
Three suspected real defects were flagged rather than quietly fixed, and get
behaviour tests first. The reachable one is an out-of-bounds read from an
attacker-supplied cursor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The list still said Windows owed its release configuration hours after the
Windows leg landed and closed it. A map that disagrees with itself is worse
than one that is merely behind, because both halves look current.
What is actually outstanding is narrower: the burner stress, which is the
operator's fork, and whether earlier Windows numbers were measured against
the pinned MsQuic at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bridge imports msquic.dll by name and nothing ever copied the pinned
library beside the test binary. Linux failed loudly on this. Windows did not
fail at all: DLL search fell through to PATH and loaded an unrelated msquic
from the Windows Performance Toolkit, a tenth the size of the pinned build,
and every scenario passed against it with no sign anything was wrong. The
green result was what hid the defect.
Fixed by mirroring the Linux POST_BUILD copy on the Windows side. Every result
recorded here is from a rerun afterwards, and F1 is now closed on both
platforms.
The open question is how far back this reaches. Every earlier Windows number
in this campaign was measured on this machine, where the copy has never
existed. Those figures are unconfirmed until someone establishes whether the
deleted harness placed the library itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent breaks, not one. The loader failure is fixed in CMake rather
than in prose, using an argument the build already required. The second break
fires earlier: relative cache paths resolve against the CMakeLists.txt, not
the shell, so the build died on a missing header before the loader was ever
reached.
That example was published in the same commit that deleted the working path,
and nobody ran it. Running the documentation is the check that would have
caught it, and running it is what caught it now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three commits on cultnet/selection-cut1 close R-V, R-W, R-X, R-Y, S-4, S-8,
S-9, S-10 and S-11. R-V is proven in both row orders, which is the point: the
old behaviour depended on which row was enumerated first.
Making the validator refuse keywords it does not implement immediately found
twelve uses across three committed schemas that nothing had ever validated.
Those three are formally unvalidatable now. Nothing broke because nothing ran
them, which is the finding rather than a reassurance.
Hands named two gaps instead of papering over them: S-11 is fixed by
inspection with no dedicated test, and the three schemas need a decision.
Caching's three failures remain unproven rather than passing, since the
stopgap's container runs as root and they expect a write to fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Hands agent working the selection cut reported that the map had no fix
batch 4 section. It did not, on the branch it could see: the live copy was on
my own working branch, main was a day behind, and the selection work branch
carried a copy from 2026-09-17. It worked from its brief instead and said so,
which is the only reason this surfaced rather than producing a cut built on
five-day-old design.
That is my failure, not the agent's. Self commits the maps and I had been
committing them wherever I happened to be standing. A map is only current if
it is current on the branch the reader has.
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>
1.0.60 removed public API (CultInspectorAssetPath) under a patch bump; its
own changelog says "Breaking" and the version number lied. Operator ruling:
semver is policy, strictly, for every published package family.
docs/semver-policy.md states what counts as breaking per surface (managed
API, Unity inspector attributes, CultCache wire format/store compatibility,
CultNet protocol, native bridge exports), the 0.y.z pre-1.0 convention, tag
naming, and required changelog shape. Records the 1.0.60 scar as history;
does not renumber it.
scripts/check-changelog-semver.mjs is the mechanical check: refuses a
changelog "### Breaking" section under too small a bump, and refuses a
version that skips, reverses, or repeats the previous published tag. Skips
re-litigating a version that is already tagged (rebuilding immutable
history is not a release action). Wired into publish-packages.yml for the
four npm/PyPI packages, and into build-unity-package.ps1 (cultlib),
packages/cultmath/scripts/build-unity-package.ps1 (cultmath), and a new
scripts/verify-caching-unity-release.ps1 (caching.unity has no build script
of its own to hook into).
18 node:test cases cover the required table (breaking+major/minor/patch,
non-breaking, 0.y.z, skip, downgrade) plus CLI-level git-tag resolution.
Hand-mutated each rule (bump classification, pre-1.0 lane requirement,
breaking-heading detection, breaking early-return, already-published skip)
and confirmed each kill, then restored byte-exact. No mutation harness is
committed; CultLib has none today.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soul S1: the 1.0.60 entry claimed the OpenSSL MsQuic bridge had shipped
since 1.0.59; git says both blobs first appear in this release. S2: the
package build staged package.json and README only, so the produced package
and the tracked template differed by the changelog.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultAssetGuidKey.cs (and the test file added in 0b88706... no, 6c7cfa7) had a
literal LF working-tree checkout under core.autocrlf=true / text=auto, so a
build run from a stale bypassed-checkout copy of this worktree embedded a
source checksum a fresh clone's CRLF checkout would never reproduce. The
tracked git object was already normalized; only the working-tree bytes in
this worktree were wrong. Restored the CRLF checkout (git add reports no
diff against the index for CultAssetGuidKey.cs itself), then rebuilt with
scripts/build-unity-package.ps1 -UpdateTemplate. GameCult.Caching(.pdb) and
every managed plugin that depends on it (GameCult.Caching.MessagePack,
GameCult.Mesh, GameCult.Mesh.Quic.Native, GameCult.Networking,
GameCult.Networking.WebSockets) picked up new MVIDs/PDB checksums as a
result; the native gamecult_mesh_quic_native.dll/msquic.dll are untouched
since the native build did not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
*.meta is gitignored repo-wide; every tracked Unity .meta file, these two
included, is force-added on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>