Commit graph

1,711 commits

Author SHA1 Message Date
Meta Vens
2e12e9eebb Mesh single-file helpers: delete the second reader, the older catalog layout and the lock-free replace
Deletes CultMesh's ReadSingleFileSnapshot, HasOlderCatalogLayout, ReadLegacySchemaCatalogEntry,
WriteFileAtomically and the File.Exists gate, ReadStore's entry-reader delegate, and the three tests
of the older catalog layout. This commit does not build: the Mesh read and write sites still name
the deleted reader; the next commit routes them through the CultCache single-file store.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:59:13 +02:00
Meta Vens
245b0619b4 Declared-range check: find npm's package root on Unix as well as Windows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:35:45 +02:00
Meta Vens
172759fd30 Declared-range check: a clone without release tags is refused rather than passing for want of releases to build
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:30:15 +02:00
Meta Vens
4f76bb9391 cultcache-ts 0.15.0; cultnet-ts and cultmesh-ts declare ^0.15.0; a check builds the tagged releases a declared range admits
cultnet-ts imports readSingleFileStore and cultmesh-ts imports schemaIdentityOf and SchemaConflictError, none of which cultcache-ts 0.14.0 exports. The new check builds each tagged release a declared range admits and requires every runtime name the dependent imports to be an export of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:48 +02:00
Meta Vens
2d26c8100e CultCache TS: nothing at the path needs both looks to agree; the second look is a test seam
readStoreBytes reports an absent store only when the read and the lstat both find nothing. Any other lstat failure is thrown, and an entry the lstat finds that is not a link is reported as appearing during the read. The manifest walk also runs a soft pushAll over every vector.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:47 +02:00
Meta Vens
12805eb32f CultCache Rust: a soft push_all asks the one reader, as TypeScript's does
Absent writes, a store it reads is left byte for byte, and anything the reader refuses (an unreadable file, a dangling link) is refused. The readability manifest walk now runs a soft flush over every vector, so TypeScript and Rust are pinned by the same files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 01:28:47 +02:00
Meta Vens
42f05f6681 CultCache C#: a dangling Windows junction at the store path is an IOException, and the tests reach it without a privilege
Opening a junction whose target is gone fails with UnauthorizedAccessException, not the
IOException the dangling-link rule promises. ReadDisk now turns that into the same
IOException. The dangling-link tests build a directory junction (mklink /J), which needs
no privilege, and the file-symlink cases skip where the account cannot create one.
The fixture removes links before its recursive delete.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:56:53 +02:00
Meta Vens
db4ecb1f8b Readability vectors: a store followed by one stray byte is refused by every runtime
The manifest held only three-byte and whole-store trailers, so a Rust reader that
tolerated a single trailing byte passed every suite while C#, TypeScript and Python
refused the file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:53:36 +02:00
Meta Vens
f218c9e8f3 Tests: pin the authority record shape checks and the dangling-link message
Mutation survivors of the authority reader (record count, schema id, type) and of the
reader's dangling-link error text each get a test that fails when the rule breaks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 23:28:53 +02:00
Meta Vens
a23eb8219d TS test: the forged-header case spells its newline as an escape
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:19:15 +02:00
Meta Vens
8a833730c9 cultnet-ts: Idunn authority files are read by cultcache-ts's one store reader
readAuthorityRecord decoded the store itself and accepted only cultcache.store.v1. It now calls
readSingleFileStore, so v3 reads, a zero-byte file is a StoreUnreadableError, and a refused
header is never echoed. Tests derive their bundles from the Rust-written fixtures.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:12:15 +02:00
Meta Vens
0122761a74 TS: the one store reader is exported and decides soft pushAll; absent-read and forged-header tests
pushAll({soft}) asked readStore, so it skipped every file the reader refuses. It now asks
#readDisk: absent writes, readable skips, unreadable refuses. The reader is readSingleFileStore,
synchronous and exported, so a consumer reads a store through it instead of decoding one itself.
New tests: soft pushAll over absent, readable and five unreadable files; reading an absent store
writes nothing; a forged header that does not start at the prefix is never echoed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 22:12:15 +02:00
Meta Vens
da2b9482e5 Rust: the rename fault fires inside replace_file_atomically, at the call a swallowed error would hide
The seam sat before the call, so a mutant that swallowed the real rename's result was invisible to it. It now makes the rename itself fail, on both platforms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:11:07 +02:00
Meta Vens
7b815e1ca8 TS and Python: pin the refusal text (v3 named, ASCII digits only); TS comments say a zero-byte file is refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:05:38 +02:00
Meta Vens
9a251fe60c Rust: one store-header reader; bin-encoded headers refused; rename fault seam and the tests the survivors asked for
store_header is the only reader of a store file's leading bytes. It returns the header slot (a MessagePack str only, never bin, as in C#, TS and Python) and separately the proof that the bytes are one whole array, so a newer store cut short after its header is still named unsupported. leading_header, read_array_header and read_string are deleted. decode_store_file takes the header the reader already found.

