Commit graph

1,559 commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
b36b4706fa Parity scope: Rust and Kotlin snapshot hydration is by schema id, not aliasing
F7 retires schema aliasing (one type presented as another for the same
schema). The Rust and Kotlin rows called their snapshot hydration "same-schema
alias" hydration. Both decode a raw record by its schema id into the one type
the receiving runtime registered for that schema; neither has a handle
aliasing API. The rows now say that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:59:32 +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
98999997ce CultCache: 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." C# now holds one type per schema, as TypeScript,
Python and Rust already do.

- Delete IsExactWireAlias and the branch that registered an identical second
  type by CLR type only. A second type with the same schema name and version
  is refused with CultSchemaConflictException naming both types and the
  claimed id, whether it is identical, declares other compatible ids, or has
  other members. The untyped DuplicateSchemaRegistration refusal and the
  separate SchemaIdClaimedTwice path collapse into that one refusal.
- Owner-beats-lister (batch 10) is unchanged: one type owns an id, others may
  list it as compatible.
- The per-name version index stays; it serves v1/v2 of one schema name, not
  aliasing.
- Tests: the alias-registration tests are replaced by one refusal test over
  identical, re-declared and re-shaped second types, in both registration
  orders.
- cultcache-store-composition.md drops the AsSchemaAlias stale-protection note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:15:57 +02:00
Meta Vens
1e0455028c CultMesh: AsSchemaAlias is gone; a catalog finds a handle by its type
F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." One CLR type owns a schema, so a handle never needs
converting to another type for the same schema.

- Delete AsSchemaAlias from the document and collection handles and their
  interfaces, with the MessagePack round-trip converters only it used.
- The document and collection catalogs look a typed handle up by its type.
  The schema-name/version fallback existed only to find a handle of another
  type and alias it.
- Tests: the MeshNoteAliasDocument fixture is deleted. Tests that used it
  for publication, sync, snapshot and prediction paths now use
  MeshNoteDocument; the one test that only checked alias refusal is deleted.
  Catalog tests check a type with no handle is not found.
- public-api.md stops documenting aliasing; a stale comment in
  ReadDocumentFromSnapshotResponse that cited the fixture is trimmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:15:48 +02:00
Meta Vens
5b86bc9faf Test: a flush storing a record under a new id gives it a later storedAt when the file no longer holds it
Defends the id half of the whole-view restamp rule on its own: the record C# loaded
under v1's id is gone from the file when a v2 cache flushes it back, so there are
no stored bytes to compare, and only the changed id says it is a new store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:32:57 +02:00
Meta Vens
2204ce23e9 TypeScript: a definition listing an id registers beside the one owning it; registration refuses typed
Owner beats lister, as in C# and Python. The cache kept one definition per id,
own or compatible, and refused a second with a plain Error, so a v1 definition
could not stay registered beside a v2 declaring v1's id (Soul F8, cult-cache.ts
registration). Now a definition owns its schema id and any number may list it:
a record resolves to the owner, else to the one lister. Several listers and no
owner name no single definition, and the load is refused with
SchemaConflictError naming the id, the record key and the listers.

Every registration refusal is now SchemaConflictError (Soul F10): a second owner
of one id, a second definition with one schema name, and a different definition
for one type. Each names the new definition's id and both schema names.
SchemaConflictError takes an optional message for these uses.

Tests: an owner and a lister register in both orders and each reads its own
records, and a lister alone reads the owner's id; two listers register in both
orders, a record under their id is refused typed without its owner and is the
owner's with it, registered before or after them; each of the three refusals is
a SchemaConflictError in both registration orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:31:31 +02:00
Meta Vens
84246b685a Python: a document listing an id registers beside the one owning it; registration refuses typed
Owner beats lister, as in C#. A document owns its schema id; others may list it
as compatible beside its owner, in either registration order (batch 9 refused
any overlap, which forbade keeping v1 beside a v2 that declares v1's id; Soul
F8). A record resolves to the document owning its id, else to the one document
listing it. Several listers and no owner name no single document, and the load
is refused with SchemaConflictError naming the id, the record key and the
listers: refusing at registration would depend on whether the owner had
registered yet.

