Hand mutant T4 (lead >= 0x90) survived; the empty-array case now carries a newer stamp after it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The stamp functions (Rust refuse_unread_format, TS refuseUnreadFormat) are the single owner of the stamp rule, accept v1 and v3, and are called only from the one reader every whole-store write runs under the lock. The fix's header peek (TS refuseUnreadStoreBytes, peekHeader) is deleted. Its tests that v3 is refused and that a stampless file is healed are dropped; the reversal is a Breaking line in the Rust and TS CHANGELOGs. The 0xff-after-stamp test now runs over v1 and v3.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both new tests fail at this commit; it builds. Vector copied byte-identical from C2a (blob 9fc9b64f).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Identity: C2a's StoredSchemaId/StoredVersion owns; main's PersistedSchemaId, PersistedIdentity and both MarkWritten are deleted. Stamp rule: ReadStore judges the stamp before the body. Flush over damage refuses (merge-r1-precedence). Mesh single-file writes route through SingleFileMessagePackBackingStore. TS: store-file.ts commits, C2a's header handling kept. Rust: C2a's readable_store_format in front of the grown decoder. Build not yet verified at this commit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Both directions: a TypeScript write waits on a C# or Rust holder; a C# or Rust writer
waits on a TypeScript holder; the lock wait bound names the lock path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C2a's writer, catalog and soft-push tests handed raw Uint8Array payloads to
the store. Main made the envelope payload an opaque DocumentPayload, so they
wrap with documentPayloadFromBytes and read back with documentPayloadBytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sixteen conflicted files resolved by the spec's rule: main owns payload
ownership, bounds and the Clock seam; C2a owns persistence semantics and exact
binding. MintStoredAt is internal static (previous, now); the cache and the
store pass their cache's Clock. Package versions are main's. The cultmesh-ts
payload-parse acceptance and its test are deleted under retire-schema-alias;
peer snapshot reads take the schema's own id, else an id the definition lists
as compatible. The held firstRecordAtKey fallback is removed: main's handles
take definitions only, so nothing could reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DocumentPayload is an opaque object type over the same runtime Uint8Array, keyed by a string
property, and types CultCacheEnvelope.payload, CultNetRawDocumentRecord.payload and
CultMeshRealtimeFrame.payload. decodeDocumentPayload(definition, payload) and
encodeDocumentPayload(definition, value) are the only reader and writer; documentPayloadFromBytes
and documentPayloadBytes are the two named escapes at the edges where bytes enter and leave.
resolveDocumentFormatter leaves the package index. The cache, cultnet replication, the mesh read
paths, the provider command (now a CultCache document), and the browser's decodeCultNetPayload
(definition required, cultcache-ts/document entry) all go through the owner.
Compile-fail fixtures per payload-carrying package pin the type with expect-error lines.
check-typescript-tests-wired.mjs fails when a test file is run by no script; cultcache-ts now
runs dist-test/test/*.test.js, and the portability workflow runs the check and the cache suite.
Builds in every package (tsc, src and tests); the suites run once, at the end of the pass.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The C# peer gains edit-in-place (payload and override through the public snapshot
API; collection and nested object through the held document), and the four-peer
test has Rust and Python read each edit.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The writer refuses a write that would change which type an existing record resolves to, unless
the entries of both the existing records and the written ones own the id (a rename; the registered
descriptor wins). Tests pin the arrived-overwrites-registered guard in both id orders, the
content-hash tie-break against the compatible-list order, sorted id processing and the cross-tier
refusal; a cross-runtime test has TS write a schema renamed under a stable id and TS, Rust and
Python open it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two entries of one tier that share an own id are refused when the id is carried, so a refusal at dedupe could never fire.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every writer (C# single-file and directory, TypeScript, Python; Rust already did) chooses, for each schema id a record carries, one
entry that publishes it: the entry that owns the id (a registered descriptor, else an arrived entry), else one that lists it as
compatible. A registered descriptor wins over an arrived entry with the same own id. Entries are written as chosen, with no union of
compatible ids and no dependence on arrival order; an entry no record needs is not written. Two entries of one tier that share an own id
and disagree on the schema name, records of different types under one id, or a record no chosen entry publishes refuse the write with a
typed error (CultSchemaConflictException, SchemaConflictError) naming the id, the names and a record key, and the store is left as it was.
CultSchemaCatalogEntry.MergeById and the Python and TypeScript merge paths are gone. A cache-held record unchanged since the file was read
is rewritten under its registered id (C# single-file and directory), so an entry that lists an older id is not needed to publish it.
Changelogs for cultnet-py (removed export) and cultcache-py.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cultnet-py's resolve_document_and_schema_id_for_raw_record is replaced by resolve_document_for_raw_record, which resolves the
document only; cultmesh-py keys previous values by the local schema id, so a snapshot under a compatible id or the schema name
reports an update with its previous value. Writers (C#, TypeScript, Python) merge same-id entries only when their schema names match;
another schema's entry is dropped or the record gets a default entry under its own id, and a record's own id is claimed before an
entry that lists it as compatible. The C# single-file writer prefers the entry being written now, as the directory writer does, so
name, content hash and members come from the registered schema and the compatible ids are the union. Vector:
compatible-before-owner-v3. A cultnet-py test's schema path no longer depends on the working directory.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Entries that share a schema id are one schema seen by different writers, so the writer lists every id any of them lists, in any
order: C# single-file and directory (CultSchemaCatalogEntry.MergeById), TypeScript and Python. Rust synthesises one entry per id and
has no such merge. An id that one entry owns and another lists as compatible names the owner in Rust, TypeScript, Python and the TS
inspector, as C# already did (own-id-over-compatible-v3). Also: a directory manifest in an unreadable format is the typed refusal
naming it, cultnet-py applies and reports a record under the local schema id, and Rust's error prints its cause once.
Vectors for the Mesh layout discriminator: a nil and an integer at catalog slot 5.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
cultnet-py resolves a raw record whose schema id no document lists by the schema version its payload opens with, and three
cultmesh-py tests (foreign UI-generated ids) depend on it, so that resolution is restored pending a ruling. The compatible-id
replication test goes through the real apply_raw_document_record and the reopened store.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
putEnvelope and admission stamp the registered schema's id, and the store writer publishes every record's id (the entry the
envelope carries when it lists that id, else a default), so a replicated record under a compatible id no longer leaves a store
nothing can open. cultnet-ts stops preserving the incoming id. The inspector refuses a store of the wrong slot count and a record
whose schema the catalog does not publish, instead of recovering one from the payload.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rust, TypeScript and Python each had a test that a record missing its catalog entry is recovered from its payload; each now asserts
the store is unreadable.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pushAll, push and delete read the file through the one store reader that pullAll uses, so a store holding a variant or a bad
body is not replaced by a header-only check. A store with fewer or more than three slots is refused. The readability test
walks every vector in the folder, asserts the error type and cause, the header a replaced file carries and what it holds.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rust's read path let a legacy envelope array with trailing bytes through when the
whole-array check was loosened, and no vector caught it. Add the vector; every
runtime refuses it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rewrite replaces a file only when the runtime's own reader opens it. C#
drops ReadStoreHeader (a second, header-only verdict that let a flush replace
files the reader refuses) and asks DeserializeSnapshot from open, flush and
commit; the reader now requires exactly one array, so C# refuses bytes after
the store on read as TS and Python do. A zero-byte file is an empty store for
all three paths. CultMesh.WriteSingleFileDocument reads the file it replaces
through the Mesh reader before writing. Rust reads and rewrites through one
classification (trailing bytes refused on read; a non-store first slot must be a
legacy envelope array the reader accepts). TypeScript checks a legacy array with
pullAll's own decoder before replacing it. Python already conformed.
Shared byte vectors and per-runtime verdicts live in
tests/vectors/document-variants-c2a/readability; every runtime's test reads
them. Holds gains second-dictionary-value and second-list-item cases. The
persistence format contract now marks v3 by content and states the readability
rule.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A rewrite read the header of the file it replaces but not whether the file was
a store: C# ReadStoreHeader skipped one value and ignored bytes after it, and
took a non-array value or an array whose first slot is a string that is not a
store header for a headerless legacy file (so did the C# Mesh raw write, which
uses it); TypeScript's pushAll took any non-array value or non-header first slot
for v1; Rust ignored bytes after the array.
The rule, one per runtime: a file is a store on rewrite only when it is exactly
one complete MessagePack array. Bytes after it, a value that is not an array,
and a first slot that is a string but not a header this runtime reads are
refused, and the file is left untouched. An empty file and a legacy envelope
array (first slot not a string, empty included) carry no header and are still
rewritten.
- C#: ReadStoreHeader refuses non-array values, trailing bytes and a non-header
first string, with a NotSupportedException naming which.
- TypeScript: storeHeader (store-format.ts) decides the rewrite header;
@msgpack/msgpack's decode already refuses trailing bytes.
- Rust: store_header(bytes, whole) walks with a cursor; the rewrite path asks
for the whole file, the read paths keep reading as before.
- Python: push_all already reads the whole file (unpackb refuses trailing
bytes, the legacy decoder refuses anything but a list of maps); pinned by a
test.
Tests in each runtime run the same file matrix (truncated, trailing, scalar,
map, string first; empty, empty array, legacy), plus a C# unconditional commit
over a store with trailing bytes and the C# Mesh raw write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The behaviour was already a refusal that leaves the file untouched; now a test fails if a rewrite
ever reads the disk header less strictly than a load does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
push_all overwrote a v9, variant-v2 or variant store with a v1 file, dropping its variants; TypeScript and Python
refuse. The rewrite header now comes from the file's own prefix and shares one readable-format rule with the
decoder. TypeScript pullAll resets its header when the file is gone, empty or legacy, as Python does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TS pushAll wrote the header its instance last read (v1 when fresh) without reading the
file. It now reads the on-disk header first, as push and delete do through pullAll. Python
resets its remembered header when the file is gone or empty, so the disk decides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A store that can hold element ids declares cultcache.store.v3. These runtimes pass payload
bytes through, so they read it and write back the header they found; they do not decide
when a document carries ids. Vector: tests/vectors/document-variants-c2a/v3-base.msgpack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The C# cultcache interop peer gains an object-list document (a deck of marks).
C# writes it with cache-minted ids; TypeScript, Python and Rust read the ids as
ordinary members, and TypeScript-written ids survive a C# read.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The store reader and the inspector share one header and record-slot rule;
an unknown cultcache.store.* header no longer falls through to the legacy
envelope decoder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The AsyncLocalStorage guard refused a store's change handler or timer
writing back to its cache without the store waiting for it, which only
queues, and still missed a wait that escapes the async context. The serial
queue and its own-promise chain stay.
The storePushThatAwaitsItsOwnCacheIsRefusedInsteadOfHanging test is
replaced by storeChangeHandlerWritingBackQueuesAfterTheOperation: a store
push emits an event whose handler puts without being awaited; both writes
land in order and store and cache agree.
The contract's Locking section states the rule for all runtimes: a store
call must not wait for a write to its own cache; unawaited write-backs
queue. Python refuses a same-thread re-entrant mutation; no runtime detects
a wait on another thread or async context. The globals row states the
two-step legacy retirement with compensation, and that a crash or double
failure leaving both records is refused at the next pull.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>