feat(spec): add the spec language, evaluator and caps #29

Merged
TJC merged 14 commits from feat/m2-spec-language into main 2026-08-18 12:20:02 +01:00
Collaborator

Summary

  • zod schemas for the whole language: containers, the five marks, sizing, repeat, when, widget nodes, theme, inputs
  • expressions live in saved JSON as an AST, not source text, so parsing is zod's job and eval never enters the picture
  • whitelisted function table, template and widget scopes, global theme tokens
  • all four caps of decision 6 trip as per-node errors carrying the nodeId
  • the parse boundary rejects duplicate ids, undeclared widget props and capped expressions, each against its own node
  • M1's fixed-size node types move to layout/tree.ts as the resolved tree, freeing spec/nodes.ts for the authored one
  • closes #8, closes #9, closes #10

Test plan

  • pnpm test green: 104 tests, 62 of them new across template, evaluator, purity and caps
  • adversarial specs for each cap: 257-node and 33-deep expressions, a repeat of a million rows, a 20,001st primitive
  • a 200,000-deep paste is rejected without throwing, which zod's own RangeError otherwise turns into a white screen
  • a test greps every core directory for eval, new Function, the clock and randomness
  • pnpm lint and pnpm build green
## Summary - zod schemas for the whole language: containers, the five marks, sizing, repeat, when, widget nodes, theme, inputs - expressions live in saved JSON as an AST, not source text, so parsing is zod's job and eval never enters the picture - whitelisted function table, template and widget scopes, global theme tokens - all four caps of decision 6 trip as per-node errors carrying the nodeId - the parse boundary rejects duplicate ids, undeclared widget props and capped expressions, each against its own node - M1's fixed-size node types move to layout/tree.ts as the resolved tree, freeing spec/nodes.ts for the authored one - closes #8, closes #9, closes #10 ## Test plan - `pnpm test` green: 104 tests, 62 of them new across template, evaluator, purity and caps - adversarial specs for each cap: 257-node and 33-deep expressions, a repeat of a million rows, a 20,001st primitive - a 200,000-deep paste is rejected without throwing, which zod's own RangeError otherwise turns into a white screen - a test greps every core directory for eval, new Function, the clock and randomness - `pnpm lint` and `pnpm build` green
- schemas are the source of truth; only the recursive AST and node
  unions stay hand-written, annotated so a schema they do not match
  fails the build
- expressions live in saved JSON as an AST, not source text: a JSON
  editor is the only authoring surface in V1, so a concrete syntax
  would cost a parser in the riskiest component and buy nothing
- parsing is idempotent, so a template exported per decision 14
  imports again unchanged
- move M1's fixed-size node types to layout/tree.ts as the resolved
  tree layout consumes, freeing spec/nodes.ts for the authored one

Closes #8
- one whitelisted function table, typed so the schema's enum and the
  implementations cannot disagree
- fresh scope per widget boundary, props evaluated in the parent's
  scope, theme tokens global (decisions 7 and 9)
- own-property lookup only, or a var named constructor resolves to
  something off Object.prototype
- no truthiness and no string operations: both are reported rather
  than coerced
- a test greps core for eval, new Function, the clock and randomness

Closes #9
- all four caps of decision 6, each tripping as a per-node error
- the expression walk is iterative, so a monstrous ast is measured
  rather than crashing the measurer
- catch the parser's own stack overflow: zod descends a paste as
  deeply as it nests and a RangeError there white-screens the app
- non-finite arithmetic stops at its node; Infinity draws nothing and
  NaN voids a page of geometry
- errors inside a widget answer to the widget node, per decision 21

Closes #10
- the owner id propagated down every branch, so a capped expression on
  a nested node was reported against the page root
- the tests missed it by putting the offending node at the root

Refs #10
- decision 21 routes selection, the outline badge and the placeholder
  through one id, so a duplicate misdirects all three
- scoped per spec: a widget's internals answer to the widget node, so
  a fork drops anywhere without colliding with the page around it

Refs #8
- decision 7 makes an undeclared prop not a variable of that widget,
  so it evaluated in the parent scope and was then dropped
- a mistyped prop name was otherwise a silent fallback to the default

Refs #8
- decision 7 propagates a template variable inward through a widget
  prop, which needs the parent scope to hold one
- template inputs were declared and validated but unreachable

Refs #9
- the list was three directories, so src/widgets would have landed
  uncovered and the guard would have quietly stopped guarding
- core is now defined as the eslint boundary defines it: src bar src/ui

Refs #9
- String(NaN) is already NaN

Refs #10
- an input named toString took a function off Object.prototype
- binary and call recursed through evaluate, re-walking every subtree
- the conditional arm already recursed through evaluateExpr
- a kind or operator no switch knew fell through as undefined
- the TypeError that followed escaped attempt and white-screened the app
- a select default that is no option, and a default outside min or max
- two inputs of one name, which edit one variable and lose the first
- zod drops the key rather than parsing it, so strictObject saw no unknown
  key and the undeclared-prop check saw no prop
TJC requested review from TJC 2026-08-18 12:19:52 +01:00
TJC approved these changes 2026-08-18 12:19:59 +01:00
TJC merged commit 7ca1c27ad2 into main 2026-08-18 12:20:02 +01:00
TJC deleted branch feat/m2-spec-language 2026-08-18 12:20:07 +01:00
Sign in to join this conversation.
No reviewers
TJC
No labels
chore
core
render
ui
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TJC/daily-planner-template!29
No description provided.