refactor(loader): schema-engine kernel + globals migration (n8p/3tm.1)#88
Conversation
…ion (n8p/3tm.1) Introduce the declarative-schema kernel in validate-core.ts: a FieldSpec vocabulary and a record() interpreter that owns the isPlainObject guard, the reject-unknown-key loop, result-threading, and optional-omission as ONE unconditional sequence [LAW:dataflow-not-control-flow]. FieldSpecMap<T> checks each schema against its target type — a missing field is a compile error [LAW:types-are-the-program]. Prove it on the lowest-risk module: globals.ts drops from a 70-line loop-and- guard function to a 16-line GLOBALS_SCHEMA declaration. The unknown-key "noun" is the one per-record message variation, carried as schema data; every other message is reused from the existing field combinators, so the 119-test parity surface (dsl-loader/dsl-merge/config-resolution) stays byte-green. First child of epic n8p (declarative loader schema + one combinator engine). The tag combinators (taggedUnion / oneOfPresent), refine, and lazy land with their first consumers (cache, variables, actions) so nothing ships untested.
There was a problem hiding this comment.
Z.ai Coding Agent Review
Clean refactoring that replaces per-type validation loops with a data-driven schema engine. No must-change violations.
Notable pattern (pre-existing, documented): paletteSpec() (line 263) discards the field parameter and delegates to validatePaletteName, which hardcodes "palette" as the key to read from raw. The old code had the same coupling; the diff wraps it in the general FieldSpec interface and documents the convention in a comment. This is a latent [LAW:composability] concern — the type says "usable for any field" but it silently reads the wrong key if placed under a different name — but it's not introduced by this diff and is only used once, under the key "palette". If FieldSpec is later adopted more broadly, consider making validatePaletteName accept the field name as a parameter.
✅ Approved
First child of epic n8p — Declarative loader schema + one combinator engine (the "ceiling" follow-up to 3tm's file-split floor).
What
Replace the hand-rolled validation style with a declarative schema interpreted by one combinator engine. This PR lands the kernel and proves it on the lowest-risk module.
validate-core.tsgrows from a helper-bag into an interpreter:FieldSpec<T>— the parser for one record field (reports issues, yields value-or-undefined, carriesrequired).FieldSpecMap<T>/RecordSchema<T>— the field map is checked against the target type (-?forces a spec per field; forgetting one is a compile error)[LAW:types-are-the-program].record()— the interpreter: guard object → reject unknown keys → run each spec → collect present values, as one unconditional sequence[LAW:dataflow-not-control-flow]. Absorbs the per-type isPlainObject guard, the reject-unknown-key loop, the result-threading, and the optional-omission spreads.globals.ts: 70-line loop-and-guard function → a 16-lineGLOBALS_SCHEMAdeclaration + a 4-line wrapper.Parity (the hard bar)
Every error message/path/line is the contract. The unknown-key noun ("globals key") is the one per-record message variation, carried as schema data; all other messages are reused from the existing field combinators.
dsl-loader+dsl-merge+config-resolution— 119 tests byte-greenconfig-schema+default-dsl-config+dsl-spine— 33 greenpnpm typecheck,pnpm lintScope discipline
The tag combinators (
taggedUnionfor variables,oneOfPresentfor cache/actions),refine, andlazyland with their first consumers in later epic children, so nothing ships untested. The 8 non-offender modules (cross-ref, cycles, refs, …) stay as-is — their branches are load-bearing.