Commit graph CultLib/packages
Author SHA1 Message Date
Meta Vens
cd93e4d112 cultcache-rs: keyed and document refusals keep no stored value; encode refusals are Rejected
- The redb envelope decode (get by identity and pull_all) and every typed
  document decode (get, get_all, get_all_with_keys, put_envelope, prepare_entry)
  kept serde's text, which quotes stored values. They now keep CultCache's
  context, which names the entry by type and key, and a fixed description of
  the decoder failure's kind.
- A snapshot the store cannot hold (two types claiming one schema id) was
  reported as NotReplaced, so a caller retrying on that kind looped forever.
  It is CultCacheStoreWriteFailedKind::Rejected now, raised before any I/O.
- release_lock and the read-only snapshot state one unlock policy: an unlock
  failure never replaces the result. Only the read-only path, whose API has an
  observer, reports it. release_lock also names the case where closing does not
  release the lock: a duplicate of the open file description, such as a child
  forked without exec.
- README: reads refuse a dangling link, but writes do not follow links yet. A
  write through a live link, and push_all through a dangling one, replace the
  link. Two tests pin that behaviour, named for R3 to decide between resolving
  and refusing. The write example classifies Rejected apart from NotReplaced.
- The shared-gate test waits 30 s and 60 s for the writer instead of 1 s, and
  checks that the file on disk is untouched while the gate is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:05 +02:00
Meta Vens
852ea0fe1a CultMesh TS: a node's returned documents belong to its cache
Soul F8 (M6, M7 survived): node.get, getRequired and put return the
object the cache holds, as C# and Python do (Rust copies). That is the
behaviour that ships.

- The README says the returned object belongs to the cache and must not be
  mutated, and shows the put-a-new-value pattern.
- A test pins it: two reads return the same object, put resolves to that
  object, and the caller's own value is never the one held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
091477d066 CultMesh TS: a catalog addresses each document by its id
Soul F7 (P9): a catalog holding two documents of one type answered a typed
lookup with the last one added, and nothing else could name the other. Two
documents of one type under different keys stay legal.

- CultMeshDocumentCatalog.tryDocumentById and documentById look a handle up
  by its document id, the key the catalog already holds handles under
  (it defaults to the record key).
- The publication-catalog test addresses both synced documents by id
  instead of documents[0] and documents[1].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
9118e24976 CultMesh TS: a document is read only under an id its type declares
Soul F3 and F4, and F1 in CultMesh's schema descriptor.

- The schema descriptor built from a definition uses schemaIdentityOf, so
  a handle's schema id is the id the cache stamps.
- Store documents (F3): a handle reads the record under its schema id, or,
  for a definition, under an id the definition lists as compatible, as the
  cache resolves it. The catalog-entry name/version match and both parse
  fallbacks are deleted: a record under another id is not this document,
  whatever its payload parses as.
