Resolves #14674
The config plane's overlay-drift class gets its root fix: every default leaf and formula moves into a new non-singleton Neo.ai.ConfigBase (ai/configBase.mjs, zero instantiation side effects), and ai/config.template.mjs becomes the thin, still-eager Neo.ai.Config singleton subclass — byte-identical external behavior, so the Tier-2 server templates' side-effect import (which materializes the Tier-1 parent for getParent registry resolution) and the Playwright template-resolver world are untouched. New-shape operator overlays subclass the base and carry delta-only leaf() declarations: Neo.setupClass's descriptor-driven hierarchical merge (the merge: 'deep' descriptor on the Provider's data_) makes a leaf added to the base reach every subclass overlay with zero overlay edits — spec-proven. Namespace-idempotency IS the dual-shape loader: a snapshot-style overlay self-registers Neo.ai.Config first and wins exactly as today (fail-open, deprecated-honest in the template docblock + migration output); ConfigProvider.load()'s .mjs side-effect-import path covers both shapes unchanged. A new ai/scripts/setup/migrateConfigOverlay.mjs converts existing snapshot overlays: preview-first (--write applies with a .pre-migration.bak), idempotent via shape detection, and diffing at the DECLARATION level (leaf descriptors projected without the derived parse function) so the machine's env can neither fake nor mask a delta; non-renderable differing leaves (function-valued defaults) are reported honestly and left to inherit. initServerConfigs' verbatim root copy means fresh bootstraps get the subclass shape automatically with no pipeline change.
Evidence: L2 (51 unit specs green across the five impacted suites + lint-config-template-ssot clean) → L3 (live read-only preview of the migration script against this machine's real operator-managed snapshot overlay: drift none, deltas none, dummyEmbeddingFunction honestly skipped). Residual: none — close-target ACs covered by specs + the live preview.
Deltas from ticket
- Design correction recorded on the ticket (two checkpoint comments): the initially-sketched lazy default export was falsified against the Tier-2 seam (per-server templates side-effect-import the root template precisely to eagerly materialize the Tier-1 parent) — the shipped shape is three files with NO laziness; the template stays eager and byte-identical.
- Overlay-delta contract sharpened: deltas must be
leaf() declarations, not bare primitives — a primitive merged over a leaf descriptor does not survive ConfigProvider construction (empirically falsified). Documented in the template docblock with a worked example.
leaf()'s real return shape drove the diff design: {default, env, ...metadataSpread, type, parse: Function|null} — the derived parse (a function on every env-bound leaf) is excluded from equality via a projection, and spread metadata is reconstructed into the leaf() 4th argument on render.
- The "three lived drift instances as named fixtures" AC (E6 seed): the inheritance proof covers the drift CLASS structurally (any base leaf reaches a delta overlay unchanged); the three historical instance leaf-names from the retired-epic corpus are not individually re-enumerated here. If the reviewer wants them named, that is a follow-up fixture addition, not a design gap.
- Loader change turned out to be zero-code: the "loader accepts both shapes" AC is satisfied by namespace-idempotency + the existing
load() import path — verified rather than built.
Test Evidence
- Lane suites:
npx playwright test -c <unit config> test/playwright/unit/ai/{configBase,config.template,ConfigProvider,migrateConfigOverlay}.spec.mjs test/playwright/unit/test/ConfigTemplateResolver.spec.mjs --workers=1 — 51 passed (root-fix inheritance proof, deep-merge sibling survival, formula inheritance, zero-side-effect base, template-shape ledger assertions, resolver world unchanged, migration helpers incl. parse-identity immunity + metadata round-trip).
- SSOT lint:
node ai/scripts/lint/lint-config-template-ssot.mjs — clean (0 violations, baselines intact).
- Live falsifier (read-only):
node ai/scripts/setup/migrateConfigOverlay.mjs against this machine's real snapshot overlay — drift none / deltas none / one honest function-default skip; the generated subclass overlay renders cleanly.
- Directly touched surfaces:
ai/configBase.mjs + ai/config.template.mjs: configBase.spec.mjs + config.template.spec.mjs | ai/scripts/setup/migrateConfigOverlay.mjs: migrateConfigOverlay.spec.mjs.
Post-Merge Validation
Commits
- 99e8a757c7 — split the template into the extendable non-singleton ConfigBase + the thin eager singleton subclass; adapt the ledger spec; add the inheritance/deep-merge/formula/zero-side-effect proofs.
- 5aa02fec82 — the snapshot-overlay migration script (preview-first, idempotent, declaration-level projection diff) + its 12-spec suite.
Evolution
Two of my own assumptions died to live falsification mid-implementation: reaching the overlay class through the config proxy's constructor returns a bound function that drops statics (everything misread as drift → fixed via the registry path), and the assumed leaf() shape omitted the derived parse function (everything misread as non-renderable → fixed via the projection). Both are the same lesson the ticket's own stale-extractor paragraph taught: read the primitive before coding against its imagined shape.
Authored by Vega (Claude Fable 5, Claude Code). Session c4f8e75b-bf73-448b-bee3-6a17e3b1cb45.
Resolves #14674
The config plane's overlay-drift class gets its root fix: every default leaf and formula moves into a new non-singleton
Neo.ai.ConfigBase(ai/configBase.mjs, zero instantiation side effects), andai/config.template.mjsbecomes the thin, still-eagerNeo.ai.Configsingleton subclass — byte-identical external behavior, so the Tier-2 server templates' side-effect import (which materializes the Tier-1 parent forgetParentregistry resolution) and the Playwright template-resolver world are untouched. New-shape operator overlays subclass the base and carry delta-onlyleaf()declarations:Neo.setupClass's descriptor-driven hierarchical merge (themerge: 'deep'descriptor on the Provider'sdata_) makes a leaf added to the base reach every subclass overlay with zero overlay edits — spec-proven. Namespace-idempotency IS the dual-shape loader: a snapshot-style overlay self-registersNeo.ai.Configfirst and wins exactly as today (fail-open, deprecated-honest in the template docblock + migration output);ConfigProvider.load()'s.mjsside-effect-import path covers both shapes unchanged. A newai/scripts/setup/migrateConfigOverlay.mjsconverts existing snapshot overlays: preview-first (--writeapplies with a.pre-migration.bak), idempotent via shape detection, and diffing at the DECLARATION level (leaf descriptors projected without the derivedparsefunction) so the machine's env can neither fake nor mask a delta; non-renderable differing leaves (function-valued defaults) are reported honestly and left to inherit.initServerConfigs' verbatim root copy means fresh bootstraps get the subclass shape automatically with no pipeline change.Evidence: L2 (51 unit specs green across the five impacted suites +
lint-config-template-ssotclean) → L3 (live read-only preview of the migration script against this machine's real operator-managed snapshot overlay: drift none, deltas none,dummyEmbeddingFunctionhonestly skipped). Residual: none — close-target ACs covered by specs + the live preview.Deltas from ticket
leaf()declarations, not bare primitives — a primitive merged over a leaf descriptor does not survive ConfigProvider construction (empirically falsified). Documented in the template docblock with a worked example.leaf()'s real return shape drove the diff design:{default, env, ...metadataSpread, type, parse: Function|null}— the derivedparse(a function on every env-bound leaf) is excluded from equality via a projection, and spread metadata is reconstructed into theleaf()4th argument on render.load()import path — verified rather than built.Test Evidence
npx playwright test -c <unit config> test/playwright/unit/ai/{configBase,config.template,ConfigProvider,migrateConfigOverlay}.spec.mjs test/playwright/unit/test/ConfigTemplateResolver.spec.mjs --workers=1— 51 passed (root-fix inheritance proof, deep-merge sibling survival, formula inheritance, zero-side-effect base, template-shape ledger assertions, resolver world unchanged, migration helpers incl. parse-identity immunity + metadata round-trip).node ai/scripts/lint/lint-config-template-ssot.mjs— clean (0 violations, baselines intact).node ai/scripts/setup/migrateConfigOverlay.mjsagainst this machine's real snapshot overlay — drift none / deltas none / one honest function-default skip; the generated subclass overlay renders cleanly.ai/configBase.mjs+ai/config.template.mjs: configBase.spec.mjs + config.template.spec.mjs |ai/scripts/setup/migrateConfigOverlay.mjs: migrateConfigOverlay.spec.mjs.Post-Merge Validation
node ai/scripts/setup/migrateConfigOverlay.mjs(preview) on a deployment clone with a hand-edited snapshot overlay and confirm the delta report matches the operator's known edits before any--write.Commits
Evolution
Two of my own assumptions died to live falsification mid-implementation: reaching the overlay class through the config proxy's
constructorreturns a bound function that drops statics (everything misread as drift → fixed via the registry path), and the assumedleaf()shape omitted the derivedparsefunction (everything misread as non-renderable → fixed via the projection). Both are the same lesson the ticket's own stale-extractor paragraph taught: read the primitive before coding against its imagined shape.Authored by Vega (Claude Fable 5, Claude Code). Session c4f8e75b-bf73-448b-bee3-6a17e3b1cb45.