Every registration refusal is now SchemaConflictError (Soul F10): a second owner
of one schema id, a second document with one schema name, and a second
definition of one document type. Each names the new document's id and both
schema names and document types. Before, the last two raised CultCacheError.

Tests: an owner and a lister register in both orders and each reads its own
records, and a lister alone reads the owner's id; two listers register in both
orders, a record under their id is refused typed without its owner and is the
owner's with it, registered before or after them; two owners, one schema name
and one document type twice are each refused as a schema conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:31:25 +02:00
Meta Vens
9629aa119b C#: a type listing an id registers beside the type that owns it; the owner wins
Batch 9 refused any overlap between two types' ids, which forbade the migration
CompatibleSchemaIds exists for: v2 declares v1's id while v1's class stays
registered (Soul F8). The rule is now owner beats lister. A type owns its own
schema id; any number of types may list an id as compatible beside its owner,
registering in either order. A record under an id resolves to its owner, else to
the one type listing it. An id no type owns and several list names no single
type, so a record under it is refused, typed (CultSchemaConflictException naming
the id and the listers), where it resolves: refusing at registration would
depend on whether the owner had registered yet.

A second owner of one id is still refused at registration, typed. A schema id
hashes the schema's name and version, so the only second owner is a type of the
same name and version, which the name-and-version check already meets; the
separate own-id lookup could never fire and is gone.

Tests: an owner and a lister register in both orders, the owner's id resolves to
the owner, the lister's own id to the lister, and without the owner to the
lister; two listers register in both orders, a record under their id is refused
typed with no owner and resolves to the owner with one, registered before or
after them; a cache with v1 beside v2 reads each version's records as its own
type, in both registration orders. The emitted-fixture note says why a fixture
is emitted now that v1/v2 pairs no longer need hiding from the shared registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:26:06 +02:00
Meta Vens
188d4ad88d C#: a whole-view write that stores a record differently mints it a later storedAt
A condition names a record by (stored schema id, storedAt), and that name must
say which bytes are stored. A single-file whole-view write (plain flush, or an
unconditional commit) re-encoded every record under its own type's id with the
storedAt it loaded, so the pair could name two byte strings: v1 shed a member
keeping storedAt, a second v2 cache restored v2's id keeping it again, and a v2
cache holding the original bytes committed its condition (Soul F6, A-B-A).

Now such a write stores a record under a later storedAt whenever it stores it
differently: under another id than it was loaded under, or with other bytes than
the file holds at that storedAt (a record written with the ids it minted at
load, for one). A record stored exactly as the file holds it keeps its storedAt.
The store's entries and the cache's take the id and storedAt the write gave them.

Tests: the A-B-A through commits and through plain flushes, with Expect and
ExpectUnchanged; a flush that re-encodes a record restamps the cache that
flushed (a condition holds straight after, SM12); a record stored unchanged keeps
its storedAt through commits and flushes; persisting load-minted ids gives a
later storedAt. The rolling-deploy test asserted v1's rewrite kept storedAt; it
now asserts a later one, since that rewrite is a new store of the record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:25:22 +02:00
Meta Vens
fdb8a60af9 Test: conditions on records loaded under an old id, before a rewrite and after a directory pull
A plain record and a variant loaded under a declared old id pass Expect and
ExpectUnchanged while the file is unchanged, and again after a whole-view commit
rewrites them (the variant case pins the id surviving resolution). A directory
store pull rereads a record whose manifest id alone changed, so a condition on
it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:48:40 +02:00
Meta Vens
5607d9703d C#: the directory store's loaded id is its manifest's; the variant fixture names itself
A directory-store record's identity on disk is its manifest entry, which is what
a commit condition compares; the page holds its own copy of the id. A load now
takes StoredSchemaId from the manifest, so a manifest rewritten under an older
id (AConditionOnARecordUnderADeclaredOldIdHoldsInTheDirectoryStore) is the id
the condition expects. Canonicalisation had hidden the page and manifest
disagreeing in that fixture.

The whole-view test's variant overrides its name: a variant may not share its
base's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:52:15 +02:00
Meta Vens
abb922cd57 Python: one registered document carries each schema id; a second claimant is refused typed
register_document_type refuses a document whose own or compatible ids overlap
another registered document's, raising SchemaConflictError (empty record key,
both schema names, a message naming both document types), as TypeScript does.
Before, the record resolved to whichever document the dict held first (Soul F4).
With one carrier per id, resolution by id is a single pass. SchemaConflictError
takes an optional message for this use.

