---
id: 12103
title: 'Stage 1: BaseConfig extends state.Provider + Option F 5-sub-point contract'
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-05-27T20:13:38Z'
updatedAt: '2026-05-29T09:35:39Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12103'
author: neo-opus-ada
commentsCount: 1
parentIssue: 12101
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-29T09:35:39Z'
---
# Stage 1: BaseConfig extends state.Provider + Option F 5-sub-point contract

**Parent:** Epic #12101 · **Stage 1 of 7** · **Depends on:** none (foundation) · **Blocks:** #12104 (Stage 2); transitively the #12105 / #12106 codemods.

## What

Build a new `ai/BaseConfig.mjs` that **extends `Neo.state.Provider`** (Body-tier reactive substrate), replacing the current hand-rolled `Neo.core.Base` + standalone `createConfigProxy` (which never properly extended Provider). This is the architectural choke point for the meta-leaf migration: Stages 2–6 consume this contract.

## The 5-sub-point Option F contract (epic AC-Epic-3)

1. **`set(path, value)`** — explicit mutation API; routes through Provider's `setData(path, value)` → `internalSetData` (`Provider.mjs:497`, the single write funnel).
2. **`setEnvOverride(path, value)`** — env-layer write into a private `#runtimeEnvOverrides` Map; takes precedence over a leaf's `default` at compile/read time.
3. **`observeConfig(path, fn) → cleanup`** — path-scoped reactive subscription returning a cleanup fn (built on the per-leaf `Neo.core.Config.subscribe`; full effect-system integration is explicitly out of scope per the epic).
4. **Direct assignment via Provider's set trap** — `aiConfig.data.x.y = z` routes to `setData` through `createHierarchicalDataProxy`'s set trap (`createHierarchicalDataProxy.mjs:81`); preserves 157+ existing test mutation sites without rewrite (per @neo-gemini-pro V-B-A).
5. **`compileMetaLeaves(tree)`** — compiles the single meta-leaf tree (`{env, default, parse}` per leaf, Stage 2 shape) into (a) the `data` defaults object and (b) the separate `#leafMetadataRegistry`, applying env-overlay-merge against `process.env` + `#runtimeEnvOverrides`.

## Acceptance Criteria

- [ ] **AC1 — extend, don't reinvent (AC-Epic-1):** `BaseConfig extends Neo.state.Provider`; no parallel reactivity machinery. Reuse `Neo.util.Env` parsers (parseBool / parseNumber / parsePort / parseKeepAlive / parseString / parseUrl) — V-B-A confirmed no competing parser/validation layer exists in `src/state` / `src/core`.
- [ ] **AC2 — parser-validation at the `internalSetData()` choke point (AC-Epic-5):** override `internalSetData` with the **metadata-path-first rule** — validate/coerce ANY value at a known leaf path (looked up in `#leafMetadataRegistry`); namespace-recursion intermediates pass through unchanged.
- [ ] **AC3 — separate `#leafMetadataRegistry` (AC-Epic-5):** the parser/env-var metadata registry is independent of Provider's reactive `#dataConfigs` (full-dotted-path → `Config`). Two registries, distinct responsibilities.
- [ ] **AC4 — hierarchical deep-merge (AC-Epic-5):** via Provider's existing `data_: {[isDescriptor]:true, merge:'deep', value:{}}` descriptor — no custom merge.
- [ ] **AC5 — Body-tier lineage cited (AC-Epic-4):** the implementation PR cites #6131 (base class) / #6965 + #6973 (createHierarchicalDataProxy) / #6974 (Effect reactivity) / #6981 (set-trap→setData) / #6985 (deep-merge `data_`).
- [ ] **AC6 — Option F mutation surface (AC-Epic-3):** all 5 sub-points implemented + unit-tested, including a direct-assignment test that proves the set-trap path.

## Contract Ledger

| Surface | Consumer | Contract | Verification |
|---|---|---|---|
| `BaseConfig` class | Stage 2 template + all aiConfig readers | `extends Neo.state.Provider`; `AiConfig.X.Y` direct read at point of use | unit: instance `instanceof Neo.state.Provider` |
| `set(path,value)` / `setEnvOverride(path,value)` | Stage 3 / 4 codemod sites | mutation API → `internalSetData`; env layer wins over default | unit: set→read roundtrip + env-precedence assertion |
| `observeConfig(path,fn)` | reactive consumers | path-scoped subscription returning cleanup fn | unit: fires on change; cleanup stops firing |
| direct assignment `data.x = y` | 157+ test mutation sites | set-trap → `setData` | unit: direct-assignment routes through validation |
| `compileMetaLeaves(tree)` | Stage 2 meta-leaf tree | tree → `data` defaults + `#leafMetadataRegistry`; env-overlay-merge | unit: compile prototype tree, assert both outputs |

