Commit graph CultLib/packages/cultcache-ts
Author SHA1 Message Date
Meta Vens
47c7111897 Delete the TypeScript re-entry guard; state the store-wait rule once
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>
2026-09-14 12:43:46 +02:00
Meta Vens
4158a38a34 Refuse re-entrant TS cache calls and adopt legacy-key globals on load
The serial chain now advances on its own promise and returns the
operation's promise untouched, so an unawaited failing put is an
unhandled rejection again instead of being swallowed by the chain's
catch.

A queued call started from inside a store call, decoder, accessor or
updater of the same cache's running operation throws at entry instead of
waiting on itself. Detection is an AsyncLocalStorage list of running
operations, so independent concurrent callers are still queued.

A pull adopts a global persisted under a key other than __global__ as
that type's global without writing. The first write or delete of it
removes the legacy record in the same call; a failed removal takes the
new record back out. Two globals of one type on disk, under any keys,
are still refused.

concurrentGlobalPutsCannotBothReachDisk now pauses inside push and fails
when the chain is replaced by Promise.resolve().then(operation).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:28:07 +02:00
Meta Vens
286435d861 Serialize TypeScript CultCache mutations and validate writes whole
A per-cache promise chain now runs addBackingStore, pullAllBackingStores,
registration and every write and delete one at a time, so validation, the
store call and applying to the cache cannot interleave with another mutation.
Those methods return promises; the builder configures its unreleased cache
synchronously. Reads stay synchronous.

put and putEnvelope resolve the home, check the global key and run the name and
index accessors before the store is touched; applying afterwards has no
fallible step. Each held record keeps the lookup values it produced at
admission, so overwrite and delete run no accessor. A global is only stored
under __global__: any other key is refused, and replacing the global writes
__global__ with no delete-then-push. registerDocumentType, registerNameLookup
and registerIndex check schema-id and schema-name conflicts and derive every
lookup before installing anything.

cultmesh-ts awaits registerDocumentType in createNode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:57:01 +02:00
Meta Vens
446122daef Build the whole next state before a pull swaps it in
A pull is all-or-nothing, but TS cleared the cache and then ran user name and
index accessors while re-applying entries, and Python swapped values in and
then rebuilt lookups. An accessor that threw left the cache partly loaded.

TS: the hydrated maps (entries, type entry ids, name and index lookups, global
keys) are one HydratedState. The pull builds a fresh one, running every check
and accessor, and replaces #hydrated in one assignment at the end. The
reset-then-apply sequence, #resetHydratedState, the staged global-key map and
the three per-map getOrCreate helpers are deleted; apply/remove take the
target state. The duplicate-global check now lives only in apply. Name and
index lookup rebuilds build a local map before assigning.

Python: the pull derives names and indexes into local dicts and assigns
values, envelopes, names and indexes together; _rebuild_indexes delegates to
the same pure _derive_lookups.

Rust already stages into a local map and assigns self.entries last, with no
derived lookups; unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 01:03:30 +02:00
Meta Vens
1f442f3345 Refuse duplicate global records while staging a pull
A load is all-or-nothing. TypeScript checked for a second record of a
global type inside #applyHydratedEntry, which runs after
#resetHydratedState, so a refused pull left the cache cleared and partly
reloaded. The check now runs in the staging pass beside the home check,
before anything is cleared; the apply step's pull-only branch is gone.

Python already validated before swapping, but its check only fired when
the __global__ record arrived second, so a store holding __global__ then
another key of a global type loaded both. Any second record of a global
type is now refused regardless of order.

Rust has no global document types and replaces its entry map only after
staging; it had neither flaw.

pullWithDuplicateGlobalLeavesCacheUnchanged (TS) and
test_pull_with_duplicate_global_leaves_cache_unchanged (Python) fail on
9ab1706 and pass here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:57:09 +02:00
Meta Vens
6b117def62 Keep cultcache-ts record homes stable and refuse strays at load
Conform cultcache-ts to the store-composition contract, following C#:

- addBackingStore refuses, attaching nothing, when the new store would
  move the home of any record type the cache already holds (for example a
  store routed to "settings" attached after settings:app landed in the
  generic store).
