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.7.0] 2026-10-13
A minor that adds three public modules and two optional keys to the datamodel document. StatifierDatamodel.Events indexes the new events and sends keys, StatifierDatamodel.Conformance checks a value against a type, and StatifierDatamodel.Reads checks the paths a chart's conditions read against a document. Index, Document, Declarations, Types and Coverage answer exactly as before for every document. A document that already carries a top-level events or sends key of its own is now read by the new index, and the shipped JSON Schema stays advisory: the upgrading page says what to change. No runtime dependency is added.
Added
StatifierDatamodel.Reads.undeclared/2checks the paths a chart's conditions read against a datamodel document: given the document and a list of reads, each%{events: names, path: segments, location: term}with the path in the expression language'sreferences/1form, it answers the reads the document does not declare, in input order, each with its location and a reason,:no_declared_event,{:undeclared, event_name, prefix}or{:undeclared_path, prefix};_event.datapaths are checked against the payload type of every declared event the names match (SCXML token-prefix matching,*and.*included), the_eventsystem fields are skipped, other roots are checked against the declared paths only when the document declares one, and the package takes no dependency on the expression language.StatifierDatamodel.Eventsreads two new optional top-level keys of the datamodel document,eventsandsends: lists of entries carrying aname, a payloadtype(one of the nine or a declared record or shape), and an optionallabelandnote, indexed in list order with the first occurrence of a name kept, throughfrom_document/1andfetch/3; an entry missingnameortypeis dropped, and an entry whose type names nothing declared is typed:unknownand listed byunresolved/1, never refused.- The JSON Schema gains the optional
eventsandsendsproperties, each an array of entries requiring a non-emptynameandtype. - A document carrying a top-level
eventsorsendskey (a host's own annotation until now) is read by the new index; nothingIndex,Document,Declarations,TypesorCoverageanswers changes, but a host whose own annotation used either name should rename that key, or rewrite it as the entries above, since the JSON Schema now rejects either key when it is not a list of such entries. StatifierDatamodel.Conformance.conforms/3checks a value, such as an event payload decoded from JSON or built in Elixir, against a type expression (a declared record or shape, an inline shape, or one of the nine), and answers:okor{:error, mismatches}, each mismatch{path, reason}withreasoneither:missing(a required field absent ornil) or{:expected, type}; scalars conform as a decoded JSON value spells them or as the matching struct, adecimaland adurationalso as ADR-0001 decision 4's strings, a list descends its item type, extra keys are ignored andone_ofis not enforced;Types.satisfies/3andCoverage.missing/3answer as before.
[0.6.0] 2026-10-11
A minor that adds one public function and types four keys in the shipped
schema. StatifierDatamodel.Types.value_kind/1 is new, and no existing
signature or answer changes: index/1 and path_types/1 answer exactly as
before for every document. The shipped JSON Schema stays advisory. Upgrading
needs no migration and adds no runtime dependency.
Added
StatifierDatamodel.Types.value_kind/1answers the expression language's value kind for a type expression::string,:numberfor:integerand:decimal, and:boolean,:date,:datetimeand:durationas themselves, withnilfor:object,:listand every type that is not one of the nine. It is the answerStatifierDatamodel.Index.path_types/1gives, which now reads its kinds from it and answers exactly as before.
Fixed
- The shipped JSON Schema now rejects a scope whose
labelordescriptionis not a string, and a declaration whosenameorlabelis not a string; the schema stays advisory, andindex/1admits and answers for every document exactly as before.
[0.5.2] 2026-10-08
A documentation release. The code is unchanged from 0.5.1: no module, function, type or shipped schema moves, and upgrading needs nothing. What changes is what a reader finds on hexdocs and on the package page.
Added
- "Upgrading a host from 0.4 to 0.5", a page that says what a host changes to move from 0.4 to 0.5 and on to the 0.5.1 patch, and says NONE where the answer is nothing; it ships in the package so the README's link to it resolves on hex.pm.
Changed
- The hexdocs sidebar gains an Upgrading group holding the new page, below the Explanation group, and the README's documentation map links the page.
[0.5.1] 2026-10-05
A documentation release. The code is unchanged from 0.5.0: no module, function, type or shipped schema moves, and upgrading needs nothing. What changes is what a reader finds on hexdocs and on the package page.
Added
- "Why the family shares one type registry", an explanation page on why the block editor and the expression editor read the datamodel document through one package that depends on neither, what it returns, the alternatives weighed and what the arrangement costs; it ships in the package so the README's link to it resolves on hex.pm.
Changed
- The README is rewritten as an introduction: what the package is, why it exists, how to install it, one basic-usage example (still run as a doctest), a documentation map grouped by the reader's question that links the module pages, the explanation page and the decision records, and compatibility. The per-function walk leaves the README; the module documentation on hexdocs already carries it.
- The hexdocs sidebar groups pages by the kind of page they are, with the README and this changelog ungrouped at the top and the new Explanation group below them. The decision records are no longer published as hexdocs pages; the README links them on GitHub.
[0.5.0] 2026-09-26
The datamodel document gains a JSON Schema a host can validate against.
The package now ships a hand-written draft-07 schema of the version-1
document in its tarball, and StatifierDatamodel.Schema answers where it
is installed and what it says, so a host checks a document with the
draft-07 validator its own language already has before anything indexes
it. The schema is advisory: nothing in the package calls it, and
StatifierDatamodel.Index.index/1 answers exactly as before for every
document. Upgrading needs no migration and adds no runtime dependency; the
release is a MINOR under 0.x because it adds a module and a shipped file
and changes no existing signature.
Added
priv/schemas/datamodel-document.schema.json, a hand-written draft-07 JSON Schema of the version-1 datamodel document, ships in the package;StatifierDatamodel.Schema.path/0answers where it is installed andjson/0its text. It is advisory: nothing in the package calls it, andStatifierDatamodel.Index.index/1answers exactly as before for every document.
[0.4.0] 2026-09-06
A type expression grows one arm. A consumer holding a structural value the
host never declared can now say what it holds inline, {:shape, members},
instead of inventing a declaration in the document to point at; the read check
decides an inline shape against a declaration and against another inline shape
member-wise, and identity for that arm is member-set-wise rather than by term.
Nothing a document spells changes, and a document that declares only named
types reads exactly as it did, which is what makes this arm additive - the
release is a MINOR under 0.x because the type expression a caller matches on
admits a shape it did not before. Beside it, Compatibility.breaks/2 now
documents the first element of a break as the kind, the row of the
redefinition table the break came from, so a case over the five kinds is
exhaustive without re-deriving from the two declarations why a row broke. The
inline shape arm is recorded in the sd-ADR-0001 amendment, accepted on the
operator's ruling of 2026-09-06.
Added
- A type expression admits an inline, unnamed shape,
{:shape, members}, so a consumer holding a structural value the host never declared can say what it holds without inventing a declaration. The read check decides it against a declaration and against another inline shape member-wise, andto_string/1renders it.
Changed
- A break from
Compatibility.breaks/2documents its first element as the kind: the row of the redefinition table the break came from. The five kinds are unchanged and the vocabulary is closed at them, so acaseover:field_removed,:type_changed,:made_required,:required_addedand:made_optionalis exhaustive and a host need not re-derive from the two declarations why a row broke. No result changes shape or value. - Identity in the read check is member-set-wise for an inline shape: two inline shapes carrying the same members in a different order are the same type expression. Every other arm still compares by term, no document spelling changes, and a document that declares only named types reads exactly as it did.
[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 ruling of 2026-09-06.
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.