feat: two-layer math value schemas -- LaTeX presentation, MathExpression semantics, symbol tables, units - #18
Merged
Merged
Conversation
…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.
|
🎉 This PR is included in version 3.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: MathExpressionis authoritative for computation. Neither is stored derived from the other: string-to-tree lowering is total (worst case anunparsednode), 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 whereNumberwould already have drifted past 2^53qty-- rational value + unit-registry id + GUM-style uncertainty (exact magnitude, optional unit override, optional coverage factor)sym-- symbol-table reference; resolves lexically, so asum/prodbinder shadows same-id entries inside its bodyapp-- namespaced operator registry (math:divide,math:sqrt, room for domain registries); arity and semantics belong to the registry the prefix namessum/prod-- binders with full-expression boundsmatrix-- rows of expressions, equal width enforced on both validation pathsunparsed { latex }-- first-class fallback, so coverage gaps stay visible data rather than parse failuresUnits 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 fiveContentDocumentarms 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 insideLayoutMetadataSchema-- that schema is shared withLayoutDocument, 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' }carryingContentDocument { kind: 'formula' }-- the five-variant envelope shipped in 3.0.0 is extended rather than given a sixth variant, and the embedding block'sframesdescribe only the rendered box: the semantic payload is position-independent by nature.Mechanics: the recursion uses the repo's
z.customtype-guard pattern (MathMlNodeSchemaprecedent;z.lazy()still collapses tounknownin the pinned Zod).ContentFormulaSchemajoins the hand-transcribed set insrc/content-json-schema-defs.ts(itsmathml/contentfields drag in opaque custom nodes, exactly likeContentTableCellSchema),SymbolTableSchemais transcribed so each arm's field is one named$refrather 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 livez.toJSONSchema()comparison.CONTENT_FORMAT_VERSIONstays 3: every new field is optional and additive, so this lands as a minor.Generated by Claude Code