A stdout chunk can end inside the benchmark JSON line; the waiter matched the half line and JSON.parse threw (seen once
in two runs on the rust image). Waiters now read complete lines only.
All commits build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
main.ts called decodeCultNetPayload with no definition, which the required-definition signature refuses; esbuild strips
types, so only the browser noticed. The sample now holds a counter definition and plain-map definitions for the Eve
documents, and verify-eve-browser-network.mjs runs tsc over main.ts against the module map the bundle uses (and builds
cultcache-ts, which cultmesh-browser now imports) before bundling. The receipt is read as the typed wire object instead
of a cast of the Eve receipt type.
All commits build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The python-build step runs the same guard whose output is the dist PyPI
uploads, so a NODE_OPTIONS preload in the workflow or job env could stub
its checks exactly as it could on the npm Publish line. It now starts
under env -u NODE_OPTIONS; the pin's exact text and semver-policy.md say
so. The pin test's if: always() edit matched the npm line's old text and
had become a no-op; it now matches the pinned line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The npm Publish step starts the guard under `env -u NODE_OPTIONS`, and the
pin's exact text carries it, so a workflow or job NODE_OPTIONS preload
cannot run inside the guard (preload-fakes-guard).
- The pin's shell/defaults ban and its test are gone: it defended only a
deliberate edit, which is review's job (suite-gate-structural-not-parsed),
and refused a harmless defaults.run.working-directory. A step-level shell
still fails the exact comparison (shell-ban-deliberate-only).
- docs/semver-policy.md states that the guard and pin stop accidental edits,
that the cleaned environment removes known variables rather than proving no
inherited one matters (env-denylist-overclaim), recommends a
required-reviewer environment for the npm job, and gives the first-release
note with the valid next versions.
- check-changelog-semver accepts `## [x.y.z] - YYYY-MM-DD` and
`## x.y.z - YYYY-MM-DD` beside the plain forms (changelog-first-entry).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A job or workflow defaults.run.shell can run the exact Publish line through any
command, with the token in its env. Only continue-on-error leaves the pin: a
check that fails inside the guard refuses the release whatever the step allows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/release-after-suite.mjs runs the release-checks suite and then the
release action (npm publish, or the Python sdist and wheel build that the PyPI
step uploads) from one process, and releases only if the suite passed. No step
condition can release what the suite did not pass, so the pin that read if:
conditions (status functions, multi-line if:, the leading-conjunct rule) is
deleted with its tests and docs. What remains compares each job's release steps
with their expected text exactly, less their if: line, and refuses
continue-on-error, shell and defaults in the workflow.
Ruling asura:ruling:suite-gate-structural-not-parsed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul s2 found the CultMath bytes follow the checkout's line endings (an LF clone
built 53084D94, not the tracked 6781BF20) and its path when no .git maps the root
to /_/. .gitattributes pins packages/cultmath/src/** to eol=crlf, which the
tracked DLL was built from, so no re-release. The build script refuses up front
without .git, and the refusal and docs name line endings beside source and
toolchain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul s2 found 'success() || c' and plain or quoted if: values continued on a
following line passing the pin. Any status function now refuses, and stepCondition
reports an if: whose value continues (block scalar or a deeper-indented next line)
as multi-line, which the pin refuses. Soul's four probe forms are asserted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Publish if: calling always(), cancelled() or failure() runs after the suite failed, the
continue-on-error bypass by another name; a block-scalar if: was compared by its indicator, so
'>-' equalled '>-' whatever followed. Both are now refused. The third refusal no longer calls
sound conditions narrower: it names the three accepted shapes, and the code comment and
docs/semver-policy.md list the sound forms textual matching refuses and why.
(Soul findings cut-3-gate.s1.status-function-bypass, block-scalar-if-compared-as-text,
pin-refusal-cost-undocumented.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unchanged CultMath source built on another SDK or host gives other bytes (Linux sdk:10.0:
db2a11cc against tracked 6781bf20), so the refusal no longer sends the operator to release a
CultMath with no change (Soul finding cut-3-gate.s1.toolchain-drift-named-as-source-change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Directory.Build.props puts CultMath's obj at obj\packages\cultmath\src\CultMath, which the
obj\src-only clean left in place, so an old-mtime CultMath source change could reuse a stale
CultMath.dll and pass the byte gate (Soul finding cut-3-gate.s1.cultmath-obj-not-cleared).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suite step's if: is absent, equal to Publish's, or a leading conjunct of
a Publish condition that has no ||. A disabled or narrowed suite step fails
the pin. Closes asura:follow_up:suite-step-condition-pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The declared org.gamecult.cultmath dependency must equal the CultMath package
version (-ne, not -gt), and every CultMath.dll a managed publish produced must
equal the DLL that package tracks. The type-name check and its helper,
scripts/check-cultmath-types.cs, are gone: the byte gate subsumes them.
$managedPublishes is the single owner of publish-root identity for the clean
step, the publish loop, $publishedByName, the byte gate and --api-refs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
publish-packages.yml runs the suite in both jobs, after .NET setup on a full-depth
checkout and before any check or publish step; the three Unity release scripts run
it before the checker. A test reads each file and fails when the step is removed,
moved, or made non-fatal. The Caching Unity pin now needs a prefix declared once
whose exact tags hold the package's own manifest at the tag's version. No NUL byte
in the checker is a test. The policy records that releases run from a full clone.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Also defines the PE fixture before the tests that use it, so a filtered run no
longer reaches it before initialization.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Monorepo snapshot tags track every package's directory, so tracking a file
proved nothing about a package's own releases. A package with an assemblies
directory is a first release only when no tag under any prefix tracks a managed
assembly there (decided by PE bytes, once per blob). Assembly-less packages are
a first release when no tag of their own prefix exists; their prefix is pinned
by the suite test tying each declared prefix to a workflow trigger or an
existing tag, which now fails on a typo for every assembly-less package.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The changelog-heading guard missed 12 of 17 heading shapes and a first changelog
entry. It and its parsing are deleted. A release with no same-prefix baseline is
a first release only when no tag in the repository, under any prefix, tracks a
file under the package's folder (declared assemblies dir, else the changelog's
directory); otherwise it refuses naming the tag. A tag whose tree cannot be read
refuses. Tests pin the mistyped prefix for CultMath 0.3.1, cultlib 1.0.61 and a
Python package, a genuine first release, the moved-folder choice, and an
unreadable ancestry and tree.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The previous release is chosen by version order among same-prefix tags strictly older than the version, and must be an ancestor of HEAD, so a later backport on another branch never moves a rebuild's predecessor. A release with no previous tag is refused when the changelog already lists an earlier version, so a mistyped prefix cannot make an unmeasured first release. The declaration is validated as a whole: two packages sharing a prefix, or an entry without a string tagPrefix and changelog, refuse.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The checker reads each package's tag prefix, changelog and assemblies directory
from scripts/release-packages.mjs and derives the release record from git tags.
A first release is the absence of any older tag in a checkout that holds other
tags; a malformed same-prefix tag, a checkout without tags and an unreadable
repository refuse. The measured set is every managed assembly (CLI header in
the PE bytes) tracked at the previous tag at any depth; an unbuilt one is a
break, a built assembly with no namesake is an addition.
Deleted with their branches, refusals and tests: --first-release, --api-new,
--tag-prefix, --changelog, --api-baseline-path and the workflow first-release
decision line. Any unknown option is now a usage error.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every assembly the previous tag tracks that the build does not hand over is a break, whatever its name; a run that compared nothing is refused (the --api-new rename of a lone assembly). The cultlib build now hands over every shipped assembly. --first-release is believed only with no other same-prefix tag and the prefix its changelog directory names. git ls-tree/show failures and an unreadable changelog become refusals. Tests pin the CultMath-shaped rename, the left-reference scenario (a dependency's netstandard base chain changes), the first-release probes and the workflow's own first-release line.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The ApiCompat tool runs from scripts/api-gate, so a relative --api-built or --api-refs
failed as an unreadable assembly. The release scripts pass absolute paths; any other
caller does not have to.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A malformed --version fell into the tag-reading catch and was reported as unreadable
tags. It is now validated first and refused without repeating the value. A canary test
runs seven refusals (measured break, unreadable built DLL, unreadable baseline blob,
missing entry, skipped version, breaking under too small a bump) with a canary in the
changelog and the assemblies and asserts it never appears in the output.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A missing or moved baseline path, a previous tag that does not track the DLLs, and a built DLL
with no namesake at the tag (unless declared with --api-new) refuse, naming what is missing.
- First release is declared with --first-release, never inferred: no previous tag, or tags that
cannot be read, refuse without it.
- The "already published" skip is removed; a local tag of the released version exempts nothing.
- ApiCompat and the pinned NETStandard.Library.Ref 2.1.0 live in scripts/api-gate and are restored
there, so the root manifest (dotnet-stryker) is no longer needed by a release. Both sides resolve
netstandard through the reference assemblies.
- Tests: refusals, netstandard false breaks, cross-assembly references, restore diagnostics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checker now runs with the target repo as its working directory in tests,
so the tool manifest is found from the script's own location as it is in the
release scripts. Adds tests for usage errors, a missing changelog, backslash
and trailing-slash baseline paths, temp-file cleanup, a restore failure and a
missing dotnet failing closed, the five-line failure excerpt and the
five-diagnostic cap. Drops two unreachable defaults and an unreachable
spawn-error branch from the checker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check moves from before the build to once the assemblies under judgement
exist, and receives every shipped GameCult.* DLL (cultlib) or the fresh
CultMath.dll with the baseline path of the previous tag's tracked plugins.
The early calls are deleted; each script keeps exactly one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real assemblies built in the test and compared through the CLI: removal
without ### Breaking, removal under too small a bump (post-1.0 and 0.y.z),
additions alone, a declared break with nothing measured, a removed GameCult.*
assembly, tool failure failing closed, inherited-member removals, an
already-published version, the baseline coming from the tag's blob, and a
first release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check-changelog-semver.mjs takes --api-baseline-path, --api-built and
--api-refs, extracts the previous tag's tracked DLLs as git blobs, and runs
the pinned Microsoft.DotNet.ApiCompat.Tool on each built assembly. Measured
breaks make the release breaking for the lane check and must be declared
under ### Breaking; a tool failure without a diagnostic refuses the release;
a GameCult.* assembly the tag tracked and the build lacks is a break.
Behavioural tests follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge 69bd45ff made GameCult.Caching.MessagePack reference CultMath
(PackageId GameCult.Math), so its nuspec depends on GameCult.Math 0.2.4.
The packer's hand-written project list never packed it, and GameCult.Math
is not on nuget.org, so every clean consumer of the local feed failed
restore with NU1101. That is the Eve surface consumer smoke in the
"one-command multiplayer onboarding" jobs, red on both OSes since then.
The packer now walks ProjectReference from the public roots (GameCult.Mesh,
GameCult.Networking.WebSockets) and packs the whole closure. The expected
package names come from MSBuild's PackageId and Version for each project,
since GameCult.Math carries its own identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/check-cultmath-types.cs reads the assembly with System.Reflection.Metadata
and reports which named types are not public top-level CultMath types. The old
NUL-delimited string search passed an appended string or a member named like the
type, and failed a real type whose name is tail-shared in the string heap.
Prerelease and range versions now get a clear message instead of a raw
System.Version error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The package build now fails when the declared org.gamecult.cultmath version is
ahead of the CultMath package, or when that package's CultMath.dll lacks a type
CultMathResolver formats. It also drops the shipped-plugin guard that could not
fire, since the output root is wiped first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GameCult.Caching.MessagePack now references CultMath, so the Unity package
declares org.gamecult.cultmath in package.json, references CultMath.dll from the
asmdef, and the package build refuses to ship a second CultMath.dll and checks
the asmdef against the shipped plus external assembly lists.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The closure smoke already asserted dist/index.js and dist/index.d.ts for
every workspace package; extend it for cultmesh-ts to assert the four
committed native binaries plus SHA256SUMS, MANIFEST.txt and
MSQUIC-LICENSE.txt are in the packed file list, on every host. The tarball
carries both platforms; the host only decides which one loads. Cut 6
(packaging), 11.5/11.6.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding 8(c): the dev image builds the Node bridge's own libmsquic copy into
a build-local directory (scripts/build-quic-native.sh), which is never on the
loader's search path. .NET's System.Net.Quic resolves "libmsquic" through
that path independently and failed with PlatformNotSupportedException before
this. The smallest honest fix: dpkg -i the same pinned 2.5.9 deb (same URL,
same SHA-256 as the build script) system-wide and run ldconfig, so both the
Node bridge's own copy and .NET's independent resolution find a libmsquic.
Changes the image; the new tag is cultlib-quic-native-dev:75cb89e73850
(first 12 hex of this file's sha256), built fresh on Yggdrasil.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The interop lane's dotnet build FailFasts on startup in this minimal Debian
image: no ICU package, and .NET's globalization stack aborts without one
rather than degrading. The interop peer already parses every argument through
CultureInfo.InvariantCulture, so invariant mode costs it nothing; it is also
Microsoft's own documented fix for a minimal container, cheaper than pinning
and installing an ICU package by name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut 4's own npm tests and the C#-to-TS interop lane need to run in the same
container that builds the native bridge, and the image had neither runtime.
Smallest change: install Node 24 from NodeSource and the .NET 10 SDK from
dotnet-install.sh, both pinned by channel/major version the way the rest of
this image pins its toolchain by digest.
Co-Authored-By: Claude Sonnet 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>