- Peer snapshots (F4): syncDocumentFromPeerSnapshot and definition-named
  peer-snapshot handles take the peer's record at the key under the type's
  schema id only. The shape filter is deleted and no match is not-found.
  P8a (syncing A at k while the peer holds only B at k overwrote the local
  B and then threw) and P8b (a peer's C stored as A) write nothing now.
- HELD: a peer-snapshot handle named by a bare schema-id string still takes
  the first record at the key. Eve Electron reads the C# Aetheria daemon
  that way (EveElectron src/cultmesh-provider-client.mjs:129-142): the
  daemon stamps SHA-256 schema ids and the client asks by
  "gamecult.eve.provider_advertisement.v1", so only the fallback finds its
  records. How a TS reader names a C#-hosted schema is raised as a fork;
  the fallback goes when it is ruled.

Tests: the store- and peer-snapshot tests that asserted shape routing are
replaced by tests of the refusal, including P8a and P8b, and one that pins
the held fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:57 +02:00
Meta Vens
0ee98e8c2e CultNet TS: the registry resolves only ids a definition declares
Soul F1, F2 and F3 on the registry. The F2 principle (operator,
2026-09-30): "CultCache never destroys or relabels a record it doesn't
understand; keeps it byte-for-byte, refuses writes that would overwrite it."

- F1: the registry derives every binding's identity with cultcache-ts's
  schemaIdentityOf, so it stamps, refuses and resolves exactly as the cache
  does. P1 (A with schema name "p1.shared", B with schema id "p1.shared")
  is now refused by both.
- F2a: an id nobody owns resolves to the one binding that lists it as
  compatible. Several listers and no owner is a SchemaConflictError naming
  the id and the listers, whatever order they were registered in; an owner
  beats every lister. This is refused at resolution, as the cache refuses
  it at load, not at registration: refusing at registration would make the
  outcome depend on whether the owner was registered before the listers.
- F2b: version inference is deleted. "p3.note.v7" no longer resolves to
  the type named "p3.note" and gets relabelled "p3.note.v1".
- F2c: a schema version string, and a schema name, are not ids. Snapshot
  filters, typed puts and deletes, and raw puts match only declared ids.
- F3: a raw record under an id no binding declares is refused. The
  registry no longer tries each binding's parser and stores the record
  under the first that accepts it (P5: an identity parser registered first
  took everything).
- The dead resolvePayloadSchemaVersion helper is gone.

Tests: the three shape- and alias-routing tests are replaced by P1, P2,
P3/P4 and P5 as behaviour tests. The one-type-per-schema test registers two
bindings ahead of the holder (F6/M11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:41 +02:00
Meta Vens
426c956a40 CultCache TS: one function derives a definition's schema identity
Soul F1 (hold on 98999997..6e47710e): the cache stamped records with
schemaId ?? schemaName ?? type, while the CultNet registry and CultMesh's
schema descriptor used schemaId ?? type. A definition that set only a
schema name was stamped one id by the cache and bound under another by the
registry, so its records could land on a peer as a different type.

- schemaIdentityOf(definition) returns the schema id, schema name, schema
  version and every id the definition answers to (its own first). It is
  exported, and it is the only place that derivation lives.
- The cache's catalog entry is built from it; nothing else changes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:49:41 +02:00
Meta Vens
abc847337d cultmesh-rs: the refusal is sent unreliable and unordered
The refusal went reliable and ordered, but the session ends with it, so
it is never resent: a peer missing any earlier reliable packet from the
server held it behind that gap forever. It now goes unreliable and
unordered, so it is delivered on arrival whatever the peer is missing.

The test drops the server's snapshot reply before the refused put: the
refusal still arrives, followed by the goodbye.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:41:35 +02:00
Meta Vens
b3176de1f8 cultmesh-rs: a refusal carries fixed text, never a sink's or source's error
The refusal sent to a rejected peer carried the rejection's Display
text, which for SinkRefused and SnapshotSourceFailed is the sink's or
source's own error. Odin's sink formats the peer-supplied document type
into its error, so a peer's input came back on the wire.

The refusal now carries fixed text per rejection variant
(REFUSAL_TEXTS). The rejection returned from poll_once still carries the
detail for the caller; the server itself logs nothing.

publish_cultnet_message_to_rudp_catalog no longer quotes a catalog's text
unfiltered: it reports "refused the put", quoting the text only when it
is one of the fixed refusals.

The test fixtures' sink and source errors carry a canary; the refused
peer's text is the fixed one and never contains it. A fake catalog that
refuses with a canary of its own shows the publisher's error stays
fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:41:35 +02:00
Meta Vens
ead1a25aff cultmesh-py: RUDP option rules apply only to a RUDP server; the daemon reports a refused limit as a usage error
The RUDP option rules refused a TCP-only server (enable_rudp=False) for
RUDP limits it never uses. Rust checks those options only for the RUDP
server they configure, so Python now checks rudp_resend_delay_ms,
rudp_max_pending_reliable_packets, rudp_max_fragment_bytes and the empty
snapshot's fragment rule only when enable_rudp is true. The snapshot
limits still apply to every server.

The daemon turned --max-snapshot-documents 0 into a traceback. The server
still owns the rule; the daemon reports its refusal through
parser.error: exit status 2 and the rule named, no traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:38:20 +02:00
Meta Vens
699432128b cultmesh-py: a put the store cannot take is refused to its peer, never raised
A well-formed put still killed the RUDP thread: _handle_raw_put called
apply_raw_put_message unguarded, and cache.put_envelope raises for a
blank recordKey, a payload that is not MessagePack or does not decode to
its schema, a global document under another key, or anything a
document's own decoder or validator raises. Over TCP it ended the
connection.

The apply site now refuses any put the store cannot take to the peer
that sent it, as malformed_document_put, over RUDP and TCP alike, and
logs the error's type. Neither the refusal nor the log quotes the error,
which can quote the put. No new shape checks: the store decides what it
can take.

The test sends each shape over RUDP and over TCP and shows the RUDP
thread alive, the TCP connection serving its next message, and other
peers served.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:38:20 +02:00
Meta Vens
d655f7585b cultmesh-ts: the served-bound test comments stop saying admission sizes receipts
Admission has sized only the served reply since a1065cab; three comments
and one fixture assertion still described the receipt bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:36:24 +02:00
Meta Vens
3c9e195b36 cultcache-rs: pin the text a failed write reports
cargo-mutants found no test of what CultCacheStoreWriteFailed says. Both
outcomes now assert the path and the outcome in the error text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:17:40 +02:00
Meta Vens
7472ede480 cultcache-rs: an unlock failure never turns a landed write into a failure
with_shared_lock, with_exclusive_lock, try_with_exclusive_lock and the redb
store's with_database returned the unlock's error in place of the action's
result, so a write that had replaced the store could be reported as failed
and read by Odin as not replaced. Closing the lock file releases the lock
whether or not the explicit unlock succeeds, so all four now release through
one helper that returns the action's result. The unlock failure is not
surfaced anywhere: there is no diagnostic channel on these paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:14:27 +02:00
Meta Vens
1d65755a3b Test: catalog write arguments and the catalog refusal's names and message
Stryker on 43ed9396..3763a53c left 20 survivors, in two places:
- cultMeshCatalogWriteArguments. No test wrote through a catalog's
  replace or submitPrediction, so which argument was the context went
  unobserved. A test now writes through both, in every calling form,
  and checks the context and value that reached the document.
- The catalog refusal's schema names and message. The refusal test now
  checks both names and that the message names both types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:56:46 +02:00
Meta Vens
93ac594513 cultcache-rs: a failed store write says whether it replaced the file
Every single-file write now carries CultCacheStoreWriteFailed in its error
chain, above the cause. NotReplaced: creating, writing, syncing or renaming
the staging file failed, and the store file holds what it held before.
ReplacedNotDurable: the rename landed and the directory sync failed. Odin
could not tell these apart, so it kept every failed snapshot as a candidate.
The text names the path and outcome only.

fsync does not fail on a healthy filesystem, so the staging and directory
sync steps take a fault armed per thread by tests; builds outside tests never
fail there. The staging write (RLIMIT_FSIZE child) and the rename (a
directory at the store path) fail for real.

README: the write outcome, the dangling-link rule and the no-echo rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:59 +02:00
Meta Vens
e4e2a0b9d7 cultnet-ts: drop decodeRawDocumentPut, which only receipt admission used
The registry method existed on this branch only to size a receipt at put
admission in cultmesh-ts, which no longer does. It never reached main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:41 +02:00
Meta Vens
a1065cabe0 cultmesh-ts: admission sizes only the served reply, never a receipt
Admission sized the larger of the snapshot reply and a receipt echoing
the put, so a put servable at the bound was refused, within 89-125 bytes
of it, for a receipt an application that sends none never sends. Whether
a receipt is sent is onDocumentPutRaw's to decide after it stores the
put, so admission cannot know it (ruling: Self, fork A option a).

Admission now sizes only the snapshot reply serving the document alone.
A receipt that cannot be sent takes the existing path: onError hears why
and the peer is told with a cultnet.error.v0 that it is not coming. This
removes the receipt bound of 9757ed54 and its test.

The new test pins the band at its boundary with puts whose echoed receipt
is over the limit: exactly at the limit is admitted and served, with no
receipt and with an echoed one, and one byte over is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:37:40 +02:00
Meta Vens
853e8db5e5 cultcache-ts: refusals echo a header only in its known shape; a dangling link is not absent
An unsupported header is echoed only as cultcache.store.v<digits>; any other
is described by its byte length (a 2,914-character header was echoed whole).

pullAll's ENOENT catch wrapped the whole decode, including the legacy-repair
write, so a failed repair read as an empty store. It now covers only the read,
and only nothing at the path is absent: a dangling symbolic link is refused
instead of being replaced by the writer. A soft pushAll uses the same read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
53f6b54e15 cultcache-py: refusals echo a header only in its known shape; a dangling link is not absent
An unsupported header is echoed only as cultcache.store.v<digits>; any other
is described by its byte length, since the bytes are the store's.

A read that found nothing through a dangling symbolic link returned an empty
store, and the writer then replaced the link. Both stores now share one read
rule: only nothing at the path is absent. JsonLinesBackingStore also drops
`Path.exists()`, which reported a symlink loop or a file-as-parent as absent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
c70dbd58be cultcache-rs: the header canary checks for the quoted header, not a prefix
"cultcache.store.v" is a prefix of the "cultcache.store.v1" the refusal names,
so the unquoted check failed on a correct refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:35:19 +02:00
Meta Vens
0e6e6321c2 CultMesh Kotlin: the reconciliation self-test note gets its own schema
4002a996's refusal caught the self-test itself: the reconciliation block
registered kotlin.reactive_note beside kotlin.reactive_note.ui under one
schema id in one cache. The block tests reconciliation, not two types for one
schema, so its note is now kotlin.reconcile_note under its own schema id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:34:37 +02:00
Meta Vens
53cff052b8 cultcache-rs: refusals never echo store bytes; a dangling link is not absent
A decode refusal kept serde's text, which quotes values read from the file
(`invalid type: string "..."`). The detail is now CultCache's own context and
record refusals, which name only a record's key and schema id, plus a fixed
description of the decoder failure's kind. rmp-serde reports no position, so
none is given. An unsupported header is echoed only in the shape
`cultcache.store.v<digits>`; any other is described by its length.

`is_absent` used `fs::metadata`, which follows links, and a read that found
nothing through a dangling link returned an empty store, so a writer then
renamed a regular file over the link and the store left its volume. Only
nothing at the path is absent now (`symlink_metadata`), and a read through a
dangling link is an io::Error.

Tests: canaries for a catalog and a legacy value and for unshaped headers,
the dangling link on every read with nothing written, and a
`cultcache.storeX` header that claims no format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:31:59 +02:00
Meta Vens
03a344da20 CultMesh TS: a catalog's value type is never inferred from a value
43ed9396's type test caught it: TypeScript inferred the catalog value type
from a written value or an expected result through the conditional's false
branch, so replace and submitPrediction accepted any value and an annotated
collection read took the annotation as its type. The false branch is now
NoInfer, so only a definition or an explicit type argument decides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:08:38 +02:00
Meta Vens
38000efab2 cultmesh-rs: rustfmt the refusal tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:55:30 +02:00
Meta Vens
25b075a473 cultmesh-rs: the rejection test reads the refused peer's acknowledgement before going on
A refused peer now acknowledges its refusal, and that datagram reaches
the server after the session has ended. The test took the next poll to
be the second client's request and failed on the dropped acknowledgement
instead. It drains the server until idle after each refusal is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:52:27 +02:00
Meta Vens
835bee3ae0 cultmesh-py: the daemon refuses a snapshot document limit of zero
The daemon test ran with --max-snapshot-documents 0 to make every
snapshot a peer error. A limit of zero is now refused at startup, as the
Rust document server refuses it, so the test pins what the daemon does
with it: it exits non-zero naming the rule and never writes its ready
file. The limit's enforcement as a peer error stays pinned in process by
test_cultmesh_local_server_rejects_oversized_snapshot_responses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:48:40 +02:00
Meta Vens
9f66852d22 CultNet TS: the registry's refusal message names the schema id
Stryker left one survivor on 9adea366: emptying the refusal message. For a
same-type claimant the message did not name the schema id at all. It now
names the claimant's schema id in every case, and the refusal test checks
that it does. The message is built from document types and the schema id
only; registration carries no record, so no record value can reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:55 +02:00
Meta Vens
4002a996b8 CultMesh Kotlin: a cache refuses a second type for one schema id, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultCache.register wrote its type and schema maps
unconditionally, and put, get, getAll and delete register implicitly, so one
Kotlin cache could hold two types for one schema and codecForSchema answered
with the last.

- register refuses a codec whose type already holds another schema id, or
  whose schema id another type holds, with the new
  CultSchemaConflictException (schema id, both document types, empty record
  key), the Kotlin counterpart of the other runtimes' typed conflict. The
  implicit registration in put, get, getAll and delete goes through the same
  check. Registering the same type for the same schema again stays allowed.
- Self-test: other-type same-schema and same-type other-schema claimants,
  through register, put, get, getAll and delete, each in a fresh cache; the
  first codec still answers for its schema afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:48 +02:00
Meta Vens
fc3f3ee5ff CultNet Rust: a second type for one schema id is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultNetDocumentRegistry::register inserted into its
type and schema-id maps unconditionally, so the last binding for a schema id
won, as the TypeScript registry did before 9adea366.

- register returns Result and refuses a binding whose type already holds
  another schema id, or whose schema id another type holds, with
  cultcache_rs::SchemaConflictError: the new binding's schema id, both
  document types (a binding carries no schema name), an empty record key.
  Nothing is bound on refusal. Binding a type to the schema id it holds
  again replaces the binding.
- Call sites propagate the refusal: the cultmesh-rs registry macro, the
  interop peer example, and the tests.
- Tests renamed to what they test: raw snapshot replication hydrates the
  receiver's own type for the schema; a reactive document coalesces direct
  member writes.
- New test: both claimant shapes in both orders, the first binding intact
  afterwards, and a same-type rebind replacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:47:39 +02:00
Meta Vens
d465845516 cultmesh-rs: a rejected peer is told why, and nothing it sent is acknowledged
Every application rejection, DocumentUnservable included, ended with
self.sessions.remove(&key): no refusal and no goodbye. The peer could not
tell a refusal from packet loss, and a publisher timed out and retried
the same put.

The session still ends, but the peer is now sent the refusal as a
cultnet.error.v0 carrying the reason's text (no code or details, as the
TypeScript server's refusals carry none), then the session's goodbye.
The refusal is built before the reset and carries the goodbye's emptied
acknowledgement fields, so it never acknowledges the refused message:
publish_cultnet_message_to_rudp_catalog reads that acknowledgement as
admission. A refusal that cannot be encoded or queued is not sent; the
goodbye still is.

publish_cultnet_message_to_rudp_catalog now returns a catalog's refusal
as an error naming it, instead of waiting out its flush deadline.

Tests read the refusal from the client side: the refusal text, the put
still pending after it, and the goodbye invalidating it; and a publisher
refused by a sink hears the sink's reason well inside its deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:42:44 +02:00
Meta Vens
1a173f6000 cultmesh-py: a put that could never be stored is refused, not raised
_unservable_put sized a put by building the envelope it would store, and
that read document["recordKey"] and document["payload"] directly: a put
over RUDP with no recordKey raised KeyError out of the RUDP loop and
ended the thread for every peer. A string payload raised TypeError the
same way.

_unservable_put now refuses a put whose document lacks a string recordKey
or a binary payload as malformed_document_put, the code the server
already uses for a put with no document map, before any sizing reads it
and before its early return, so the TCP path refuses it too instead of
raising in apply_raw_put_message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:42:43 +02:00
Meta Vens
d04d71e898 cultmesh-py: refuse options that could answer nothing, as Rust does
With one-byte fragments and a reliable queue of 16, even an empty snapshot
can never be queued: every reply vanished while rudp_send_failures
counted, and the peer timed out. Rust refuses such a configuration at
startup in validate_options; Python's __post_init__ accepted it.

__post_init__ now applies Rust's rules to every option the two servers
share, in Rust's order: max_snapshot_bytes, max_snapshot_documents,
rudp_resend_delay_ms, rudp_max_pending_reliable_packets and
rudp_max_fragment_bytes must be greater than zero (the first two when
set; zero was accepted before), max_snapshot_bytes must hold an empty
snapshot response, and an empty snapshot response in fragments of
rudp_max_fragment_bytes, counted rounding up, must fit the lesser of
65535 and the reliable queue. Rust's session, lifetime and payload-budget
rules have no Python option to apply to.

The test measures the empty snapshot the server actually sends, pins each
rule at its boundary, and shows the configuration at the bound does
answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:41:19 +02:00
Meta Vens
3763a53cef CultMesh TS: a catalog refuses a second type for a schema it holds, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." A catalog indexed handles by schema id and by schema
name and version with plain map writes, so a handle of another type for a
held schema silently replaced it for every lookup by schema alone.

- Document and collection catalogs refuse, with the SchemaConflictError the
  registries use, a typed handle whose type differs from the one holding its
  schema id or schema name and version, or whose type already holds another
  schema id. Nothing is indexed when a handle is refused.
- Allowed: handles of one type for one schema (one per record), the same
  handle again, and a handle without a type, which claims none.
- Test: other-type same-id, other-type same-name-and-version and same-type
  other-id claimants, both orders, via add and via construction, for both
  catalogs; the first handle still answers afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:37:32 +02:00
Meta Vens
43ed9396d3 CultMesh TS: a catalog lookup infers its value type from the definition
The deleted parse option was the only thing that typed a catalog lookup, so
every caller had to name the value type. Catalog methods now take the schema
argument as a type parameter: a CultCache document definition yields its own
value type, an explicit type argument still wins, and a bare schema
descriptor yields unknown. CultMeshCatalogValue names that rule.

- Document catalog: tryDocument, document, latest, replace,
  submitPrediction, authoritativeWriter, predictionWriter, watch.
  Collection catalog: tryCollection, collection, latest, watchChanges.
- The two copies of the (value, options) / (context, value) argument split
  collapse into cultMeshCatalogWriteArguments, and the typed lookups share one
  private #require per catalog.
- Tests: the three reads that named their type infer it again. A type-level
  test pins inference for reads, rows, watches, writes and writers with
  @ts-expect-error on wrong assignments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:36:18 +02:00
Meta Vens
e9d5ae2cad cultnet-ts: pin that a fragmented send over one datagram's size is sent
The EMSGSIZE pre-check measures a send's largest datagram, which is one
fragment when maxFragmentBytes is set. Nothing pinned that: a check that
compared the whole payload (Soul's S-TM2) would refuse every large
fragmented send, and large sends are live. A 70,000-byte send with
1,024-byte fragments must not throw and must arrive whole. The test
kills the mutant whose size check ignores maxFragmentBytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:47 +02:00
Meta Vens
6e47710e81 Test: catalog lookups by definition name their value type
The catalogs' parse option went with asSchemaAlias, and with it the only
thing that inferred a catalog lookup's value type. Three test reads now pass
the note type explicitly, as a caller does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:05:11 +02:00
Meta Vens
ff8d89d512 cultcache-ts: an empty store file is refused, not read as empty
An existing zero-byte file is decoded and refused, like any other
undecodable store, matching the Rust and C# runtimes. A missing file stays
empty (ENOENT only), and tests pin that a symlink loop and a file-as-parent
surface as ELOOP and ENOTDIR. `[0x90]` stays an empty legacy store.

Refusal text no longer points at a variant-aware runtime: an unsupported
header names the format found, and a v1 record with a fifth slot says the
file is not a valid store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
202c4bbd63 cultcache-py: only a missing store reads as empty; an empty file is refused
`Path.exists()` returns False for ELOOP and ENOTDIR, so a symlink loop or a
file where the parent directory should be read as an empty store. The read
now catches FileNotFoundError only. An existing zero-byte file is refused
with ValueError, like any other undecodable store, matching the Rust and C#
runtimes. `[0x90]` stays an empty legacy store.

Refusal text no longer points at a variant-aware runtime: an unsupported
header names the format found, and a v1 record with a fifth slot says the
file is not a valid store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
271801106b cultcache-rs README: only a missing store is empty; an empty file is Undecodable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:04:39 +02:00
Meta Vens
6acc414639 cultcache-rs: only a missing store reads as empty; an empty file is not a store
The single-file reads tested `path.exists()` and treated false as an empty
store. `exists()` is also false when the path cannot be inspected, so a
symlink loop, a file where the parent directory should be, or a parent
without permission read as an empty store, and a writer would then replace a
store it never saw. The read now maps only ErrorKind::NotFound to empty;
every other failure stays an io::Error. `pull_all`'s lock-free absence check
gets the same rule.

A store file that exists with zero bytes is Undecodable: no CultCache writer
produces one. `[0x90]` stays an empty store: it is the legacy envelope array
with no envelopes and carries no header, so it claims no format.

Refusal text: an unsupported header names the format found and the one this
runtime reads, and a v1 record with a fifth slot says the file is not a valid
store. Neither suggests a variant-aware runtime; the C# reference refuses the
same v1 bytes.

Tests pin a symlink loop and a file-as-parent on every read, the empty file,
the newer-header truncation boundary (1-19 bytes Undecodable, 20 and up
UnsupportedFormat), the refusal text, and staging cleanup when the rename
fails rather than the write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:02:27 +02:00
Meta Vens
2c959b228f CultMesh TS: asSchemaAlias is gone; a catalog finds a typed handle by its type
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." One type owns a schema, so a handle never needs
presenting as another type for the same schema. Parity with C# 1e045502.

- Delete asSchemaAlias from the document, bound document, collection and
  bound collection handles, and cultMeshSchemasAreCompatible, which only it
  and the catalogs' alias step used.
- Document and collection catalogs answer a typed lookup from that type's
  handle or not at all. A descriptor without a type is still looked up by
  schema id, then schema name and version. The parse option on every catalog
  method existed only to convert an aliased value, and is deleted.
- CultMeshNode passes the caller's definition to its cache. The node no
  longer resolves another type's definition by schema id, and no longer
  re-parses values into the requested type, so a type the node did not
  register is refused by the cache. node.get and node.put now return the
  cache's own value, as CultCache.get and put do.
- Tests: the noteAliasDocument fixture is deleted. Tests that used it for
  publication, sync, snapshot, store and prediction paths now use
  noteDocument. otherTypeForNoteSchema stands for another type claiming the
  note schema: catalogs do not find it, and a node refuses it on every path
  and refuses to register it beside noteDocument.
- README stops documenting node aliasing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:17 +02:00
Meta Vens
9adea36680 CultNet TS: a second type for one schema is refused, typed
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." CultNetDocumentRegistry.register silently replaced
the binding for a schema id, so two types could claim one schema and the
last one registered won.

- register refuses a binding whose definition claims a type, schema id or
  schema name another bound definition already holds, with the
  SchemaConflictError CultCache uses for the same refusal: the new
  definition's schema id, both schema names, an empty record key.
- Binding the same definition again still replaces its binding, and a
  definition listing another's id as compatible is not a second owner.
- Test: identical-id, same-name and same-type claimants refused in both
  registration orders, leaving the first binding in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:08 +02:00
Meta Vens
016df4626c Merge FEC: own the GF(2^8) Reed-Solomon math; drop reed-solomon-erasure
Operator: "FEC: Please own the math." rs-gf256-v1 implemented in
media_fec.rs, wire-identical to the crate (1,795 geometries, independent
derivation, 6,520 cross-version recoveries, 60k hostile blocks identical).
lru and instant advisories gone; 13 fewer dependencies. combine uses one
product row per coefficient: encode ~1.0-1.4x, decode ~1.2-1.4x the crate.
Follow-up: k^3 inversion makes 255+1 recovery ~124 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:48:18 +02:00
Meta Vens
b7908668f4 cultcache-rs README: how a consumer tells store read failures apart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:21:06 +02:00
Meta Vens
d9d4b32ded cultmesh-rs: connect() takes a server over any snapshot source
The previous commit's tests did not compile: connect() was typed to the
default Source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:20:22 +02:00
Meta Vens
c0ab617e61 cultcache-rs: pin that only a cultcache.store header claims a format
A file that opens like a store but carries some other string where the
header goes is undecodable, not a store in a format this runtime does not
read. cargo-mutants left the guard's `true` mutant alive without this case.

The size-limited child's stdout and stderr now travel with the parent's
failure message, so a child that fails for another reason says which.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:17:45 +02:00
Meta Vens
d54110fe24 cultnet-ts: the send-failure tests send requests without response-only fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:14:20 +02:00
Meta Vens
5ab0f9d337 cultmesh-rs: the at-bound fixture exercises the default served_record
The test Source overrode served_record in both modes, so no test ran the
trait's default and a default that stripped provenance survived. Source
now keeps the default, which the at-bound fixture pins (the record as
received); a separate StrippingSource overrides it for the stripping and
failing-source tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:11:15 +02:00
Meta Vens
31b6c09d64 cultnet-ts: the send-failure tests build operation requests the contract accepts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:08:51 +02:00
Meta Vens
9757ed54a5 cultmesh-ts: a put is refused unless its receipt could be sent
Admission now sizes two replies built from the record the registry
would store: the snapshot reply serving it alone and the receipt
echoing its value under the put's message id with the server's default
provenance, through the one receipt builder the send path uses. A put
whose larger reply exceeds maxPayloadBytes, 65535 fragments or the
reliable queue is refused before onDocumentPutRaw, so a stored put
always gets its receipt when the application stores through the
registry and echoes that value. The existing fallback (a receipt that
cannot be sent all the same reaches onError, and the peer a
cultnet.error.v0) stays for handlers that acknowledge something larger.

The served-bound fixtures carry a long storedAt, which the served record
keeps and the receipt replaces, so they still pin the served bound; a
new test pins the receipt bound: one byte over is refused, exactly at
the limit is stored and receipted.

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