Context
PR #12164 implemented BaseConfig with a metaTree config + construct-time compile + scalar-only validation — a re-derived shape, not the graduated Discussion #12100 prototype. Operator opted in (2026-05-29) to conform to the prototype if it is genuinely more elegant and makes sense. It is: the prototype uses the standard data config + an afterSetData override (the canonical Provider compile seam) + a leaf(default, env, type) factory (explicit type + .validate() parser). This restores elegance the #12164 divergence dropped, and aligns with AC-Epic-1 ("extend, don't reinvent"). Provenance: comparing #12164 to the graduated prototype surfaced the gap; operator-directed to conform.
The Problem
- Parallel
metaTree config. A separate metaTree config slot + construct-time imperative setData is the kind of parallel machinery AC-Epic-1 disfavors, and it isn't reactive — the tree compiles only at construct, not on a data reassignment.
- Type inference loses null-default types. Leaves are raw
{env, default, parse} literals; compileMetaLeaves infers type from the default via Neo.typeOf. A null/undefined default yields type: null → that leaf can never be validated.
- Scalar-only validation (#12167) — object/JSON leaves bypass the parser check.
The Architectural Reality
Provider#afterSetData(value) → processDataObject(value) (src/state/Provider.mjs:165 / :594) is THE seam that converts a plain data object into the reactive #dataConfigs map. The prototype overrides it: compileMetaLeaves(value) → super.afterSetData(plainData) → reactive build → #applyEnvLayer().
- The prototype's
leaf(default, env, type) factory produces {default, env, parse, type} where parse carries a .validate() method and type is explicit (not inferred).
- The prototype's
#resolveLeafWithEnv(path) resolves #runtimeEnvOverrides.get(meta.env) ?? process.env[meta.env] ?? default at override-resolution points (bounded — not an every-read live re-read).
The Fix
- Drop the
metaTree config + construct override; subclasses declare the meta-tree as data: {…}; override afterSetData to compile → super.afterSetData(plainData) → #applyEnvLayer.
- Add a
leaf(default, env, type) factory (home decided via structural-pre-flight at impl — BaseConfig export or small helper module); rewrite all six templates' leaves to use it; carry explicit type + a .validate() parser.
- Validation via
parse.validate() for any value at a known leaf path (subsumes #12167); namespace-recursion intermediates still pass through.
#resolveLeafWithEnv precedence (runtime override ?? process.env ?? default), resolved at override points.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
BaseConfig tree-source config |
ai/BaseConfig.mjs (metaTree → data) |
Tree declared as data; compiled in afterSetData |
n/a |
JSDoc |
metaTree+construct confirmed in #12164 |
leaf(default, env, type) factory |
new (ai/BaseConfig.mjs or helper) |
Produces {default, env, parse, type} w/ .validate() + explicit type |
n/a |
JSDoc |
Prototype #12100 |
internalSetData validation |
ai/BaseConfig.mjs |
parse.validate() any value at known leaf path |
warn-keep |
JSDoc |
Scalar-only gate (#12164) |
6 config.template.mjs leaves |
all templates |
{env,default,parse} literals → leaf(...) calls |
n/a |
— |
Current literal shape |
Acceptance Criteria
Out of Scope
- Parent-chain realm inheritance (#12166) — sequence this ticket FIRST (correct the shape, then add hierarchy) to avoid double template churn.
dummyEmbeddingFunction SSOT (#12165).
- Unbounded every-read live
process.env re-resolution — explicitly NOT adopted (bounded override-point resolution only).
Avoided Traps
- Keeping
metaTree as a separate config (the #12164 re-derived divergence) — rejected per AC-Epic-1 + the graduated prototype; data + afterSetData is the canonical seam.
Decision Record impact
none — restores the graduated Discussion #12100 prototype shape; aligned with Epic #12101. Downstream amendment note: if impl surfaces an afterSetData reactivity edge, the Fix may need refresh.
Related
- Epic #12101; Discussion #12100 (prototype source); PR #12164 (the divergent shape this corrects); #12166 (parent-chain — sequences after); #12167 (subsumed by AC-3 here); #12165 (dummyEF SSOT).
Origin Session ID: b124b83a-2cca-4a2b-a711-64868439ba1e
Retrieval Hint: "BaseConfig data afterSetData seam leaf factory prototype shape conform #12100"
Context
PR #12164 implemented
BaseConfigwith ametaTreeconfig + construct-time compile + scalar-only validation — a re-derived shape, not the graduated Discussion #12100 prototype. Operator opted in (2026-05-29) to conform to the prototype if it is genuinely more elegant and makes sense. It is: the prototype uses the standarddataconfig + anafterSetDataoverride (the canonical Provider compile seam) + aleaf(default, env, type)factory (explicit type +.validate()parser). This restores elegance the #12164 divergence dropped, and aligns with AC-Epic-1 ("extend, don't reinvent"). Provenance: comparing #12164 to the graduated prototype surfaced the gap; operator-directed to conform.The Problem
metaTreeconfig. A separatemetaTreeconfig slot +construct-time imperativesetDatais the kind of parallel machinery AC-Epic-1 disfavors, and it isn't reactive — the tree compiles only at construct, not on adatareassignment.{env, default, parse}literals;compileMetaLeavesinferstypefrom the default viaNeo.typeOf. Anull/undefineddefault yieldstype: null→ that leaf can never be validated.The Architectural Reality
Provider#afterSetData(value)→processDataObject(value)(src/state/Provider.mjs:165/:594) is THE seam that converts a plain data object into the reactive#dataConfigsmap. The prototype overrides it:compileMetaLeaves(value)→super.afterSetData(plainData)→ reactive build →#applyEnvLayer().leaf(default, env, type)factory produces{default, env, parse, type}whereparsecarries a.validate()method andtypeis explicit (not inferred).#resolveLeafWithEnv(path)resolves#runtimeEnvOverrides.get(meta.env) ?? process.env[meta.env] ?? defaultat override-resolution points (bounded — not an every-read live re-read).The Fix
metaTreeconfig +constructoverride; subclasses declare the meta-tree asdata: {…}; overrideafterSetDatato compile →super.afterSetData(plainData)→#applyEnvLayer.leaf(default, env, type)factory (home decided via structural-pre-flight at impl — BaseConfig export or small helper module); rewrite all six templates' leaves to use it; carry explicittype+ a.validate()parser.parse.validate()for any value at a known leaf path (subsumes #12167); namespace-recursion intermediates still pass through.#resolveLeafWithEnvprecedence (runtime override ??process.env?? default), resolved at override points.Contract Ledger
BaseConfigtree-source configai/BaseConfig.mjs(metaTree→data)data; compiled inafterSetDatametaTree+construct confirmed in #12164leaf(default, env, type)factoryai/BaseConfig.mjsor helper){default, env, parse, type}w/.validate()+ explicit typeinternalSetDatavalidationai/BaseConfig.mjsparse.validate()any value at known leaf pathconfig.template.mjsleaves{env,default,parse}literals →leaf(...)callsAcceptance Criteria
data;metaTreeconfig removed; compile happens inafterSetDataviasuper.afterSetData(plainData).leaf(default, env, type)factory exists; all six templates use it; anull-default leaf carries an explicit type and is validatable.parse.validate()at a known path; namespace intermediates pass through.process.env?? default) preserved; runtime-override-survives-overlay behavior retained.data/leaf()shape.Out of Scope
dummyEmbeddingFunctionSSOT (#12165).process.envre-resolution — explicitly NOT adopted (bounded override-point resolution only).Avoided Traps
metaTreeas a separate config (the #12164 re-derived divergence) — rejected per AC-Epic-1 + the graduated prototype;data+afterSetDatais the canonical seam.Decision Record impact
none— restores the graduated Discussion #12100 prototype shape; aligned with Epic #12101. Downstream amendment note: if impl surfaces anafterSetDatareactivity edge, the Fix may need refresh.Related
Origin Session ID: b124b83a-2cca-4a2b-a711-64868439ba1e Retrieval Hint: "BaseConfig data afterSetData seam leaf factory prototype shape conform #12100"