All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for unreleased work are not written here directly. Each issue drops a
fragment in changelog.d/; the fragments are assembled
into a version section at release. See that README for the format and for when a
change warrants an entry at all.
[0.3.0] 2026-09-06
What Compatibility.breaks/2 reports changes, and an entry's type may now
name a declaration. A declaration field's one_of is a completion hint and
never a break, so {:group_added, name} is gone; in its place a record field
going required -> optional is a break, {:made_optional, name}, which is what
0.2.0's stricter read check made it. Beside them, an entry's type - and a
list entry's item_type - may name a declaration the document's types key
declares, and Index.type/2 answers {:declared, name} for one. A caller
matching on the tuple that is gone stops matching, which is why this release is
a MINOR under 0.x. Both arms are recorded in the sd-ADR-0001 amendment
accepted on the operator's campaign-034 ruling.
Added
- An entry's
type, and alistentry'sitem_type, may name a declaration the document'stypeskey declares: the entry carries{:declared, name}and contributes the declaration's fields beneath its own path, exactly as an inlinedobjectentry contributes itsfields. A spelling that names no declaration and nothing in the closed set is still unknown.
Changed
- A declaration field's
one_ofis a completion hint and never a break:Compatibility.breaks/2reports nothing for a value group added, removed, reordered, widened or shrunk. Match on{:made_optional, name}where you matched on{:group_added, name}, which is gone. - A record field going required -> optional is now a break,
{:made_optional, name}: an optional field stopped covering a required shape field when the read check was amended. Mark the fieldrequired?: truein the redefinition wherever the record does promise the value, and the redefinition stays compatible. StatifierDatamodel.Indexcarries the document's declarations, soIndex.type/2answers{:declared, name}as well as one of the nine types.
[0.2.0] 2026-09-06
A stricter read check, and nothing else. An optional record field no longer
covers a required shape field: step 3 of ADR-0001's decision 8 now reads the
record side's required? as well as the shape side's, so a document that
declared a field optional and leaned on the looser reading gets {:missing, [name]} where the check used to answer satisfied. That is a narrowing of
what the read check accepts and so a breaking change for a document holding
the looser reading, which is why this release is a MINOR under 0.x. The
ruling is recorded as the amendment to sd-ADR-0001 that closes the third of
the questions the record carried; that section stands at proposed until it
is flipped separately.
Changed
- The read check is stricter: a record field the document declares optional
no longer covers a shape field the document marks required, so a read that
was satisfied now answers
{:missing, [name]}. Breaking for a document that relied on the looser reading; the fix is one key,"required?": trueon the record's field wherever the record does promise the value.
[0.1.0] 2026-09-06
The first release. StatifierDatamodel is the reader of a datamodel document
and the home of every question that can be answered from the document
alone: the path/type index over the three scopes, the named record and
shape declarations under the types key, the read check between two type
expressions, the projection to the expression language's value kinds, and
what a redefined declaration takes away. The package has no runtime
dependencies and none in the family, which is what lets statifier_blocks
and statifier_ui take it without either taking the other. The contract is
ADR-0001.
Added
A datamodel document indexes to its declared paths and their types, at every nesting depth, through one total admission step that tells "not a document" apart from "a document declaring nothing".
The document-level reads over that index: the declared-path set, the declared paths strictly under a prefix in document order, and the
one_ofenumerations the document declares per path.The document's
typeskey indexes to the named record and shape declarations it carries, by name, through one total function: a half-written declaration declares nothing, and a field typed by another declared name resolves to it.Type expressions over a document - a declared name, one of the closed set's types, an opaque string that compares by identity, or unknown - with the read check over them: unknown is permissive both ways, identity is satisfied, and a record is read as a shape when the record's fields cover the shape's required set. A record is never read as another record.
StatifierDatamodel.Index.path_types/1projects an index to%{path => kind | {:list, kind} | {:one_of, values}}in the expression language's vocabulary of value kinds, which is the map an expression editor consumes. A path this projection cannot name - an object, a list whose element type the document does not give, a type outside the closed set - is absent from the map, and absence means unknown, not wrong.A redefined declaration reports what it takes away: every field removed, retyped, made required, added as required, or newly constrained by a
one_ofgroup, ordered by field name. Widening a declaration - an optional field added, a required field made optional, a group removed - reports nothing, and a name declared on neither side is an error rather than an empty list.The required field names of a shape that a map does not fill, in the shape's own field order. A name that is not a shape - a record, or a name the document does not declare - is an error rather than nothing missing.
Changed
dateis admitted in the document's closed type set, so an entry typed"date"indexes as:datewhere it was previously an unknown type. A document already spelling"date"needs no change; a host that worked around the gap by spelling such a path"datetime"or"string"should spell it"date"now.