Commit graph

969 commits

Author SHA1 Message Date
Meta Vens
796433f2d4 Delete the mutation harness; keep the QUIC scenarios runnable directly
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>
2026-09-22 18:41:28 +02:00
Meta Vens
d0480a7d83 Record the operator's acceptance: Q-BP2 A, the title rule, R-D, the Q-J door
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:55:04 +02:00
Meta Vens
5646ff7351 Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix
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>
2026-09-22 12:27:56 +02:00
Meta Vens
a1309c638c Record the Rust alias port: row-for-row with the reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:21:08 +02:00
Meta Vens
8ef170b847 Record the early Soul pass's new findings and rule fix batch 3
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>
2026-09-22 12:06:11 +02:00
Meta Vens
9b88cb4e5c Record Soul's eighth pass: the release build never runs the rule it ships
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:43:50 +02:00
Meta Vens
722859550d Record the shared fixture and rule the alias defect: the C# reference's rule is the rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:34:57 +02:00
Meta Vens
e3dbb640f7 Correct the f32 3e20 example: the Rust Hands was right
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 10:27:57 +02:00
Meta Vens
0ab91d0298 Record the C# half of the selection fix batch, and what it left undone
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:42:17 +02:00
Meta Vens
451ecc2d5e Record the whole-cut Soul pass: Cut 1 does not merge, and rule the fix batch
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>
2026-09-22 08:12:05 +02:00
Meta Vens
a704a77ec3 Correct the orphan-rule claim, and add two pre-merge items for Huginn
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:49:05 +02:00
Meta Vens
070fac062d Record the seventh fix batch: the wait is observed where it is decided
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:04:56 +02:00
Meta Vens
0a49f869f2 Record selection commit 4: parity vectors, and the hop they leave out
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:57:24 +02:00
Meta Vens
d9629a8e26 Record the selection commit 2 fix batch and Rust commit 3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:17:54 +02:00
Meta Vens
b8fad13867 Rewire the QUIC harness's native entries onto the wait seam
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>
2026-09-22 05:37:05 +02:00
Meta Vens
66830e92be Record the wait, not its shadow: a dev-only seam for cultmesh_quic_next_event
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>
2026-09-22 05:33:19 +02:00
Meta Vens
bc9fbb4fe3 Record Soul's seventh pass, and move the wait's observation to where it is decided
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>
2026-09-22 05:10:25 +02:00
Meta Vens
1fbb28ae90 Record Soul on selection commit 2 and rule the fix batch
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>
2026-09-22 05:02:05 +02:00
Meta Vens
0402ab88bc Record the Mesh collapse and correct two claims in its report
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 03:55:43 +02:00
Meta Vens
b7c641a681 Specify Q-J's canonical decimal, and sweep the float64 text the ruling left live
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>
2026-09-22 03:17:18 +02:00
Meta Vens
3d86bfacec Record selection commits 0-1, the fourth engine, and the ledger miss
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:35:34 +02:00
Meta Vens
907f216456 Record the sixth fix batch, accept the Windows priority raise, dispatch Soul 7
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:21:57 +02:00
Meta Vens
ff72f16ed6 Give the hold's guard a mutant that reads the timeout, and reach it
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>
2026-09-22 02:01:41 +02:00
Meta Vens
4ffa4c9628 Pin the wait's predicate with a host that has a second thread
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>
2026-09-22 02:01:27 +02:00
Meta Vens
37967e8b9b Probe the host's timeout at 15, 200 and 7300 ms, and stress the tolerance
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>
2026-09-22 01:59:37 +02:00
Meta Vens
b3d9cf7821 Tell an existing Windows clone how to get its build script back to LF
`*.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>
2026-09-22 01:03:51 +02:00
Meta Vens
c25de5a895 Record Soul's sixth pass: the wait's predicate is pinned by nothing
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:50:29 +02:00
Meta Vens
7cd5a2b63f Record the fifth fix batch landing and dispatch Soul's sixth pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:23:24 +02:00
Meta Vens
fa327a0627 Name the CultMesh MCP bridge as a priority campaign
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:37:00 +02:00
Meta Vens
089859deb4 Write and enforce the semver policy, per operator ruling on 1.0.60
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>
2026-09-17 22:03:22 +02:00
Meta Vens
45c2f4006f Say the OpenSSL bridge ships with this release, and pack the changelog caching-unity-v1.4.0 cultlib-unity-v1.0.60
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>
2026-09-17 20:40:12 +02:00
Meta Vens
ef81c96094 Renormalize CultAssetGuidKey.cs to CRLF and rebuild dependent plugin DLLs
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>
2026-09-17 20:12:52 +02:00
Meta Vens
9847a96070 Track the new CHANGELOG.md.meta files (force-added like other tracked .meta)
*.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>
2026-09-17 20:11:48 +02:00
Meta Vens
0b8870677e Add CHANGELOG.md to both Unity packages (UPM convention)
CultLib 1.0.60: CultInspectorAssetPath removed for CultInspectorAssetGuid +
CultAssetGuidKey (breaking for inspector annotations), and the native QUIC
bridge's Schannel to OpenSSL MsQuic 2.5.9 static-link change shipping since
1.0.59. Caching Studio 1.4.0: the GUID picker, sub-asset keys, built-in
refusal, and the stale-value warning/Clear button. build-unity-package.ps1
only stages files it names explicitly, so it neither drops nor overwrites
these; no script change needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:11:33 +02:00
Meta Vens
6c7cfa7a94 Studio: refuse built-ins by asset identity, not GUID digits; add Clear for stale keys
TrimEnd('0','f') missed Library/unity default resources built-ins whose GUID
has no all-zero or all-f suffix (e.g. 0000000000000000e000000000000000, the
LegacyRuntime font / Cube mesh in 6000.3.24f1). Ask AssetDatabase instead: a
real project asset is always a main or sub-asset under Assets/ or Packages/;
built-ins are neither. Also add a Clear button beside the stale-value warning
so a non-resolving stored key can be cleared without picking a substitute
asset first (drawing still never rewrites a stored value on its own).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:09:53 +02:00
Meta Vens
0c9cb57bea Correct stale Schannel/40,448-byte claims, ship OpenSSL's license notice
Soul flagged (F3) two docs still describing the pre-release native QUIC
bridge: unity/org.gamecult.cultlib/README.md said Schannel, and
native/GameCult.Mesh.Quic.Native/README.md still described the older
40,448-byte committed plugin as current. The shipped bridge (since the
1.0.60/1.4.0 release, commit 0155ba8) is the OpenSSL MsQuic 2.5.9 build,
302,080 bytes, static CRT, no CRT import. Both docs now describe that build
and name what it replaced.

The OpenSSL build's NuGet package carries an Apache-2.0 notice for OpenSSL
itself (Microsoft.Native.Quic.MsQuic.OpenSSL's THIRD-PARTY-NOTICES) that the
committed Third Party Notices folder never carried - the Schannel build had
no such dependency to notice. build-quic-native.ps1 now copies it out
alongside MSQUIC-LICENSE.txt, build-unity-package.ps1 stages it into the
package output and the tracked template, and it is now committed as
OPENSSL-NOTICE.txt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:49:14 +02:00
Meta Vens
1e398e0116 Studio: read and write sub-asset Addressables keys, warn on stale values
Ruling S (2026-09-17): a stored [CultInspectorAssetGuid] value is the
Addressables key form itself, the bare GUID for a main asset or
"guid[subAssetName]" for a picked sub-asset (one sprite of a sheet, say) -
the same shape AssetReference keeps and Addressables.LoadAssetAsync<T>
accepts.

CultAssetGuidKey (engine-free, GameCult.Caching) pins the parse/format rule
with unit tests (bare guid, guid[name], names with brackets/spaces,
malformed input), so headless checks can share it without touching Unity.

The Studio drawer's AssetGuid branch now: resolves either key form on read;
refuses to store a built-in resource's GUID (it has no Addressables entry)
and logs why instead of silently writing nothing; shows a warning next to a
stored value that fails to parse or resolve instead of drawing it as an
ordinary empty field, and never rewrites that stale value on its own.

Rebuilt GameCult.Caching/GameCult.Caching.MessagePack plugin DLLs
(scripts/build-unity-package.ps1 -UpdateTemplate) since the new type ships
in GameCult.Caching. Compiled CultCacheStudioDrawers.cs and
CultCacheStudioWindow.cs against Unity 6000.3.24f1's editor reference set
plus these plugins in a scratch csproj (removed after): 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:48:45 +02:00
Meta Vens
0155ba8cca Prepare 1.0.60/1.4.0 release: rebuild plugin DLLs, bump package.json
Removing the public asset path attribute is breaking. Runs
scripts/build-unity-package.ps1 -UpdateTemplate to refresh the tracked
plugin DLLs/pdbs in unity/org.gamecult.cultlib/Runtime/Plugins, and bumps
unity/org.gamecult.cultlib/package.json to 1.0.60 and
src/GameCult.Unity/Assets/Caching/package.json to 1.4.0 (dependency bumped
to cultlib 1.0.60). Tags are the next, separate step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:46:07 +02:00
Meta Vens
95a3c32250 Replace the asset path attribute with a stable GUID identity
CultInspectorAssetPathAttribute stored an engine asset path, which breaks
when the asset moves. CultInspectorAssetGuidAttribute stores the asset's
stable identity (in Unity, the .meta GUID) instead. The Studio drawer now
resolves the stored GUID to a path only to load the preview object, and
writes back AssetPathToGUID on a change. No path form survives for the
next field to pick up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:43:03 +02:00
Meta Vens
244154e2f5 Say what the restore digest is, since it is not an identity
The last report quoted the restored sha256 as though two runs agreeing on it
meant they had run the same source. They had not: `text=auto` gives a Windows
checkout carriage returns, so the win32-x64 run restored to b163cab7 and the
linux-x64 run to e3c6528a from the same commit. The digest is a restore check —
the file this run started with is the file it ended with — and it is said where
it is printed, so the next report does not borrow it for something it cannot do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 03:11:05 +02:00
Meta Vens
b985f07887 Say what the single-waiter wake actually rests on, and what is still unreached
The note beside the close's wake claimed the narrowing to `notify_one` was
equivalent, and gave the weaker half of the reason. The equivalence needs two
premises together: `closing` is set permanently under the gate before the notify
and appears in every waiter's predicate, so after a close begins no poller can
block at all and the set of waiters is fixed; and each exiting call's own scope
destructor takes the gate, decrements and wakes everyone, so one wake starts a
chain that reaches all of them. Neither carries it alone. Both are now written
where the claim is made, so it can be checked rather than believed.

Narrowing both wakes also survives, and that is not equivalent by any argument:
a wake consumed by the closer, waiting on the same variable for the count to
reach zero, leaves the chain stopped and the rest of the pollers sitting out
their timeouts. It passes on wait-queue ordering — the pollers are queued before
the closer, so a FIFO head is never the closer until it is the only waiter left.

Recorded as unreached, with the reason, and deliberately not as unreachable:
that claim has been wrong twice in this campaign. A scenario cannot force it
while the first premise holds, since the only way to put the closer ahead of a
poller in that queue is a poller that blocks after the close began. Reaching it
would need a seam that chooses which waiter a notify goes to. The standard does
not promise FIFO, so what survives here is the implementation's doing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:47:34 +02:00
Meta Vens
5ab4c627d7 Put a call in both states so the seam's own guard is pinned
The last pass's only non-comment source change was held by nothing. Reverting the
hold's guard so it fires unconditionally survived all five scenarios, because no
scenario ever had a call in both states at once: the timeout scenario never arms
the hold, and the hold scenarios never let a timeout expire.

`holdtimeout` is the missing state. The hold is armed, the runtime is idle, and
one poll asks for 150 ms while the hold stays armed for 800. A call the wait woke
is the seam's to park; a call whose own timeout expired is on its way out, and
parking it would let `holdclose` report the bridge keeping a host call inside the
library when what kept it was the fixture. The measurement is which of those two
numbers the poll comes back at.

Two entries, and the loosening is the one worth reading: the same two conditions
in the other order. The hold blocks, so the order is the rule — asked first, it
parks the call and then decides whether it should have. Both die on
`holdtimeout` at about 800 ms and on nothing else; `holdclose` and `latecall`
stay green under both, which is the point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:46:49 +02:00
Meta Vens
c682619fbd Close the timeout against waits derived from the argument, not only constants
The two bands ruled out a constant and ruled out nothing else. Any mapping that
is the identity at 200 and 900 passed for free, and the late tolerance was a flat
300 ms, so anything within that of the truth passed at every value forever.
Three ordinary ones survived all five scenarios on both targets: clamping the
wait to at most a second, adding 150 ms to it, and scaling it by six fifths.

The clamp is the one that matters. Its failure is verbatim the bug the last pass
claimed to fix — a host asking for five seconds gets one, forever, with nothing
reporting it — and "cap the wait so a shutdown gets noticed" is the most ordinary
spelling that line will ever be given.

So the long probe goes to 5000, above any ceiling a bridge would plausibly clamp
to, which kills the clamp and the scale on the early check and the late one. And
the late tolerance becomes a fraction of what was asked for — 60 ms plus an
eighth — which is what kills the offset: 150 ms is 75% late at the 200 ms probe
and there is no flat gap left to hide in. Measured overshoot on either target is
about 10 ms, against 85 ms allowed at the short probe and 685 at the long.

The table gets the clamp as an entry of its own, because the whole lesson here is
that a table of constants says nothing about a wait computed from the argument,
and a table of constants would go green again the moment these numbers drifted
back. Its honest limit is stated where the tolerance is defined: a scaling
smaller than an eighth survives, and nothing here claims otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:45:57 +02:00
Meta Vens
c177533b4f Offer the container line to both shells, and name the short-path requirement
The container recipe carried a PowerShell backtick continuation in a snippet
whose next paragraph invites a POSIX shell to run it, where a backtick is not a
continuation. A continuation has to pick a shell, so the line does not have one.
The Dockerfile's own copy of the recipe still mounted one machine's checkout by
absolute path; it mounts whatever checkout is running it, like the other two.

And the requirement a newcomer meets by tripping over it: on win32-x64 the
harness needs the checkout somewhere short. MSBuild's file tracker gives out on
long paths, so a run from a deep temporary directory fails the build for every
mutant, the no-op control included. That is loud — the harness stops on a red
control and prints what the child said — but what the child says is MSBuild, not
the path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:44:47 +02:00
Meta Vens
ca6c0f5852 Keep shell scripts LF so a Windows clone can run the Linux path
`* text=auto` and nothing else, so a Windows clone writes every shell script with
carriage returns. The documented Linux path is a container that runs one of them,
and it dies in under a second with a complaint about an interpreter named
`bash\r`. Nothing in that failure names line endings, and the same clone builds
and tests fine everywhere else, so it reads as a broken image.

`scripts/build-quic-native.sh` is the only shell script in the repository and the
only file on those paths that an interpreter reads by shebang; the mutation
runner is Node and handles carriage returns itself, and the Dockerfile builds
either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:43:37 +02:00
Meta Vens
367c9bc2b8 Record the pass that beat the bands with a clamp
No constant sits in both bands, which is true and is not the rule. A wait that
caps itself at a second passes every scenario, and capping a wait so shutdown
gets noticed is the most ordinary way that line will ever be written. An offset
and a scale survive beside it.

A fresh Windows clone also cannot run the documented Linux path at all, because
nothing tells git to leave shell scripts alone and the container dies on a
carriage return in the interpreter line. The mount fix works; this sat behind
it.

The equivalence holds, but on two premises nobody had written down, and
narrowing both wakes survives while being equivalent to nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 02:36:37 +02:00
Meta Vens
1845301f91 Record the batch that made the timeout observable
The blind spot was not where the hold sat. No scenario ever let a timeout
finish a poll, because both ended their waits with a close, so the duration
governed nothing anyone could see. A scenario that polls an idle runtime and
measures elapsed against what it asked kills all three constants, and it asks
twice at values whose bands do not overlap, because one value cannot tell a
bridge honouring the host from one waiting on that same number of its own.

The probe that reached the two-phase poll through a closed port is committed,
the wake finally has the entry the table had been claiming, and a surviving
mutant is argued equivalent and handed to Soul rather than swapped for an
easier one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 02:12:12 +02:00
Meta Vens
a937a0b07e Let the bridge's own timeout, refusal and two-phase poll be seen
Three rules the bridge has always promised, none of which anything committed
could see.

The timeout was the structural one. Both scenarios end their waits with a close,
so the duration the bridge waits on governed nothing either of them observes: a
hard-coded 2000 ms survived the whole Linux matrix and Windows, and a host asking
for five seconds would have got two forever with nothing reporting it. The fix is
not another entry at another constant. A timeout's only observable is how long
the call takes, so `polltimeout` lets an idle runtime's poll be ended by its own
timeout and measures it, twice at different values, because no single constant
sits in both bands. The seam's hold moves behind that too: it parks only a call
the wait woke, so a poll whose own timeout expired is on its way out rather than
held there by the fixture and counted as the bridge's doing.

The refusal during a close (the header's section 4) had no scenario at all. A
call admitted after the closer has begun increments the in-flight count behind a
wait that has already read it. `latecall` holds the close open with the seam and
makes a call into it.

And the two-phase poll was written off as needing a listener, a credential and a
connection. It needs a closed loopback port: the failure always carries a reason,
which is an 87-byte payload, in about a millisecond. Moving the pop above the
copy crashes on both targets.

18 killed, 0 survived, 0 skipped on linux-x64; 16 killed, 0 survived on
win32-x64, where the two ThreadSanitizer entries are honestly skipped. Both runs
restored the source to the same sha256, b163cab7d48ebc93.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:09:50 +02:00
Meta Vens
25d7de26b3 Point the container recipe at whatever checkout is running it
The mount was this machine's own path, so the one documented way to build and
check the Linux bridge needed editing before it would run anywhere else. It is
the repository root now, named as the shell names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 02:09:12 +02:00
Meta Vens
122c2172d9 Rule the reference check onto read now and write later
A reference whose target contradicts its declaration is noticed on read in this
cut and refused by name. Writing the same check into the put path is intended
rather than optional: the declaration already names the target and the writer
already holds the record, so it is cheap, and leaving it to consumers means
every tool that writes records reimplements it and a tool that forgets looks
exactly like one that did not until something refuses.

The consumer story rests on less than it sounds. There is no Studio repository
and no project by that name; the tooling that enforces this today is one
console project in one game. That makes now the cheapest moment to move the
rule to its owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:44:09 +02:00