The test that pinned owner-over-lister resolution inside one cache becomes a
refusal test in both orders; shared compatible ids and a shared own id (neither
declaring its own) are refused in both orders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +02:00
Meta Vens
4182c2502b C#: one registered type carries each schema id; a second claimant is refused typed
Registration refuses a type any of whose ids (its own, or one it declares
compatible) another registered type already owns or declares, with
CultSchemaConflictException naming the id, both schema names and both CLR types.
TypeScript already refuses this (cult-cache.ts); before, C# kept whichever type
registered first (Soul F4). An exact wire alias must now also declare the same
compatible ids: a type differing only in its declaration had it dropped when it
registered second (H4), and is refused the same way.

Exact aliases that agree in every respect still register by type, and a record
under their shared id still resolves to the first registered (H3). Removing that
would delete CultMesh's same-schema aliases; it is left for an operator ruling.

Tests: both registration orders for a shared compatible id, an owned id declared
by another type, and a same-schema type with a different declaration; an alias
with the same declaration still registers. The registry tests use the shared
EmittedDocumentTypes helper in place of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:52 +02:00
Meta Vens
5c8af219f9 C#: a commit condition compares the id a record was loaded under with the file's, exactly
Each stored document carries StoredSchemaId: the id its record sits under in its
store, the one it was loaded under until a write puts it under its type's own id.
Expect, ExpectUnchanged, the reload and merge identity checks compare
(StoredSchemaId, storedAt) with the store's bytes exactly. CanonicalSchemaId is
deleted: canonicalising the disk id let an older version's whole-view rewrite
(same storedAt, restamped to its own id, a member shed) pass a newer cache's
condition (Soul F1), and failed every condition on a record resolved by name or
through a file entry listing the local id (F3). A whole-view write marks every
record it wrote as stored under its type's id, in the store and the cache.

Tests: Soul's rolling-deploy probe as AWholeViewRewriteByAnOlderVersionFails...
(Mismatch for Expect and ExpectUnchanged, nothing derived lands, a pull shows the
rewrite and the retry commits); F3's H1, H1b, H2 pass Expect and ExpectUnchanged
when nothing changed; conditions after a whole-view commit hold for a plain
record and a variant. BareDeck is emitted at run time (EmittedDocumentTypes) so
the shared registry never sees it beside DeclaringDeck; the CanonicalSchemaId
tests and their Claimant fixtures go with the function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:40 +02:00
Meta Vens
c99f29c309 Test: the selection guard probe passes the descriptor's new compatible-ids argument
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:48:38 +02:00
Meta Vens
b295bf1688 C#: pin the directory store's conditions, drop the merge-judging canonicalisation
A condition on a record under a declared old id is tested against the directory manifest too, and
a duplicate declared id is listed once. The merge-judging comparison keeps its plain schema-id
equality: a record it re-reads is only judged again, and no test reaches a difference.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:39:11 +02:00
Meta Vens
0cef517d83 C#: a schema name that drifted no longer refuses a record resolved by its schema id
The contract says the name a catalog entry carries is metadata and a schema renamed under a
stable id opens in every runtime that holds the id. The C# shape comparison refused any name
drift, so a record under a declared old id (the entry carrying another name and a different
content hash) or under a renamed stable id could not open. The comparison keeps the slot and
type checks; a record resolved by name has the same name by construction.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:36:43 +02:00
Meta Vens
da0315e69e Test: TypeScript writes a record under an older schema id, C# declares it and rewrites it
The C# interop note declares a compatible id and the peer gains a rewrite mode; the TypeScript
cross-runtime test has TS write a record under that id and another schema name, C# open and
rewrite it, and C#, TypeScript, Rust and Python open the result.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:32:47 +02:00
Meta Vens
d589e61f67 C#: a blank declared compatible schema id is dropped, not refused
A type that failed to build a descriptor poisoned the shared registry's discovery of every
loaded document. A blank id is now filtered like the type's own id, and the test declares one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:31:28 +02:00