Skip to content

feat: two-layer math value schemas -- LaTeX presentation, MathExpression semantics, symbol tables, units - #18

Merged
Mearman merged 3 commits into
mainfrom
feat/math-value-schemas
Aug 17, 2026
Merged

feat: two-layer math value schemas -- LaTeX presentation, MathExpression semantics, symbol tables, units#18
Mearman merged 3 commits into
mainfrom
feat/math-value-schemas

Conversation

@Mearman

@Mearman Mearman commented Aug 17, 2026

Copy link
Copy Markdown
Member

Closes #15

A formula's meaning becomes two co-equal authoritative layers, joined in ContentFormulaSchema, plus the document-level curation layer they resolve against.

The two layers. presentation: { latex } is stored verbatim and is authoritative for rendering -- a renderer serialises the stored string and never re-emits it from semantics. content: MathExpression is authoritative for computation. Neither is stored derived from the other: string-to-tree lowering is total (worst case an unparsed node), tree-to-string rendering is partial, and storage takes the recoverable side of that asymmetry by carrying both verbatim. The atomic pair-edit rule is encoded in the shape and stated in the schema docs: editing one layer never silently mutates the other, and canonical forms used to match or diff the layers are derived views computed at comparison time, never written back in place.

The grammar (src/math.ts, closed and extensible through the operator namespace):

  • num -- exact rationals as canonical decimal-integer strings (numerator carries the sign, denominator strictly positive, no leading zeros, no -0), so unit chains and equality stay bit-exact where Number would already have drifted past 2^53
  • qty -- rational value + unit-registry id + GUM-style uncertainty (exact magnitude, optional unit override, optional coverage factor)
  • sym -- symbol-table reference; resolves lexically, so a sum/prod binder shadows same-id entries inside its body
  • app -- namespaced operator registry (math:divide, math:sqrt, room for domain registries); arity and semantics belong to the registry the prefix names
  • sum/prod -- binders with full-expression bounds
  • matrix -- rows of expressions, equal width enforced on both validation paths
  • unparsed { latex } -- first-class fallback, so coverage gaps stay visible data rather than parse failures

Units and dimensions. Dimension-exponent vectors over the seven SI bases; a document-carried unit registry with exact rational factor/offset conversions to coherent SI (foot = 381/1250 m, degree Celsius offset 5463/20 K) and domain normalisation contexts such as per-unit systems. The registry is data each producer populates -- this package keeps defining shapes, not shipping tables.

The symbol table. symbolTable? on all five ContentDocument arms via one shared field spread: entries key on (glyph, scope) and carry id, quantity kind, preferred unit, and definition source. It lives on the envelope, not inside LayoutMetadataSchema -- that schema is shared with LayoutDocument, which carries no formulas of its own. Presentation-inert by construction, and a standalone importable value.

Provenance. { source, pageRef?, editTrail } on the formula: origin, page reference, and an append-only audit trail.

Per the design note in the issue, in-flow math keeps travelling as ContentEmbeddedObjectBlock { objectKind: 'formula' } carrying ContentDocument { kind: 'formula' } -- the five-variant envelope shipped in 3.0.0 is extended rather than given a sixth variant, and the embedding block's frames describe only the rendered box: the semantic payload is position-independent by nature.

Mechanics: the recursion uses the repo's z.custom type-guard pattern (MathMlNodeSchema precedent; z.lazy() still collapses to unknown in the pinned Zod). ContentFormulaSchema joins the hand-transcribed set in src/content-json-schema-defs.ts (its mathml/content fields drag in opaque custom nodes, exactly like ContentTableCellSchema), SymbolTableSchema is transcribed so each arm's field is one named $ref rather than five inlined copies, the generator's override replaces both, and the regression test's transcribed-vs-live partition moves accordingly -- thirteen new math leaves are now held to a live z.toJSONSchema() comparison. CONTENT_FORMAT_VERSION stays 3: every new field is optional and additive, so this lands as a minor.

Generated by Claude Code

…able, expression grammar)

The semantic half of the formula model. Exact rationals are canonical
decimal-integer strings, because Number loses integer exactness above
2^53 and exactness is the whole point -- unit-conversion chains and
quantity equality stay bit-exact over canonical strings. Dimensions
are integer-exponent vectors over the seven SI bases. The unit
registry carries exact rational factor/offset conversions to coherent
SI plus domain normalisation contexts (per-unit systems), and is
document-carried data rather than a table shipped in this package.
The symbol table keys curation entries by (glyph, scope) mapping to
id, quantity kind, preferred unit, and definition source.
MathExpression is a closed eight-variant grammar -- num, qty with
uncertainty, sym, app over a namespaced operator registry, sum/prod
binders with bounds, matrix, and unparsed as a first-class fallback
so lowering coverage gaps stay visible data rather than parse
failures. The recursive union uses the package's z.custom
structural-guard pattern (MathMlNode's), since z.lazy collapses to
unknown in the pinned Zod.
…able on every document arm

ContentFormula gains presentation (verbatim LaTeX, authoritative for
rendering, serialised exactly as stored and never re-emitted from
semantics), content (the computation-authoritative MathExpression),
and provenance. Neither layer is stored derived from the other --
string-to-tree lowering is total, tree-to-string rendering is
partial, so storage takes the recoverable side by carrying both
verbatim -- and the atomic pair-edit rule holds that editing one
layer never silently mutates the other, with canonical forms used
for matching kept as derived views rather than written back in
place. mathml stays required (an empty array for a LaTeX-authored
formula) so every existing constructor remains valid and
CONTENT_FORMAT_VERSION stays 3. The document-level symbolTable joins
all five ContentDocument arms through one shared field spread -- on
the envelope rather than inside LayoutMetadataSchema, which
LayoutDocument shares and which carries no formulas of its own.
ContentFormulaSchema joins the hand-transcribed set: its mathml and
content fields reach the opaque MathMlNodeSchema and
MathExpressionSchema custom nodes, so the generator now replaces
every occurrence with a $ref to a new $defs.ContentFormula fragment,
clearing the auto-generated body first (a real z.object's jsonSchema
is already populated at finalize time, unlike a custom node's empty
{}). SymbolTableSchema is transcribed too, keeping each arm's
symbolTable field one named reference instead of five inlined copies
of the whole unit-registry subtree. The regression test's
transcribed-vs-live partition moves accordingly -- thirteen
non-recursive math leaves join the live-compared set, while
ContentFormula, MathExpression, and the recursive variants join the
hand-verified set downstream of the MathExpressionSchema custom
node. The smoke test's formula-variant assertions follow the new
$ref path, and the README's z.custom inventory gains
MathExpressionSchema.
@Mearman
Mearman merged commit 610b370 into main Aug 17, 2026
11 checks passed
@Mearman
Mearman deleted the feat/math-value-schemas branch August 17, 2026 14:13
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 3.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Math value schemas: two-layer math node (authoritative LaTeX presentation + JSON semantic core), symbol tables, units

1 participant