Vectors bin-header-v1/v3/v9 join the readability manifest as refuses x4. Tests: a rename fault step (WriteStep::Rename) shows a failed rename is NotReplaced, the store intact and no staging file left; every merging writer refuses through a dangling link; the unsupported-format refusal names v1 and v3 and echoes only ASCII-digit headers.

Not yet compiled or run at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:05:37 +02:00
Meta Vens
c4826a39a3 C# tests: the directory refusal shows a header only as store version digits, so the suffixed legacy names are no longer echoed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:00:51 +02:00
Meta Vens
eb79b21e51 C# tests: read "echoed" from how the refusal introduces the header, since it also names the readable formats
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:51:55 +02:00
Meta Vens
3f33445d84 C# tests: assert the header refusal text with plain checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:51:00 +02:00
Meta Vens
9e481f59b7 C#: detect a dangling store link by its reparse-point attribute (Caching has no FileInfo.LinkTarget)
Commit 425531d8 does not build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:43 +02:00
Meta Vens
425531d88e C#: a dangling link is an I/O error on read; the directory refusal echoes a header only as store version digits; contract names Rust's real type
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:12 +02:00
Meta Vens
75ba6c0f5c Mesh: a zero-byte store file is refused, not read as empty; the alias test is retargeted to one type per schema
CultMesh's single-file reader had its own zero-length-means-empty branch, so a Mesh write
replaced a zero-byte file the cache refuses (merge-r1-precedence). It now asks the cache's
snapshot reader and gets its verdict. The auto-merged test that read a same-schema instance
through MeshNoteAliasDocument cannot be built under retire-schema-alias; it now states the
ownership rule it protected (a database serves only what a shard owns) with the one type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:49:12 +02:00
Meta Vens
0f13c7d368 C#: the unreadable-format refusal names every format the runtime reads, v3 included
A mutation survivor on the merge's refusal text: dropping v3 from the list passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:17 +02:00
Meta Vens
e5f06a2a49 Python: fix the payload byte literal in the refusal-text test (the previous commit did not parse)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:04:16 +02:00
Meta Vens
0bd1bac963 Python: pin what the unreadable-format and extra-slot refusals say and what the runtime reads
Mutation survivors on the merge's refusal text (the two readable formats, the
unrecognised-header description, the record-slot refusal) had no test pinning them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:02:15 +02:00
Meta Vens
d350e150b0 Rust: bring back R1's leading-bytes readers for leading_header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:49:01 +02:00
Meta Vens
f35867a232 Rust: a newer store cut short is still named unsupported; tests follow the single reader
R1's rule that the header alone decides an unreadable-format store (a newer store
cut short is not damaged) is kept as leading_header, asked before the whole-file
proof. A directory at the store path now stops a write at the read as an I/O
error, so the two tests that used it to reach a failed rename assert that instead,
and the two c2a header tests assert R1's UnsupportedFormat kind, not c2a's old text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:48:34 +02:00
Meta Vens
ff0251bbf8 Fix the R1 merge: close the Rust store_header visitor; a whole-store replace through a dangling link keeps replacing it
The merge left store_header's visitor unclosed (does not build). R3's pin that a
whole-store flush replaces a dangling link survives the single reader: the reader
still refuses the link as an I/O error, and the flush, which has no content to
carry from a link that holds none, writes v1 over it (Rust write_all_unlocked, TS
pushAll).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:46:39 +02:00
Meta Vens
bad511f5ac Merge CultCache R1 (f789c8d1) into C2a: one reader per runtime, R1's failure taxonomy
R1's rules win where both sides decided the same question: a zero-byte file is
not a store (refused as unreadable in every runtime), only nothing at the path is
empty, a dangling link is an I/O error, refusals never echo stored values and echo
a header only as cultcache.store.v<digits>, writes report Rejected, NotReplaced
and ReplacedNotDurable. C2a's rules win on what it added: the v3 element-id
header, variants, whole-file proof, typed unreadable errors in C#, TypeScript and
Python. Rust keeps R1's CultCacheStoreUnreadable{path, kind}; C2a's
StoreUnreadableError is deleted. The readability manifest's zero-byte row is
refuses x4.

