The generic mirror comparison passes about a hundred inputs through the shader and
missed a mirror whose prune cut was 2.0 instead of 2.25, because only a rare cell has
a nonzero weight under that cut. Twenty thousand random arguments compare the shader
text and math.phacelle field by field and kill that mutant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
design.md names CultPhasor as the second struct-return consumer and describes
phacelle (stripe wave vector, weight, normalization, exact gradient and its two
discontinuity classes, exact pruning). THIRD-PARTY-NOTICES.md, in both the package
root and the Unity package, cites Rune Skovbo Johansen's Phacelle Noise (MPL-2.0)
and records that the 3D-cell generalisation, the caller-supplied side vector, the
exact gradient and the exact pruning are new.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut 2a-ii-b. phacelle(float3 p, float3 side, float offset, float normalization)
returns CultPhasor { float4 cos; float4 sin }, each float4(gradient.xyz, value.w).
- 4x4x4 cells, pcg3d-hashed jitter in [-0.5, 0.5), weight max(0, exp(-2 d^2) - 0.01111),
normalization as upstream (lpmitchell/AdvancedTerrainErosion PhacelleNoise). The
caller supplies the stripe wave vector; offset is in cycles.
- The gradient is exact: weight derivatives and the normalization are included, not
upstream's grad(cos) ~ -sin * side.
- Cells whose per-axis lower bound is at least 2.25 have weight exactly 0 and are
skipped. An internal overload switches the prune off so the test can pin bit
equality without a second copy of the loop.
- cultmath_phacelle and CultPhasor mirror it in both tracked CultMath.hlsl copies
(the same blob). The mirror test compares the struct field by field; it now covers
34 functions.
- PhacelleTests: value against an independent double-precision reference, quarter-cycle
phase rotation, gradient against central differences on both sides of the
normalization threshold (excluding the threshold and each cell's weight-support
edge, where the exact gradient is discontinuous), stripes along the flow on the unit
sphere (0.215 against the 0.25 bound), step-difference continuity across cell planes,
and pruned == unpruned bit for bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stray, malformed, foreign-id or refused datagram is dropped and counted,
never fatal; a refused reliable frame ends the peer's session instead of
being acked and lost; flush fails once its session ends; one reliable
Accept per session; the TS operation service expires idle sessions and
survives a throwing handler. Four Soul passes; session-generation and
reconnect semantics move to the ack cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also adds a swept-after-handler test for the operation service and states
the Rust receipt comment exactly: only a peer Disconnect invalidates receipts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repeated-Connect answer applies its acknowledgement field in every runtime;
no test failed when a runtime stopped doing so. The TS replaced-peer test now
lets client A receive its Accept before A sends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Disconnect left the session's pending writes and scope alone, and the
transport's Invalidated status leaned on disconnect_reason, which connect()
clears. After a reconnect the receipt returned to Pending and could become
Acknowledged in the new session. The session now rotates its scope and drops
what it still owed on Disconnect, so the session is the one owner of
Invalidated and the transport override is gone.
A flush records the session scope it started in and fails when a Connect that
replaces the peer rotates it, instead of resolving on the emptied queue.
Tests: Disconnect-then-reconnect receipt, stale end reason after a new Connect
is accepted, flush across a replaced peer, and a repeated Connect's ack field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A server-mode transport treated a Connect from a new endpoint as a repeated
Connect once the session was connected: the new client got an Ack and never an
Accept, and the old session never ended. Only a Connect from the accepted
endpoint repeats; another endpoint resets the session and gets its Accept.
A flush recorded nothing about the session it started in, so a disconnect
listener that reconnected cleared the end reason before the 5 ms poll saw it.
Sessions now carry a generation; a flush fails if its generation ended.
The operation service's idle sweep deleted a session whose handler was still
running, losing the response. A session with a handler running, or one that
finished a handler within the idle window, is not idle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Imagination pass 1. Probes found acknowledged ordered frames lost in live
sessions in every runtime; the map derives Delivered at the sender from a
correct receiver watermark. Operator questions Q-A1..Q-A5 open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hub test counts replaced sessions, since a fresh session repeats the same
Accept sequence and the distinct-sequence count could not see it. The TS test
requires an Ack, not a new Accept, once the Accept was acknowledged. A peer
Disconnect is pinned as an ended session for flush and receipts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AddJournal runs under the gate right after Apply, once per admission that
changed something, before observers. Soul: merge with follow-ups; the
dispose and catch-path fixes landed. Cut 4 must decide what a throwing
journal means for replicas and whether a journal needs the change origin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FlushReliable throws for writes an ended session forgot. A server-mode
transport and the listener answer a Connect from the peer they already hold
with AnswerRepeatedConnect: the Accept still owed, or an Ack, queueing
nothing. The listener replaced the peer session on every Connect, reusing
sequences and never reporting PeerDisconnected; it now replaces only on a
different connect payload and reports the old peer, as the Rust hub does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flush now rejects for writes an ended session forgot. acceptConnect on a
connected session delegates to the new answerRepeatedConnect, so a repeated
Connect re-sends the Accept still owed, or an Ack once it was acknowledged,
and queues nothing; the operation service no longer builds a session per
Connect. The operation service dropped sessions never, so 64 abandoned
clients locked every new one out: a session idle for sessionIdleTimeoutMs
(default 30 s, the document server's session_idle_timeout) is now dropped.
A request whose handler rejects ends its session like a transport refusal,
so the invoker learns promptly and a retransmit is not acknowledged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flush_reliable raised nothing for writes the ended session had forgotten; it
now raises ConnectionError with the reason. Server mode answered every
repeated Connect by queueing a fresh reliable Accept; a Connect from the peer
already accepted is now answered with the Accept still owed and queues
nothing, as the Rust hub does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session that refused a packet, or was disconnected, forgot its
unacknowledged writes, so flush_reliable returned success for writes the peer
never acknowledged. The transport now fails the flush with the reason and
reads such writes as Invalidated. Reset no longer reissues sequence numbers or
fragment ids: a late duplicate Connect in server mode reused sequences the
peer had seen. end_refused_session owns the reset-then-goodbye ordering for
the hub, the transport and the document server, which used to end a refusing
session silently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AddJournal's Disposable.Create counted a refused dispose (from inside a journal
or another cache's hold) as done, leaving the journal registered forever. A
private registration now flips only after the removal succeeded. Held's catch
path dropped the hold's journal failures; it now throws an AggregateException
with the body's exception first. The store-composition contract states exactly
when the second getter read runs on a conditional commit. Adds the direct test
that a load is journaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Attaching a store with no file never calls Loaded, so it did not exercise the
rule; a commit removing a key the cache does not hold admits nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A disposed R3 subscription ignores OnNext, so a no-op removal from the cache's
observer array cannot be seen by what a receiver gets; the test now checks the
cache stops holding the subscriber. An admission that changed nothing (a store
attached with no records) is not journaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fragment-id-0 test is caught by the session-refusal branch, so a frame that
fails inside the handler needs its own test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultNetRudpPacketCodec.Decode did checked((int)length), so a datagram declaring
0x80000000 threw OverflowException, which no receive loop catches; the length
is now compared as a long and takes the ordinary malformed-packet drop.
Server mode claimed its endpoint on the first datagram of any type; only a
Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the socket transport and the listener end it: the
session forgets the peer (so the goodbye does not acknowledge the refused
frame), the peer gets a Disconnect, the listener drops the peer and raises
PeerDisconnected, and a server-mode transport forgets its endpoint.
CultNetRudpSession gains ResetPeerState, and the session and the listener
reject an InitialSequence of uint.MaxValue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultCache.AddJournal registers an ordered admission journal. It is called under
the gate right after Apply, once per admission that changed something, with the
admission's changes (dependents included) in Sequence order, before any
observer. A journal that throws joins the hold's failures: the admission stands,
the other journals run, the change is published and the writer hears of it
afterwards. Any hold entered while a journal runs throws. A hold now carries its
own changes and failures. Order stays data; no delivery thread or scheduler is
added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server mode claimed the endpoint on the first datagram of any type with the
right connection id, so an orphan Ack made a later real client a stranger. Only
a Connect claims it now. A session that refuses a packet has already recorded
its reliable sequence, so the transport ends it: the session forgets the peer
(so the goodbye does not acknowledge the refused frame), a Disconnect goes to
the peer, disconnect_reason is set, and a server-mode transport forgets its
endpoint. CultNetRudpSession gains reset_peer_state and rejects an
initial_sequence of 0xFFFFFFFF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session that refuses a packet has already recorded its reliable sequence, so
the socket transport now ends it: the session forgets the peer (so the goodbye
does not acknowledge the refused frame), sends a Disconnect, forgets the
endpoint in server mode, and emits disconnect and close. The operation service
did `void handleServerDatagram(...)`, so any rejection was an unhandled
rejection that ended Node; it now drops and counts the packet (packetsDropped),
ends a session that refuses a frame, and holds at most 64 sessions, dropping a
Connect from a new peer beyond that. CultNetRudpSession rejects an
initialSequence of 0xFFFFFFFF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Judging the merge against the file re-runs Project for the records the commit
lands and for re-resolved variants, so "once per landing record" holds only for
the admission. Handing the plan's projections to Judging would not cover the
re-resolved dependents, so the contract now states the second read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hold whose body throws after it admitted changes still publishes them, and
Held now assigns its result up front so it compiles under Stryker Safe Mode
(CS0165 had dropped every mutant in it). Disposing a Watch subscription removes
the observer from the cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Disconnect a refusing session sent carried the ack of the very frame it
refused, so the sender saw the frame acknowledged. The session forgets its
peer's sequences before building the goodbye, and a server-mode transport
forgets its endpoint so only a Connect can claim it again.
CultNetRudpSession::answer_repeated_connect is the one answer to a repeated
Connect for the hub and the document server: it records the sequence (the
document server's old accept_connect did, and a client that reconnects in one
session depends on it), applies acks, queues nothing, and returns the pending
Accept or an Ack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document server called accept_connect again for a Connect on an existing
key, queuing another reliable Accept each time: a resend storm, and the full
queue Err that the previous commit turned into a drop. The repeat now re-sends
the Accept still pending, or an Ack once acknowledged, as the hub does. A fresh
session's accept_connect cannot fail under validated options, so its error is
propagated again rather than counted as a drop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CultNetRudpSession::receive records a reliable sequence before reassembly, so
keeping the session after it refuses a frame made the sender's retransmit be
acknowledged and the frame silently lost. The hub and the socket transport now
end that peer's session, send it a Disconnect, and surface the end to the
caller (a Disconnected event, disconnect_reason()). The hub and socket
constructors reject initial_sequence u32::MAX and max_pending_reliable_packets
Some(0), so the accept_connect drops are unreachable. pending_accept_for_resend
becomes public for the document server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul verdict: merge. Before Cut 4 relies on sequence density: pin the
publish-on-throw guard in Held and observer removal on unsubscribe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every public CultMath value type serializes: MessagePack in the default resolver
chain (byte-identical to Aetheria's and StreamPixels' copies across 653 values
and all 115 float fields of the real Aetheria.cc) and System.Text.Json converters
(STJ-parity number handling, math types as dictionary keys). CultVec2/3 and
CultRect are gone with bytes unchanged. The cultlib Unity package depends on
cultmath; the release gate refuses until CultMath 0.3.0 carries every type.
Three Soul passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Named literals now follow AllowReadingFromString and WriteAsString as
System.Text.Json's float does; quoted numbers refuse whitespace;
UnmappedMemberHandling.Disallow and AllowDuplicateProperties=false are honoured
and a repeated property replaces the whole value; dictionary keys read only the
canonical spelling. Components live in stack spans, property names are
pre-encoded, and there is no per-property closure, so a float3 read allocates
nothing (424 bytes before). Tests pin unknown nested values, de-DE and sv-SE
style cultures, and a distinct negative-zero key.
Co-Authored-By: Claude Sonnet 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 held-ancestor path compared the raw empty string, so a blank variant name
and a blank held base name never met the blank rule; NameRead now returns null
for blank on both paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A malformed frame, a foreign connection id or a packet the session refuses
threw out of TryReceiveOnce. They are now counted in Stats.PacketsDropped and
the read reports progress. The client-side transport checks the connection
id before it claims the peer endpoint; the server listener drops refused
packets instead of throwing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A malformed frame, a foreign connection id or a refused packet raised out of
receive_once. They are now counted in stats.packets_dropped and receive_once
returns None. The id is checked before the peer endpoint is claimed, so a
stray arriving first can no longer become the server-mode peer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete the dead removed-ancestor branch and the duplicated name-read in the
variant name rule (resolution already refuses a missing or removed base and a
variant shares its base type). Add tests for the index rule counting a record
the same commit removes, blank names not colliding, and Dispose rethrowing
escaped completions after every observer completed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A foreign connection id, a malformed frame or a refused packet was emitted
as an error event; with no listener that throws and ends the process. They
are now counted in stats.packetsDropped. The id is checked before the peer
endpoint or session state is touched, and an unadmitted first packet in
server mode no longer claims the endpoint. Listener faults still surface as
error events.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>