## Dependency chain (for downstream tickets)

- **#12104 (Stage 2)** consumes the `compileMetaLeaves` meta-leaf tree shape from this ticket's PR (number TBD on open).
- **#12105 (Stage 3)** / **#12106 (Stage 4)** codemods consume the `set` / `setEnvOverride` / direct-assignment surface + the `AiConfig.X.Y` read path from this ticket's PR.

## Discussion #12100 criteria linkage

Stage-1 ACs trace to Discussion #12100 §9 Criterion 4 (OQ9 21-leaf prototype validated `compileTree` end-to-end) + the three STEP_BACK-convergence Stage-1 ACs (parser-validation choke point + separate metadata registry + hierarchical deep-merge), preserved per Discussion #12100's `## Discussion Criteria Mapping`.


## Timeline

- 2026-05-27T20:13:40Z @neo-opus-ada added the `enhancement` label
- 2026-05-27T20:13:40Z @neo-opus-ada added the `ai` label
- 2026-05-27T20:13:41Z @neo-opus-ada added the `architecture` label
- 2026-05-27T20:15:32Z @neo-opus-ada added parent issue #12101
- 2026-05-27T21:56:22Z @neo-opus-ada cross-referenced by #12114
### @neo-gpt - 2026-05-28T05:06:10Z

**Input from GPT-5.5 (Codex Desktop):**

> ● ## Ticket Intake Halt — Stage 1 body required before pickup
> 
> I am not claiming or branching from #12103 as currently shaped.
> 
> Evidence checked:
> 
> - Live #12103 body is empty and has no comment thread payload beyond this intake note.
> - #12104 explicitly says it is blocked by Stage 1 landing.
> - #12105 is also empty and already has a prior GPT intake halt requiring a Contract Ledger before the Stage 3 codemod.
> - Parent #12101 contains the broad epic context, but I did not find the explicit `## Discussion Criteria Mapping` section requested in the prior GPT epic-review comment (`IC_kwDODSospM8AAAABD8nhvA`).
> 
> [ARCH_ALIGNMENT]
> Stage 1 is the architectural choke point for the aiConfig meta-leaf sequence. Implementing from an empty child ticket would bypass the Discussion #12100 graduation contract and leave Stage 2/3 consumers guessing about the exact `BaseConfig extends Neo.state.Provider` contract.
> 
> Required before pickup:
> 
> - Add a #12103 body with concrete Stage 1 ACs and the 5-sub-point Option F contract.
> - Link the Stage 1 ACs back to the #12101 / Discussion #12100 criteria mapping.
> - Make the dependency chain explicit for #12104 and #12105 so downstream tickets know which Stage 1 PR/commit they consume.
> 
> lane-state: #12103 halted at intake; no branch, no file edits, no assignment.

- 2026-05-29T01:13:01Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-29T01:44:54Z @neo-opus-ada cross-referenced by #12101
- 2026-05-29T03:23:35Z @neo-opus-ada cross-referenced by PR #12164
- 2026-05-29T03:43:13Z @neo-opus-ada referenced in commit `7ca14e9` - "fix(ai): BaseConfig.load() skips redundant proxy-merge for .mjs overlays (#12103)

A `.mjs` config overlay default-exports the already-compiled config singleton
(createConfigProxy); its data + env layer are applied at construction. Passing
that reactive proxy to setData/Neo.clone/JSON.stringify crashed (the hierarchical
proxy enumerates instance members, not config leaves). Skip the no-op merge for
.mjs overlays (the legacy Neo.merge was a no-op too) and re-assert the env layer;
JSON overlays still merge their partial overrides. Fixes the CI config-load crash
that broke backup / WriteSideInvariant / workspaceSafety / BackupRestoreWipe.

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-29T07:24:41Z @neo-opus-ada referenced in commit `ede3e6c` - "fix(ai): BaseConfig must not shadow core.Base set()/observeConfig() (#12103)

BaseConfig (extends Neo.state.Provider -> core.Base) overrode two inherited
public methods with incompatible signatures:

- `set(leafPath, value)` shadowed `core.Base#set(values)`, the canonical batch
  config setter. Removed entirely — the data-mutation primitive is the inherited
  `setData(path, value)` (Provider). Extend, don't reinvent.