Not yet built or tested: verification follows in the next commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:41:35 +02:00
Meta Vens
15ccdbb953 Test: a staged write a commit landed is no longer staged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:52:42 +02:00
Meta Vens
a9741dbf57 Tests: pin the staged-key rule, the multi-key refusal and the carried record's stamp; a pull no longer clears staged keys
A pull runs only on a clean store, and a store is clean exactly when nothing is staged, so the pull's clear was redundant and is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:47:47 +02:00
Meta Vens
86eea40bce One owner for the foreign-record refusal: it reaches the cache, names every key, and refuses only what the cache wrote
CacheBackingStore.RefuseForeign now decides, for both store kinds, which written keys a foreign record refuses, drops the
store's staged change and the cache's copy of each through the Loaded path a pull uses, and throws one conflict naming every
refused key. The single-file store tracks the keys staged since its last load or write, as the directory store already did, so
a clean copy of a record another writer made foreign is carried, not refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:34:09 +02:00
Meta Vens
bc15dba81b Test: the owner's-id case of the moved-record lay-back publishes the owner's catalog entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:00:52 +02:00
Meta Vens
0a98004bc4 Tests: pin the Stryker survivors in the lay-back and the directory refusal
The file's record moves when only its storedAt or only its schema id differs from the loaded one; a
staged removal is refused and dropped with the rest; a refusal leaves staged what it did not refuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:58:50 +02:00
Meta Vens
603d972f57 Single-file store: an unowned variant has no resolved document at load, so its in-place check is skipped
The previous commit serialized every unowned record's document at load, which a variant cannot give
until the cache resolves it (five tests failed on it). A variant persists its delta, never its
resolved document, so there is no in-place change to detect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:54:41 +02:00
Meta Vens
b0f7d59c96 Directory store: a refused foreign overwrite leaves the pending set with its failed write
WriteGeneration left the refused key in the dirty and deleted sets, so every later generation
refused it. The refusal is now one check before any staging, shared by flush and commit, that
removes the refused keys from the pending sets. Tests cover both store kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:51:54 +02:00
Meta Vens
c246e0c5ae Single-file store: lay back an unowned record only while the file still holds it as loaded
A whole-view write laid back the loaded bytes of an unclaimed record over whatever the file held,
reverting its owner's newer write and moving storedAt backwards. It now lays back only while the
file holds the loaded (schemaId, storedAt); a newer record is carried as the file has it and a
removed one is not written back. The v3 header is decided by records that are written as the cache
holds them, so a laid-back id-less record no longer marks the store. A document changed in place is
written as a write of the record, not dropped. A refusal drops the store's copy of the refused
records, so it no longer refuses every later write.

