0.2.0
An adversarial review of 0.1.0 — a hands-on pass against the shipped binary and an independent multi-angle code review — found defects in the kernel, claims the documents made that the code did not keep, and gaps in the shell. All of it is addressed here; the record is docs/reviews/2026-08-25-adversarial-review.md.
Changed — behaviour you may be relying on
- A definition with a key the model does not declare is now refused rather than ignored:
requried: trueleft a field optional, and aprecondition:that should have beenpreconditions:left an operation unguarded. A condition must carry exactly one known operator —{eq: …, ne: …}used to parse aseqand drop the rest. - A precondition may no longer read
$state. It resolved to the state the operation was heading for, soeq: [$state, draft]on adraft → submittedtransition refused every time it should have passed. Use$from_stateand$to_state, which say which one they mean. An invariant may no longer read$to_state. eq,ne,inandcontainsnow compare numbers numerically, so100equals100.0and they agree withgt/gte/lt/lte. A definition tested with integer fixtures used to refuse the same document written with a decimal point.- A
setvalue or event payload whose reference its scope could never resolve —$args.*in a creation event, an argument the operation does not declare,$now— is refused when the definition is registered, not on every execution. - A reference path is checked in full:
$fields.address.countriand$fields.title.lengthare refused at registration. They used to register and then readfalsefor every instance. Registry::registerrefuses a definition whose(entity, version)is already registered;Registry::replaceis how to mean it. Two--definitionfiles of one type used to let the last one silently win.- A constraint on a kind it does not govern (
valueson astring,itemson anobject,min_lengthon aninteger) is refused instead of ignored. EntityInstance.fieldsis aserde_json::Maprather than aBTreeMap, which removes the conversions that copied every field to read one of them. Ordering is unchanged: by name.entity validatereports every file it is given, whatever went wrong with the one before it, and exits1— a file it cannot read or parse is one of its findings rather than a usage error. It no longer prints a JSON refusal after its report.- The YAML reader is
serde_yaml_ng;serde_yaml0.9.34 is published as deprecated and receives no fixes.
Added
unknown_state: an instance claiming a lifecycle state the definition does not declare is refused by name, before the operation is looked at.- An empty or whitespace identity is refused at
create. EntityDefinition::validate, so a tool can check a definition without building a registry.Registry::replaceandRegistry::versions.- A condition with an unknown operator now says which operator, and lists the twelve that exist, instead of reporting that the data matched no variant of an untagged enum.
entityparses inline and piped JSON as JSON before trying YAML, so surrogate-pair escapes (whatjson.dumpsandjq -aemit) are accepted; and refuses a second flag reading standard input rather than handing it an empty document.entity graph --format dotescapes names, so a state containing a quote produces valid DOT.- Defaults declared inside an object are applied, at every depth an object already reaches.
- Integers outside the range of a 64-bit signed value are compared numerically rather than wrapped.
Fixed — in the guarantees themselves
- The purity scan (R-01) was evadable by a grouped import, an alias,
std::io,include_str!or a line beginning with*. It now strips comments and string literals, expands everyusepath and matches whole words, and is checked against fourteen plantings and eight lookalikes. - The requirements checker accepted any
fnas a pin, and could not parse a row whose id cell carried a marker — 21 rows were checked by nothing. Both are checks now. - R-34 and AGENTS.md invariant 4 claimed the lifecycle state was closed by the type. It is closed by the kernel's own writes; the documents now say which, and
unknown_statecloses the gap that could be closed without sealing the type. - The release workflow ran a shorter gate than CI — no
cargo doc— so a tag could ship with broken intra-doc links. Both now call one reusable gate workflow, which also runs an MSRV job. - Release and Pages workflows pin every action by commit; the release job no longer persists a token in
.git/config.