The legacy envelope-array decoder refuses a duplicate key as v1 does. The single-file writer
refuses (SchemaConflictError, C# Retyped wording) a put/push_all that would land a second type
under a key the store holds or the batch already names; file unchanged. Replace-by-key stays
for the same type. Retyping is remove-then-add.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tests/vectors/slot-nil was read by TS only. The Python reader accepts the same 15 rows
and refuses the same 2 as the C# reference and rewrites the reference's bytes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
push, push_all and delete hold <path>.lock (flock; LockFileEx over the whole file on
Windows, as cultcache-ts store-file.ts) across the read and the replace. The staged file
is <path>.tmp-<pid>-<uuid>, fsynced before os.replace, with a directory fsync after off
Windows; a failed write removes its staged file and leaves the store.
Tests fail when the rule breaks: a write waits while a flock holder (Rust's lock) and the
TypeScript runtime's withStoreWriteLock hold the lock, three processes lose no writes,
fsync precedes replace, staged names differ, a failed replace leaves no staged file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pyproject (and cultnet-py) already declare msgpack as required; the code treated it
as optional behind a cultcache-py[msgpack] extra that does not exist.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each test file carries the snippet between markers; the docs show it verbatim and a second test in the file fails if the README block drifts. Matrix cells for Rust and Python CultCache now link them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Owns finding c2a-public-breaks-not-in-changelog. Leaf changelogs gain v3 read and write and element ids carried; cultcache-rs gets its first changelog (the whole-store writer fix is not on main, so no 0.3.1 section).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Wrap and the Outcome arms gain id slots (unit arms become one-slot id records);
the Python unit-arm codec behaviour is kept on a separate unit union.
Co-Authored-By: Claude Sonnet 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>
leaves.py (string, boolean, bytes, int32, int64, float32, float64, any, list, array, nullable, lazy) held to the
numeric-leaves vectors. Removed: DatabaseEntryField.many, callable codecs and _resolve, the System.Object literal,
the present-nil default, the union's nil pass-through, interop.py's hand-written catalog. A float32 keeps its width
through _pack_tree. Nil is refused unless wrapped in nullable; a default fills only a slot past the payload's end.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tree walk counted only list, tuple and dict, so a codec whose tree used deque,
mappingproxy or another abc container was unbounded. Every Mapping, Sequence and Set
that is not text or bytes now counts. RecursionError from a user encode or decode,
and from the JSON decoder, reaches the caller as itself with its traceback; only
msgpack's StackError (raised by depth alone) is mapped to the depth refusal.
All commits build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Removes the equivalent continue in the walk (the stack only ever holds containers).
Pins the exact refusal text on the RecursionError and StackError paths, both sides,
and that the validator receives the written and the decoded value. Every commit builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DatabaseEntryField validates its key in one place (0..MAX_SLOT_KEY, no bool), so
database_entry_field, direct construction and field tuples share the rule. A record
shorter than its declared slots no longer refuses a missing slot: the slot is nil
and its default applies, as the C# reference reads it. define_union refuses True
and False as arm keys, each pinned alone. Every commit builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iterative container count over the decoded and the encoded tree (MAX_PAYLOAD_DEPTH,
CultCachePayloadDepthError); msgpack StackError and RecursionError map to the same
refusal. Union decode names an out-of-Int32, non-integer or nil-slots arm as the
reference does. Fixture arms Wrap, Floor, Ceiling; loop over bounds-outcomes.msgpack.
Pins the bool arm-key refusal's callers in the vector loop only; every commit builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
define_union and define_record_codec share the slot logic of define_database_entry_type
(one record codec); a field takes a codec, many, or a thunk for recursion. Unknown arm
keys are refused naming union and key, never the payload. All commits build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
`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>
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>
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>
A record now resolves to the document that owns its schema id, else one that lists it as
compatible, before the catalog name is tried; a schema renamed under a stable id opens, and an id
that names one document is not overruled by a name that names another. The name resolves a record
only when no local document has the id.
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, the cross-tier refusal and the rename, and
a cache whose document does not list the id a loaded record sits under.
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>
A read-back record's type is its schema's name and a supplied one's is the document type, so records of different tiers are never
compared; two supplied (or two arrived) records of different types under one id still refuse the write.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>