Does not build with the directory store untouched? It builds; the directory twin follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:51:54 +02:00
Meta Vens
a771e71bd6 Rust test: pin the cargo-mutants survivors in the catalog lay-back
- The fixture gains an older entry listing the id ahead of the one that
  owns it, and the whole-store write carries the record under the
  registered type a Rust cache's pull gives it, not the entry's name.
  Kills the own-id lookup flip and the ||-to-&& flip in the entry filter.
- A store path that cannot be read (a directory) is an error, not an empty
  store (the NotFound guard had no test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:59:52 +02:00
Meta Vens
904c00ac81 Test: pin the Stryker survivors in the foreign-record and lay-back cut
- The lay-back fixture gains an older entry listing the id ahead of the
  one that owns it, and a test where another writer re-describes the id
  after the load: a whole view (flush and commit) lays back the entry it
  loaded. Kills the survivors in Publisher's own-id preference, the
  laid-back entry's precedence in the written catalog, and the commit's
  choice of that catalog.
- A store whose file is gone lists no foreign records.
- The registry's public resolution still refuses an unclaimed id, naming
  the schema and the id (was NoCoverage).
- A directory load retried after a writer's commit lists each foreign
  record once.
- Deleted LaidBack.ExceptWith(replaced): equivalent, since an upserted key's
  new entry already carries what the cache would be told.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:59:52 +02:00
Meta Vens
976b57f10c Test: each runtime's writes leave another runtime's records and catalog entries unchanged
C# writes a store; Rust, TypeScript and Python each write their own note
into it; the C# record and catalog entry are unchanged and C# reads the
store. C# writes again, and the other runtime's record and entry are
unchanged too. Compared on decoded MessagePack values, every slot of the
entry and its members included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
eae56189c6 Rust: a write lays back the catalog entry the file holds for an id
C2a F2, widened by Self. The schema-identity probe showed a Rust write
replacing the C# catalog entry it read with a fabricated one: version
"sha256:...v1", members [], canonical JSON naming the hash.

- The store's one reader returns the file's catalog beside its envelopes.
- The encoder writes back the entry the file holds for each id it writes
  (by own id, else a lister) and describes an id itself only when the file
  has none, or when a registered type owns the id under another name (a
  rename, whose rule is unchanged).
- Test: a push and a whole-store push both lay back a C#-shaped entry byte
  for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
a11d30fce2 C#: a whole-view write lays back a record under an id its type does not own
C2a F2, widened by Self: records and catalog entries a runtime does not own
survive its writes byte for byte. The schema-identity probe showed a C#
rewrite restamping every other runtime's record to its sha256 id and
dropping their catalog entries.

- The single-file store remembers, per key, a record it loaded under an id
  its type neither owns nor declares compatible (resolved through the
  catalog's schema name or an entry pointing at a local id), with the entry
  that published it. While the store still holds the entry it loaded, a
  whole-view write (flush or unconditional commit) lays that record back
  with its id, storedAt, bytes and catalog entry. A write of the record
  itself still stores it under the type's own id.
- A record published by an entry under the type's own id is this schema's
  and is re-encoded as before (the registered entry wins its own id).
- A laid-back record's load-minted element ids stay in memory only, so
  MintElementIds still rewrites it.
- The rolling-deploy tests lose their premise: an older version's whole
  view no longer sheds a newer record's member. They now pin that it lays
  the record back and the newer cache's condition holds; the A-B-A test
  keeps F6's defence through an explicit older write.
- Contract: the stamp and the lay-back rule in the persistence format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 21:20:11 +02:00
Meta Vens
7dd19baac3 Test: a store holding a schema the cache does not register opens and carries it
The corrupt-store test pinned the old refusal of an unknown schema at open.
Under C2a F2 that record is foreign: the store opens, lists it, and opening
still rewrites nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:52:08 +02:00
Meta Vens
05221dae74 C#: a record no registered type claims is carried untouched, and a write over it is refused
C2a F2, operator: "CultCache never destroys or relabels a record it doesn't
understand; keeps it byte-for-byte, refuses writes that would overwrite it."

- The registry's claim decision (Claimant) is split out of resolution. It
  never decodes or compares shapes, so a store classifies a record from its
  header. The last branch, "No local CultCache schema matches", now means
  foreign instead of a refused load.
- Both store kinds keep a foreign record out of the cache and list it in
  CacheBackingStore.ForeignRecords (key, schema id, catalog name, storedAt).
- Single-file store: a whole-view write lays back every foreign record the
  file holds now, with its catalog entry, and keeps a marked header while it
  carries one (it cannot see their ids). A staged write, a commit (onto the
  file or whole view) and a whole view over a key the file holds as foreign
  refuse with CultSchemaConflictException naming the key and id. A merge
  judgement never decodes a foreign record, and one that replaced a held
  record takes it away.
- Directory store: a foreign record's page is never read; a generation that
  would replace or remove one refuses before any page is written.
- Contracts: hard rejection no longer lists "no compatible local schema
  candidate"; the persistence format says what a foreign record is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:50:15 +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
92d9e13868 Test: a string-named store read is typed as a note in the assertion
The handle named by a bare schema id yields unknown; 3c9 test code read .body
off it and did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:20:54 +02:00
Meta Vens
9bdf387382 Test: identity defaults, lister dedup, filters by a listed id, store id order
Stryker survivors on 426c956a..fa266e58, each now a behaviour test:

- schemaIdentityOf's defaults (version "<name>.v1", compatible ids holding
  only the own id) are asserted for a name-only definition.
- A lister bound twice is one lister: an unowned id it lists still
  resolves to it rather than being refused as ambiguous.
- A snapshot filtered by an id a binding lists returns that binding's
  records, on the typed and raw paths.
- The ambiguity refusal's record key and message tail are asserted, and a
  raw snapshot record's payload encoding.
- A string-named store document reads the record under its id, and a
  definition's own id wins over an id it lists wherever each record sits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:15:53 +02:00
Meta Vens
b810810cf3 cultcache-ts: a record refusal names only a string key; pin link writes for R3
The five-slot refusal ran a non-string key or schema id through String(),
quoting stored bytes. A string is named; anything else is described by its
type.

A push through a live link, and a pushAll through a dangling one, replace
the link with a regular file. Tests named for R3 pin that until R3 decides
whether writes resolve or refuse links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:51 +02:00
Meta Vens
549b745ee6 cultcache-py: a record refusal names only a string key; pin link writes for R3
The five-slot refusal echoed a non-string key or schema id with repr, quoting
stored bytes. A string is named; anything else is described by its type.

A push through a live link replaces the link with a regular file. A test
named for R3 pins that until R3 decides whether writes resolve or refuse
links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 19:54:51 +02:00
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
fa266e5829 CultNet C#: a second type for one schema id is refused, typed
Soul F5. F7 (operator, 2026-09-30): "Schema aliasing sounds like a really
bad idea, I support retiring it." CultNetDocumentRegistry.Register wrote
both maps unconditionally, so the last binding for a schema id won.

- Register refuses a binding whose type already holds another schema id,
  or whose schema id another type holds, with the reference runtime's
  CultSchemaConflictException: the new binding's schema id, both document
  types, an empty record key. Nothing is bound on refusal. Binding a type
  to the schema id it holds again replaces the binding. This matches Rust
  (fc3f3ee5) and Kotlin.
- Tests: both claimant shapes in both orders with two bindings ahead of
  the holder (F6/M11), the first binding intact afterwards, and a same-type
  rebind replacing.

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