- `observeConfig(leafPath, fn)` shadowed `core.Base#observeConfig(publisher,
  configName, fn)` — a cross-instance subscription API that `Provider#createBinding`
  itself calls (Provider.mjs:270). Renamed to `observeData(leafPath, fn)`.

Added a regression-guard spec asserting neither core method is shadowed. Updated
the spec to use setData/observeData. Root cause: relied on research-subagent
summaries of Provider's data mechanics instead of reading core.Base/Provider's
full inherited method surface.

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-29T08:03:11Z @neo-opus-ada referenced in commit `c6c491b` - "fix(ai): observeData uses hierarchy-aware resolution + destroy cleanup (#12103)

observeData reached into `this.getDataConfig(leafPath)` directly, bypassing the
hierarchy-aware resolution every other data accessor uses: `Provider#getData` and
`createHierarchicalDataProxy` both resolve via `getOwnerOfDataProperty(fullPath)`
-> `owner.getDataConfig(propertyName)`. A leaf owned by a parent provider would
falsely return null. Now resolves via getOwnerOfDataProperty (extend, don't reinvent).

Also tracks each subscription's cleanup and releases it in a `destroy()` override,
mirroring `core.Base#observeConfig`'s `#configSubscriptionCleanups` contract: a
parent-owned Config outlives this instance, so the subscription must be torn down on
destroy rather than leaking.

Added hierarchy-resolution + destroy-cleanup regression specs (12/12 green).

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>"
- 2026-05-29T09:35:39Z @tobiu referenced in commit `1b378a0` - "feat(ai): aiConfig meta-leaf tree + Provider-based BaseConfig (#12103) (#12164)

* feat(ai): aiConfig meta-leaf tree + Provider-based BaseConfig (#12103)

Stage 1+2 of Epic #12101. BaseConfig now extends Neo.state.Provider; the six
config.template.mjs files use a single {env, default, parse} meta-leaf tree,
retiring the parallel defaultConfig + envBindings shape. Consumer codemod and
Env.mjs cleanup (Stages 3-6) are deferred to follow-up PRs.

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(ai): BaseConfig.load() skips redundant proxy-merge for .mjs overlays (#12103)

A `.mjs` config overlay default-exports the already-compiled config singleton
(createConfigProxy); its data + env layer are applied at construction. Passing
that reactive proxy to setData/Neo.clone/JSON.stringify crashed (the hierarchical
proxy enumerates instance members, not config leaves). Skip the no-op merge for
.mjs overlays (the legacy Neo.merge was a no-op too) and re-assert the env layer;
JSON overlays still merge their partial overrides. Fixes the CI config-load crash
that broke backup / WriteSideInvariant / workspaceSafety / BackupRestoreWipe.

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(ai): BaseConfig must not shadow core.Base set()/observeConfig() (#12103)

BaseConfig (extends Neo.state.Provider -> core.Base) overrode two inherited
public methods with incompatible signatures:

- `set(leafPath, value)` shadowed `core.Base#set(values)`, the canonical batch
  config setter. Removed entirely — the data-mutation primitive is the inherited
  `setData(path, value)` (Provider). Extend, don't reinvent.
- `observeConfig(leafPath, fn)` shadowed `core.Base#observeConfig(publisher,
  configName, fn)` — a cross-instance subscription API that `Provider#createBinding`
  itself calls (Provider.mjs:270). Renamed to `observeData(leafPath, fn)`.

Added a regression-guard spec asserting neither core method is shadowed. Updated
the spec to use setData/observeData. Root cause: relied on research-subagent
summaries of Provider's data mechanics instead of reading core.Base/Provider's
full inherited method surface.

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>

* fix(ai): observeData uses hierarchy-aware resolution + destroy cleanup (#12103)

observeData reached into `this.getDataConfig(leafPath)` directly, bypassing the
hierarchy-aware resolution every other data accessor uses: `Provider#getData` and
`createHierarchicalDataProxy` both resolve via `getOwnerOfDataProperty(fullPath)`
-> `owner.getDataConfig(propertyName)`. A leaf owned by a parent provider would
falsely return null. Now resolves via getOwnerOfDataProperty (extend, don't reinvent).

Also tracks each subscription's cleanup and releases it in a `destroy()` override,
mirroring `core.Base#observeConfig`'s `#configSubscriptionCleanups` contract: a
parent-owned Config outlives this instance, so the subscription must be torn down on
destroy rather than leaking.

Added hierarchy-resolution + destroy-cleanup regression specs (12/12 green).

Co-authored-by: neo-opus-ada <neo-opus-ada@neomjs.com>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-29T09:35:39Z @tobiu closed this issue