- pullAllBackingStores reads, decodes and home-checks every record before
  replacing the cache's view. A record delivered by a store that is not
  its home throws naming the key and both stores; nothing is admitted and
  no store is written.
- A cache with zero stores is in memory: put, putEnvelope and delete no
  longer throw. Once any store is attached, a type with no home still
  throws.

Tests for each rule fail on 6bb41f3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:50:07 +02:00
Meta Vens
8dd5a45378 Give every cultcache-ts document type one home store
Cut 5 (TypeScript). The cache no longer mirrors: StoreRoute and its mirrors
list are gone, #resolveRoute returns the single home store (the store that
claims the type, else the generic store), and put, putEnvelope and delete write
exactly one store.

addBackingStore now refuses a second generic store and a type already claimed
by another store, attaching nothing when it refuses, matching C#. No bytes
change; nothing in CultLib attached more than one store.

Tests: rejects a second generic backing store, rejects a type registered to
two stores, routes each type to its home store. The writer-by-reader interop
test now runs the C# write-routed mode and checks that catalog.cc and run.cc
are each a complete v1 snapshot holding only their own record, and that every
reader (TS, Rust, C#, Python) reads the routed catalog store.

Version 0.13.5 -> 0.14.0; cultnet-ts and cultmesh-ts depend on ^0.14.0 so the
workspace link still satisfies their range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:37:31 +02:00
Meta Vens
830a5d718f Make the Eve onboarding checkpoint run against the contract that exists
The portability workflow has been red since 2026-08-22. Three things,
each hidden behind the one before it:

- Eve started refusing to build a command intent for a surface that has
  not advertised a command boundary and receipt schema (Eve 4338a42,
  2026-08-22). The two-runtime sample never advertised one. It now passes
  a provider advertisement with worldInteraction for its surface, which is
  the declaration the lowering was right to demand.
- The sample and the package-closure script still imported "cultcache-ts";
  the package became @gamecult/cultcache-ts on 2026-09-04 (8cb3b72) and
  CI never got far enough to say so. Two READMEs carried the same import.
- The job pinned Eve to codex/eve-browser-host-isolation, a branch that was
  never merged and stopped moving on 2026-08-25. The sample is written
  against Eve main and passes there; the pin now says so.

The interop workflow's Python step ran one test file by path; it now
discovers the three split suites.

  scripts/verify-eve-two-runtime-sample.ps1 -EveRoot F:\Projects\Eve
  (Eve main ccc3ffe) -> browserCount 1, headlessCount 1, restartCount 1

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:55:46 +02:00
Meta Vens
95493862bd Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py
One distribution named cultcache-py carried three libraries. Rust and
TypeScript layer them as cultcache < cultnet < cultmesh; Python now does
the same, one package per layer, module names unchanged so every
`from cultmesh_py import ...` keeps working.

Two imports ran against that layering and are resolved by moving code to
where the dependency direction says it lives, not by adding a shim:

- cultnet_py.interop_peer imported the Verse/peer catalog, authority
  lease, stream, and simulation-fact contracts from cultmesh_py. They are
  wire contracts a bare peer has to speak without owning a node, which is
  why cultnet-rs and cultnet-ts keep them in the net layer. They now live
  in cultnet_py.cultmesh_contracts; cultmesh_py re-exports them.
- cultcache_py.benchmark imported cultnet_py. It measures framed CultNet
  parse beside cache paths, so it and compare_csharp move to cultnet_py.
  verify checks all three packages and moves to cultmesh_py.

The single 6,693-line test module is split by what each test touches: a
test using any cultmesh_py name lands in cultmesh-py, else cultnet_py in
cultnet-py, else cultcache-py. Each suite runs with only its layer and the
layers below on the path, so a test that reaches up fails to import
rather than quietly binding the packages back together. 11 / 53 / 78 =
the same 142.

Workflows install all three; the publish job is one matrix over the
three packages keyed by tag prefix. The TS test harnesses put all three
src directories on PYTHONPATH. cultcache-py bumps to 0.2.0 because it no
longer contains cultnet_py or cultmesh_py; the new packages start at
0.1.0. Nothing was ever published, so no PyPI consumer moves.

  cultcache-py  11 tests, path: cultcache-py/src
  cultnet-py    53 tests, path: + cultnet-py/src
  cultmesh-py   78 tests, path: + cultmesh-py/src
  python -m cultmesh_py.verify -> ok, all five checks
  cultnet_py.interop_peer imports with cultmesh_py absent

Not run here: the cultnet-ts interop ring and cultcache-ts store matrix
need dotnet/kotlin peers; their PYTHONPATH change is four lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:10:47 +02:00
Meta Vens
b304df5e47 Promote the swarm document catalog into cultcache-ts
Odin carried defineOdinDocuments: 45 CultCache document-type definitions for
documents that cross service boundaries. Eve surfaces and provider
advertisements, Ghostlight, Heimdall, Idunn, Muninn, Sleipnir, Aetheria,
VoidBot, Stonks. Almost none of it is Odin. It lived there because Odin was
written first.

That was tolerable while Odin was the only consumer of consequence. It stops
being tolerable when Hermodr moves to its own repository, because the choice
then is a published contract or two definitions of one wire format, and the
second is the failure the Hermodr extraction exists to remove.

The catalog is now defineSwarmDocuments here, beside defineDocumentType, which
is what builds it. defineDocumentType stays injected rather than imported: that
is how it worked in Odin, it keeps the catalog clear of the generic signature in
./document, and it lets a caller supply another runtime.

Ported as a mechanical transform rather than retyped, then verified rather than
assumed: serialising all 45 definitions from both implementations against the
same probe gives byte-identical output and identical key sets. The 43 callbacks
that gained annotations are named DocumentValue rather than bare any, because
these definitions predate typing and read fields defensively, and pretending
otherwise would be inventing types nobody has verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 19:40:37 +02:00
Meta Vens
8cb3b7282b Publish the non-Rust packages and make the licensing uniform
Rust consumers pin a CultLib revision through cargo git dependencies, which is
how Ghostlight, Epiphany, CodexConnector and Odin already consume the core
stack. npm and pip cannot take a package from a subdirectory of a git repo at
all, which is why the TypeScript and Python consumers vendored forks instead:
there was nothing else to depend on. This gives them a versioned artifact.

Tagging cultcache-ts-v* or cultcache-py-v* builds, tests and publishes. The tag
version must match the manifest version or the workflow refuses rather than
shipping a mislabelled artifact. PyPI uses trusted publishing, so no long-lived
token is stored; npm needs an NPM_TOKEN secret.

cultcache-ts is now @gamecult/cultcache-ts. The scope is not cosmetic: it makes
the package unambiguously org-owned on a public registry, and it is what lets
CultLib hold the name rather than racing for an unscoped one. CultLib's own
cultmesh-ts and cultnet-ts and their imports move with it; all three packages
typecheck and cultcache-ts passes 12 of 13 tests, the exception being the
cross-runtime store test that shells out to Python, which cannot run on this
machine because its Python has no msgpack.

Licensing was inconsistent with the repo. cultcache-py declared itself "Private
GameCult infrastructure" and cultcache-rs and cultmesh-rs declared nothing,
inside a repo whose LICENSE is MIT and whose every other package says MIT. All
three now say MIT. That was load-bearing here: a package that calls itself
private is not one to push to a public registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:41:44 +02:00
Meta Vens
6528f8ea2f Make TypeScript packages release-packable 2026-07-13 23:47:38 +01:00
Meta Vens
8758a65e8e Add single-file document handles 2026-06-27 15:46:25 +01:00
Meta Vens
a0b31b8f2a Recover schema-stamped cache records across runtimes 2026-06-27 14:34:53 +01:00
Meta Vens
a1ef58cb2d Add Python CultCache wire parity package 2026-06-13 20:35:58 +01:00
Meta Vens
3755642716 Keep Huginn runtime out of CultCache package 2026-06-05 12:23:58 +01:00
Meta Vens
5c69bfc581 Bring language packages into CultLib monorepo 2026-05-30 00:39:39 +01:00