Repository navigation
0.19.0
Fixed
- Eventlog-backed synchronous commands reuse one verified tenant read during each batch's preflight, keeping multi-action revalidation within its caller deadline without changing guarded append, fresh recovery, receipts or durable record bytes.
service/1adjacent unions admit a variant object's nestedvalueorcontentmember. The outer tag/payload pair, exact retained bytes, and malformed-value refusals are unchanged.- Legacy Eventlog import binds the acquisition source identity into each persisted anchor, including boundaries with no envelopes. Same-source retries recover after reopen; identical histories from another source conflict. New source-bound anchor blobs use version 2; existing version 1 anchors remain readable without an inferred source identity.
Added
-
AsyncImportedAnchorWriter::import_anchorsestablishes a batch of imported boundaries in one atomic append group, from one capture and one post-capture, instead of the two complete tenant captures each singularimport_anchortakes. A capture re-verifies every blob digest of the whole authority, so importing a store cost time proportional to the square of its size; on a copy of the real ESS planning store oneapplyis 7,810 imports att(k) = 115 + 0.992·kms. The anchors, blob keys and receipts a batch writes are byte-identical to the ones the same histories written one at a time write, into the same subject streams, and a batch commits completely or not at all. A subject named twice in one batch settles the second mention as the exact replay two singular calls settle it as; two mentions carrying different anchors are refused before any provider call, as is a record identity the destination already holds or that two members share. Those refusals are made from the batch's own capture, before any blob is uploaded, on every provider. On a provider that binds a group's blobs inside the group's transaction — of the three shipped here, the File provider — the batch also pays one durability barrier for the whole batch instead of one per blob, and any refusal leaves no bound blob behind. A provider that does not implement that refuses it outright, having written nothing, and the batch then takes the same slow path the singular import takes: every blob on its own, then the same guarded group, with the same bytes, keys and receipts and only the single barrier lost. A retry that carries its batch is admitted a second time, so the destination guard now refuses an occupied record or subject only after checking which anchor occupies it — a retry of a batch's own commit settles instead of being refused into a second append. A batch refuses before it writes when committing it would put the destination past theCaptureLimitsits own handle reads it back with, asAsyncStoreError::BatchExceedsReadBounds, so a caller can divide the work to bounds it chose rather than discover them afterwards. That variant is a breaking addition for exhaustive matchers:AsyncStoreErroris not#[non_exhaustive], so in Rust's terms adding to it is a semver-major change, and amatchover it without a wildcard arm stops compiling. No consumer names the type today — measured across the aep, aep-service, atlas and bench checkouts — which is why it was taken rather than deferred, and is a fact about today rather than a guarantee. When a batch is blocked by a subject another writer holds, the refusal names that subject and the revision it is actually at. The singular entry points are unchanged.EventlogRecordedStore::calls()reports the captures a handle has taken, so a caller can assert that fixed cost. -
Eventlog-backed File, SQLite and PostgreSQL facades preserve complete recorded receipts, retries, observations, queries and atomic groups behind explicit open/provision authority. The retained legacy stores gain read-only typed acquisition for explicit out-of-place import; the opt-in CLI Eventlog feature selects its File facade with a closed authority-and-bounds document.
-
create_derivedanddecide_create_derivedlet Entity Runtime select a creation outcome and derive the storage address from that outcome's validated logical identity. Existing caller-supplied creation entry points retain their behavior and bytes; both paths produce the same decision, record, replay and original request when their final address agrees. A derived creation explicitly refuses a circular pre-address$idread. -
service/3operations can request typed Set, Preserve and optional Remove actions after the runtime selects and validates the loaded outcome. Events, responses and replay use the same resulting values; exact retries retain the actions underer.record/4ander.request/4. Earlier service formats and record bytes keep their existing behavior. -
Service bindings can decide input-only refusing branches before loading a subject and continue subject-dependent work through an opaque, identity-bound continuation.
service/2definitions can copy one typed optional argument into creation state, event and response members while preserving absence through replay and retry underer.record/3ander.request/3. -
A definition may now opt in to a second set of document rules with
semantics: service/1. Under them a creation and an operation carry named outcomes — ordered branches, each with its own guard, effect,set,emits, declared response and optional refusal — instead of, or beside, the transitions they carry today. The kernel selects the branch; a caller never names one. New entry pointsdecideanddecide_createanswer a refusing branch as a value (Evaluation::Refused);createandexecutekeep their signatures and reach the same branch as the typedCoreError::Refused, so every existing caller compiles and behaves as it does now. -
service/1definitions may declare a logical identity (identity: { field: … }): an ordinary typed schema field, kept apart from the storage address the instance is keyed by. The address is derived from it by one total function,entity_core::identity::address, and the kernel checks that the two still agree after every branch —IdentityMismatchwhere they do not. Text-like identities address ass:followed by their own contents, so an empty or whitespace identity is admitted; numeric identities share one canonical spelling, so1,1.0and1e0are one address and-0.0and0.0are one instance; composites address as canonical JSON. -
service/1definitions may declare relations (owns/references,one/many, and the field that carries them).EntityDefinition::validatechecks areferencescarrier's shape and optionality;Registry::validate_allchecks its kind against the target's identity field, anownscarrier on its target, an unregistered target, a second owner of one entity and a second relation claiming one field. Whether the referenced instance exists is still the shell's. -
Three field kinds,
service/1only:map(declared key spelling, declared value shape),union(adjacently tagged, with the content key derived from the tag) andbinary64(a finite double held as its token, so the sign of a zero survives in the bytes). -
Four condition operators,
service/1only:compare(an exact three-valued scalar comparison that answersunknownwheregt/ltanswerfalseand whereeqcompares structures),truthy, and the quantifiersfor_allandfor_anyover an array's elements or a map's values. Two new address forms come with them:<collection>.countand<array>.<n>. -
scales:declares the ordered value scales text comparison is answered inside. With none declared — which is every definition today — ordering two text values isunknown, neverfalse. -
Add runtime-neutral asynchronous complete-record storage ports, an atomic in-memory reference provider, bounded history verification and an executor for exact single and named-batch retries. Existing synchronous store APIs and bytes remain unchanged.
-
Add the
entity-eventlogIO edge: complete canonical records, requests, batches and imported boundaries use immutable Eventlog references with native complete-capture checks, transactional indexes, explicit binding administration and a bounded dedicated-thread synchronous bridge. The new crate requires Rust 1.91; the pure runtime crates remain on 1.85.
Changed
-
Every crate that depends on the Eventlog providers —
entity-eventlog,entity-sqlite,entity-postgresandentity-cli— pins them to Eventlog 0.3.0 (ac6b1731654329d32f1e3c9cf164fefad6a5b46a), which verifies history and blobs once per open and re-checks only what a transaction changed, and which proves on every resume that the committed prefix is still the one it verified — a committed frame damaged in place afteropenrefuses without altering the history, as it did before the once-per-open change. Measured through the runtime adapter over a 48-record store: 48 commits cost 29.97 s and 53.22 s against 74.86 s and 65.54 s before, while a read through an already-open handle is unchanged at about 240 ms — the adapter verifies every blob it reads against its own domain-framed digest, so the saving a caller sees here is on the write path. Stored bytes, guarded append, receipts, recovery and refusals are unchanged. The four manifests name one commit, so a consumer enablingeventlog-facadeoreventlog-providersresolves a singleeventlog-core. -
entity-sqliteselects the exact bundledrusqlite0.40.2 line shared with its Eventlog provider, preserving the existing schema, storage semantics and Rust minimums while allowing both providers in one consumer dependency graph. -
A
service/1decision's record carriesoutcome,effectandresponse, and itscreatecommand carries the caller'sargumentsbeside the fields they produced. Because that is a shape change, such a record is frameder.record/2and its original requester.request/2; theer.batch/1tag does not move, and each batch member carries its own record framing. A reader that knows onlyer.record/1refuses aner.record/2document at the framing tag, by name, before reading its payload. A store holdingservice/1records must not be opened by a build that predates them. -
Under
service/1, a numeric predicate answers on the value the specification source would have observed while the authored token is stored unchanged. In practice:1.0000000000000000001is equal to1and is still stored as written;1e-400is falsy and is still stored as written;9007199254740993and9007199254740992remain two values; and anintegeris narrowed to the source'si64range, so a value in theu64tail is now refused with its path. Equality, membership andmin/maxanswer whatcompareanswers. Nothing aboutkernel/1numbers moves — every operator and bound answers exactly what it answers today. -
rehydraterefuses aservice/1definition by name, before it reads an event: an event-only fold cannot see which branch ran. Replay aservice/1history from its decision records instead. -
docs/design/kernel-v0.1.md§ 6 andAGENTS.mdinvariant 8 said the evaluation order was eleven steps and numberedUnknownStatebeforeEntityMismatch. The code has always checked the type first and the order has always been twelve steps; both documents are corrected against the code, and a test now reads them so neither can drift back. No behaviour changed.
A kernel/1 definition is untouched by all of the above: it serialises to the bytes it serialised to before, gets the evaluation it got before refusal variant for refusal variant, and is refused at registration if it carries any key, field kind or operator only service/1 admits.