Commit graph

1,700 commits

Author SHA1 Message Date
Meta Vens
c358ef9739 Semver check: drop the unobservable stdio option; the empty-package test really creates the directory
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:38:55 +02:00
Meta Vens
df13b502b9 Semver check: built and reference paths are resolved against the process, not the tool's directory
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>
2026-10-02 21:30:37 +02:00
Meta Vens
0ac870f132 Semver check tests: pin git's silence, the gate manifest file, netstandard.dll, the first-release hint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:28:26 +02:00
Meta Vens
e7786ffac2 Semver check: a malformed version is refused by name, and refusals are pinned never to repeat input contents
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>
2026-10-02 21:27:03 +02:00
Meta Vens
c5d9728b05 Semver policy: refusals, first-release declaration, unmeasured changes list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:17 +02:00
Meta Vens
83e3f81263 Semver check: measuring nothing refuses; reference assemblies; tool restored alone
- 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>
2026-10-02 19:52:14 +02:00
Meta Vens
32f5700b2a Semver check tests: double trailing slash, and a process started outside the target repo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:03:00 +02:00
Meta Vens
84a34b8735 Semver check tests: pin the broken-manifest failure text
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:35:27 +02:00
Meta Vens
f40069dd88 Semver check tests: scope the cleanup assertion to the checker's own temp dirs, cover a broken manifest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:05 +02:00
Meta Vens
6a3dae8954 Semver check: run tests from outside the repo, pin cleanup, usage and restore failure
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>
2026-10-02 18:32:38 +02:00
Meta Vens
ed31a97689 Semver check tests: cache built fixture assemblies by content
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:01:37 +02:00
Meta Vens
65bc144da0 Semver policy: describe the measured input and its failure rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:08:36 +02:00
Meta Vens
b8e1f7e0f6 Unity release scripts: run the semver check after the build, with the built DLLs
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>
2026-10-02 17:08:36 +02:00
Meta Vens
8f2eab7651 Semver check: behavioural tests for the measured public API
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>
2026-10-02 17:08:36 +02:00
Meta Vens
716ef9341c Semver check: measure the public API against the previous tag's DLLs
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>
2026-10-02 17:03:09 +02:00
Meta Vens
614fd44584 Merge CultMath 0.3.0 release (Asura 2a-ii-c) cultmath-unity-v0.3.0
snoise moves to the upstream kernel (breaking values, minor bump for 0.y),
smin_grad, cellular, snoise/fbm/ridged gradients, Phacelle, BoundedLeastSquares.
Unity package licence is MIT AND MPL-2.0. Soul holds: asura:verdict:cut-2a-ii-c.s3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:58:28 +02:00
Meta Vens
9e0f0ca36d CultMath 0.3.0: notices say the Unity package is MIT AND MPL-2.0, not MPL-2.0 as a whole
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:56:55 +02:00
Meta Vens
8ab384448c CultMath 0.3.0: scope snoise constants to 3D in the changelog; Unity package is MIT AND MPL-2.0
2D snoise keeps r^2 0.5 and scale 130; only the permutation (+10) changed.
package.json license and LICENSE.md now match CultMath.csproj: MIT overall,
Phacelle files MPL-2.0 (operator ruling cultmath-license-mit-and-mpl).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:48:16 +02:00
Meta Vens
98e3b91389 CultMath 0.3.0: rebuilt tracked DLL and pdb (Windows, Starfire)
Output of packages/cultmath/scripts/build-unity-package.ps1; reruns of the
script exit 0 with a clean tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:05:23 +02:00
Meta Vens
32ff786aed CultMath 0.3.0: version bump and changelog (snoise values break, minor bump)
Does not carry the rebuilt DLL yet; the Unity package script's version
parity check fails until the binary follows in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:03:56 +02:00
Meta Vens
c2cd4202d2 Tape campaign: meshing defaults to surface nets plus the tile pass, not dual contouring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:40:05 +02:00
Meta Vens
f7d4b63940 Park the CultMath tape campaign: interval-pruned field evaluation, option c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:30:54 +02:00
Meta Vens
35ff3fbe42 Variants map: Soul holds C2a F2 (entry bytes, stale lay-back, stuck flush; main merge is its own cut)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:53:45 +02:00
Meta Vens
7007a17059 Variants map: Soul holds the cultcache-ts push identity fix (label match survives; Python and Rust share the bug)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:29:44 +02:00
Meta Vens
0a26f96ede Variants map: F2 fork A ruled (forbid slot reuse); fork B deferred as the next parity cut
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:08:21 +02:00
Meta Vens
9dcb3aa154 Merge put-serve bound: a put the server could never serve is refused, and refusals say why
CultMesh document servers (Rust, TS, Python) refuse a put whose served snapshot reply
could never fit the response bound, before storing it. Refusals carry fixed text per
variant, acknowledge nothing, go unreliable and unordered, and never echo peer input or
OS text; publishers and the snapshot client report a refusal instead of timing out. Send
failures follow Cut D; a reply the queue cannot take is answered, not dropped; a failed
send's goodbye names a fixed error code in every runtime. Python validates options as
Rust does and survives any unstorable put or malformed TCP message.

Soul passes 1-4 plus merge verdict; the final batch closes it. Verified on Yggdrasil at
8b8f8436 (main merged): every CultNet/CultMesh suite in Rust, TS, Python and C#, interop 27/27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:40:50 +02:00
Meta Vens
8b8f8436cc cultmesh-ts: a snapshot refusal carries fixed text and acknowledges nothing
The snapshot fallback sent the caught error's message to the peer, and
errors from getCache or the registry went the same way, so a message the
application or a peer supplied could reach the wire. Its ack fields also
covered the refused request. The snapshot path now refuses per case with
fixed text (the snapshot source failed / the snapshot response is too
large / the snapshot response could not be queued) through refuse(),
which sends unreliable and unordered, so it needs no room in a full
queue, with ack fields that acknowledge nothing, as every runtime's
refusal does. onError keeps the detail.

Tests, with a raw peer that acknowledges nothing: a queue of 8 filled
exactly by an unacknowledged 8-fragment reply, then a refusal that still
arrives and acknowledges nothing; and a getCache failure carrying a
canary, refused with "the snapshot source failed".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:06:03 +02:00
Meta Vens
e1bdf92321 C#: an unsendable goodbye names the failure by a fixed code
EndUnsendable put the SocketException's message on the wire, the
platform's wording. SendErrorCode names a permanent failure EMSGSIZE,
EINVAL or EAFNOSUPPORT, anything else UNKNOWN, so every runtime's
unsendable goodbye reads "packet could not be sent: CODE" with the same
codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:40 +02:00
Meta Vens
d541e9eb35 cultmesh-py: a refusal acknowledges nothing and names its own send failure; TCP handling refuses instead of dying; the daemon checks its port
Queue-full refusal (findings 2 and 6). The refusal was built in
_send_rudp_schema_frame before _end_refused_peer reset the session, so
its ack fields covered the refused request (ack=102 mask=0x3). A reply
the queue cannot take now raises _ReplyUnqueueable; the loop ends the
peer through _end_refused_peer, which builds the refusal before the
reset, gives it ack fields built after it (acknowledging nothing, like
the goodbye and like Rust's refusal), sends it unreliable and unordered,
then the goodbye. A refusal fragment that can never be sent is not
dropped: the goodbye names its failure ("packet could not be sent:
EMSGSIZE"), where it used to say the session refused a packet.

Queue full to the brim (item 1). The tests fill a queue of 8 exactly with
a reply the peer never acknowledges, then show the next reply is answered
with response_unqueueable, acknowledging nothing, followed by the
goodbye; and that a refusal whose fragment cannot be sent ends the
session with a goodbye naming EMSGSIZE.

TCP (finding 8). A message whose handling raised (snapshot recordKeys as
an int, a shard-log afterSequence that is not a number, subscribe,
schema-catalog and verse requests with int fields) killed the
connection's thread with a traceback that could quote the peer. The TCP
handler now refuses such a message with fixed text (message_unhandled),
logs the error type only, and serves the connection's next message.

Daemon (finding 10). --port 99999 is an argparse error (exit 2), not a
traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:07 +02:00
Meta Vens
c3cc2b49c4 cultnet-py: an unsendable goodbye names the failure by a fixed code
end_unsendable_session put the OSError's text on the wire, which is the
platform's wording and can quote an address. send_error_code names a
permanent failure EMSGSIZE, EINVAL or EAFNOSUPPORT (Winsock equivalents
included), anything else UNKNOWN, as the Rust and TypeScript goodbyes
do; is_permanent_send_error is now "has a known code".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:04:07 +02:00
Meta Vens
a71f62e997 cultmesh-rs: pin the refusal at a queue full to the brim, and a real EMSGSIZE
The queue-full test used an empty queue, so a reliable refusal fitted and
the reliable-refusal mutant lived. The new test fills a queue of 8
exactly with a reply the peer never acknowledges; the next reply cannot
be queued, and its refusal must still arrive, acknowledging nothing the
peer sent. A RawPeer helper drives the peer packet by packet; the gap
test uses it too.

A response too large for any datagram fails with the kernel's EMSGSIZE:
the rejection is ResponseSendFailed("EMSGSIZE"), the goodbye is exactly
"packet could not be sent: EMSGSIZE", and no refusal is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
b8f754c898 cultmesh-rs: the snapshot client quotes only a refusal it recognises; the publisher doc says what holds
request_raw_snapshot_from_rudp_catalog put a catalog's error text into
its own error unfiltered. It now reports "refused the snapshot", naming
the refusal only when it is one of the fixed CultMesh refusal texts, as
the publisher does; both share recognised_refusal.

The publisher's doc claimed any refusing catalog is reported as a
refusal. That holds against a Rust catalog, which never acknowledges a
message it refuses; a catalog that acknowledges on receipt and refuses
afterwards (the TypeScript document server) is reported as having
admitted it. The doc now says so.

A fake catalog refusing with a canary is shared by the publisher test
and a new snapshot-client test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
5ef988b1d0 cultnet-rs, cultmesh-rs: an unsendable goodbye names the failure by a fixed code
end_unsendable_session put the io::Error's Display on the wire: the
platform's wording, which differs by OS and by which packet failed.
cultnet-rs gains send_error_code, the fixed name of a failed send's
cause (EMSGSIZE, EINVAL, EAFNOSUPPORT, or UNKNOWN), matching the codes
the TypeScript goodbye already carries; is_permanent_send_error is now
"has a known code". The goodbye reads "packet could not be sent: CODE".

ResponseSendFailed carried an ErrorKind, which lost the cause (EMSGSIZE
has no kind and read as Uncategorized). It now carries the same fixed
code, and the session ends where the send failed, over the real error,
so the goodbye and the caller's rejection name the same failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:01:33 +02:00
Meta Vens
6275b175e6 Merge origin/main (b248b0a1) into hands/cultmesh-put-serve-bound
No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:53:51 +02:00
Meta Vens
1d6e136440 Error-contract map: put-serve Soul merge-verdict notes (ENOSPC label, detail keys, new code)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:53:42 +02:00
Meta Vens
b248b0a148 Variants map: C1 questions ruled (override deleted; declared compatible ids resolve); defaults stand
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:52:25 +02:00
Meta Vens
ebb1eb2528 Variants map: C1 cut map (one schema id on the wire); two operator questions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:50:08 +02:00
Meta Vens
4a7fa1d63f Variants map: schema identity ruled C1 (<name>.vN on the wire); AetheriaEve not a consumer; C2 deferred
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:34:09 +02:00
Meta Vens
daedfdd658 Merge CultCache R1: typed read failures, no empty-file stores, no echoed values, typed write outcome
Only "nothing at the path" reads as an empty store; a zero-byte file is Undecodable in every
runtime; a dangling link is an I/O error. Errors name keys and schema ids only, never stored
values, and echo a header only in the cultcache.store.v<digits> shape. Single-file writes
report NotReplaced / ReplacedNotDurable / Rejected; an unlock failure after a successful write
no longer replaces the write's result.

Soul passes 1-3; batch 4 closes pass 3. Merged tree 9d9e76d4 verified on Yggdrasil: Rust
(cultcache, cultnet, cultmesh), cultcache-ts 44 including the four-runtime interop test,
cultnet-ts 198, cultcache-py 47, GameCult.Caching.Tests 346.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:13:24 +02:00
Meta Vens
fa34b68593 cultmesh-rs: drop the unreachable earlier-failure arm; rustfmt the resolution
receive() returns a reply only for a Ping, answered with a Pong, and a
Ping delivers no frame, so no rejection can follow an earlier send in the
same poll. The arm that carried such a failure into the rejection's end
was dead code; its mutant survived for that reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:05:45 +02:00
Meta Vens
3d285f4923 Variants map: cross-runtime schema identity fork (C1 recommended); F-ID.1-3 open
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:04:55 +02:00
Meta Vens
2ee707d05e cultmesh-rs: no permanent send failure is dropped when a rejection ends the session
A reply sent earlier in the same poll could fail permanently and then be
dropped when a rejection returned first; and a refusal fragment that can
never be sent must end the session naming its error. Both now reach
end_session as the unsendable end. The test makes the refusal itself
unsendable: the peer's goodbye names the error, nothing is counted as a
lost datagram, and the other peer is served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:43 +02:00
Meta Vens
62b34a7139 Merge origin/main (ack Cut D) into hands/cultmesh-put-serve-bound
Send-failure semantics are Cut D's: a transient failure is counted and
never reported; a permanent one ends that peer's session.

Conflicts:
- cultmesh-ts index.ts: the two sendPackets. Cut D's
  sendPacket(record, packet) is the one; the branch's reporting
  sendPacket is deleted, and so is the options doc's "Every failed
  datagram send reaches onError".
- cultmesh-py server.py: the rudp_send_failures doc is Cut D's. The
  receive loop is Cut D's (handling guarded, permanent send errors
  carried) with the branch's over_rudp=True. _send_rudp_schema_frame
  sends in rudp_max_fragment_bytes fragments and returns Cut D's
  permanent error from any fragment; a reply the queue cannot take no
  longer vanishes into a count (below). _send_rudp_packet and the two
  peer-ending helpers are Cut D's.
- cultmesh-py test imports: both create_rudp_schema_transport and
  decode_rudp_packet.

Semantic resolution:
- cultmesh-rs: end_rejected_session and Cut D's end_refused_session and
  end_unsendable_session collapse into one end_session(key, SessionEnd).
  A refusal fragment that can never be sent is not dropped: the goodbye
  names its error. The refusal stays unreliable, unordered, and
  acknowledges nothing (its ack fields are built after the reset).
- cultmesh-rs: Cut D's "snapshot response could not be sent: {error}"
  becomes CultMeshRudpRejectionReason::ResponseSendFailed(ErrorKind),
  with a fixed REFUSAL_TEXTS entry. Its session ends as unsendable: the
  peer gets the goodbye naming the error and no refusal.
- P3: a snapshot the reliable queue cannot take is answered.
  cultmesh-py sends a small cultnet.error.v0 (response_unqueueable,
  fixed text) unreliable and unordered, then the session ends as Cut D
  ends any message the handling cannot serve. cultmesh-ts sends its
  snapshot fallback error unreliable and unordered. cultmesh-rs already
  answers through the refusal (ResponseQueueFailed); a test pins it.
- Tests follow: the branch's EMSGSIZE tests now expect Cut D's permanent
  handling (the TS one is deleted, Cut D's own tests cover it), and the
  Python queue test expects the answer instead of a count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:49:55 +02:00
Meta Vens
b04d03b6f1 Ack map: Cut D landed at 2f06ef84
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:44:13 +02:00
Meta Vens
2f06ef8400 Merge ack Cut D: a failed send to one peer stays that peer's loss; C# schema server survives peer input
A peer's undecodable or unhandleable message ends only that peer's session and never
escapes Poll (the C# RUDP schema server's live crash). A send failure's goodbye belongs to
the generation that sent it. TS transient send failures are counted, not reported. The
interop harness finds the Rust peer through cargo metadata.

Soul passes 1-4; batch 5 closes pass 4's findings. Merged tree a892e117 verified on
Yggdrasil: GameCult.Networking 481, Mesh 264, cultnet-ts 198, interop 27/27 with
CARGO_TARGET_DIR set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:44:01 +02:00
Meta Vens
f789c8d133 cultcache-rs: the document canary uses a value serde would quote
The first canary put a string where a u32 goes, and rmp-serde refuses that by
marker without quoting it, so the canary passed with the serde text back.
A string where a unit enum goes is quoted as an unknown variant. The canary
now covers get, get_all, get_all_with_keys, put_envelope and prepare_entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:35:47 +02:00
Meta Vens
108071e8b3 Interop harness: ask cargo where the Rust peer is, and name missing prerequisites
The harness assumed that the Rust interop peer builds under
packages/cultnet-rs/target. When CARGO_TARGET_DIR or a cargo config's
build.target-dir is set, cargo builds elsewhere, and every Rust lane
failed with a spawn ENOENT. The path now comes from `cargo metadata`'s
target_directory, read once on first use.

The TypeScript peer imports @gamecult/cultcache-ts, and the Python peers
import msgpack. When either was missing, the lanes died with import
errors in a child's stderr or with timeouts. The first use of each peer
now checks its prerequisite and fails with a message that names it and
the command that provides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:44 +02:00
Meta Vens
1f04d5eda3 cultnet-ts: pin the one-fragment size check with a large fragment too
With only 1 KiB fragments, a pre-check measuring a small multiple of a
fragment (Soul's min(payload, frag*2)) still passed a 70,000-byte send.
The fixture now also runs with 40,000-byte fragments. Any such multiple
of that is over the datagram limit, so the mutant dies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:36 +02:00
Meta Vens
ce63ff88dc C#: a handler's fault ends the session without blaming the peer
The schema server's backstop ended every session with "session refused
a packet", including when the fault was the server's own: for example a
handler's response too large to send, or a refusal the handler did not
type. Decoding and handling are now two catches:
- A message that cannot be decoded is the peer's. The session ends with
  the refusal reason.
- A handler fault ends the session with "server could not handle a
  message".
The transport's EndRefusedPeer becomes EndPeerSession(peer, reason). It
reports whether it ended the session, so the log says when a response
that failed after its first fragment had already ended it over the send.
Before, the log reported that it ended a session that was already gone.

The comment said that ending the session matched cultmesh-py. It does
not: Python silently skips a malformed frame. The honest anchor is the
C# TCP schema server, whose ServePeerAsync ends the one peer whose
message fails to decode or handle. The comment now says so.

Pinned, each by a test that kills its mutant:
- a handler fault's goodbye carries the server's reason, not the refusal
  (mutant: the refusal reason);
- the post-fragment-0 path logs that the session had already ended
  (mutant: a constant outcome);
- EndPeerSession says no goodbye to a replaced peer's endpoint (mutant:
  the Ended check dropped);
- a refused peer's frames still queued are never handled (mutant: the
  queue purge dropped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:29 +02:00
Meta Vens
b37f898465 C#: a send failure on an already-ended session says no second goodbye
3b9ffac2 claimed that the generation check subsumed the UnsendableReason
once-guard. It did not. A thread that reads the generation only after
the session has ended carries the ended session's own generation, and the
check lets it through. For example, the polling thread answers a Ping on a
session that a caller's send has just ended but that has not yet been
retired. When that Pong fails permanently, the thread says a second
goodbye and overwrites the first failure's reason (Soul pass 4, PR4).
EndUnsendableSession now also returns when the session has already ended.

Two tests:
- The polling thread answers a Ping on a session already ended, and the
  Pong can never be sent. Exactly one goodbye goes out, and the first
  failure's reason stands. The hook admits the Ping after the session
  ended and before the peer is queued for retirement. The test kills the
  mutant that drops the Ended check.
- A failed send's goodbye is sent under the gate. A polling thread
  admitting a new Connect from the endpoint waits on the gate, so the old
  generation's goodbye never follows the new session's Accept. The test
  kills the mutant that sends the goodbye outside the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:19:19 +02:00