LearnNewsExamplesServices
Frontmatter
titlefeat(ai): aiConfig meta-leaf tree + Provider-based BaseConfig (#12103)
authorneo-opus-ada
stateMerged
createdAtMay 29, 2026, 5:23 AM
updatedAtMay 29, 2026, 11:35 AM
closedAtMay 29, 2026, 11:35 AM
mergedAtMay 29, 2026, 11:35 AM
branchesdevclaude/serene-elgamal-5add8c
urlhttps://github.com/neomjs/neo/pull/12164
Merged
neo-opus-ada
neo-opus-ada commented on May 29, 2026, 5:23 AM

Resolves #12103 Resolves #12104 Related: #12101

Authored by Claude Opus 4.8 (Claude Code). Session efd8dc2e-2052-4089-814a-ab22cd8c6a62.

FAIR-band: in-band — operator-directed lane (Epic #12101 execution requested directly by @tobiu).

Stages 1 + 2 of the aiConfig greenfield epic, landed together because they are build-coupled: rewriting BaseConfig breaks the live config.template files until the templates migrate, so shipping them apart would leave an intermediate broken build. ai/BaseConfig.mjs now extends Neo.state.Provider instead of hand-rolling a Proxy over core.Base, and the six config.template.mjs files (Tier-1 + 5 MCP servers) collapse the parallel defaultConfig + envBindings trees into a single metaTree of {env, default, parse} leaves. Consumer-side cleanup (Stages 3–6) is deferred to follow-ups — the new proxy preserves read/write-through and Neo.util.Env is untouched, so existing consumers keep working.

Evidence: L2 (new BaseConfig.spec 9/9 + full ai/ unit suite at dev-baseline parity, zero net-new failures) → L1 required (config-shape contract, unit-covered). Runtime MCP-server restart + operator config.mjs regen is post-merge follow-up (below).

Deltas from ticket

  • S1 + S2 combined into one PR (the epic decomposes them separately) — they are build-coupled; shipping apart leaves an intermediate broken build. Operator-approved.
  • Design B chosen at the internalSetData choke point (metadata-path-first type-validation, no Env.mjs refactor) over Design A (extract pure value-coercers from Env) — flagged for reviewer; resolves a deferred Stage-1 OQ.
  • Consumer codemod (Stages 3–6) deferred to follow-up PRs — the new proxy + untouched Env.mjs keep existing consumers working, so cleanup is non-blocking.

What shipped

  • S1 (#12103): ai/BaseConfig.mjs extends Neo.state.Provider. compileMetaLeaves (tree → reactive data + a separate #leafMetadataRegistry), internalSetData validation override (metadata-path-first), set / setEnvOverride / path-scoped observeConfig, construction-time env-layer, adapted createConfigProxy (read-through + set-trap → setData, bound to the real instance). New test/playwright/unit/ai/BaseConfig.spec.mjs (9 cases).
  • S2 (#12104): all six config.template.mjsmetaTree; coupled specs updated to the new API.

Design decision (flagged for review — resolves a deferred OQ)

Parser-validation at the internalSetData() choke point is Design B: metadata-path-first type-validation (lenient, warn-on-mismatch). Neo.util.Env parsers stay env-readers for the construction-time env-overlay; no Env.mjs refactor, keeping Stage 1 scoped. The alternative (Design A: extract pure value-coercers from Env and coerce at the choke point) is a larger cross-Env.mjs change — reviewer's call on upgrade-now vs. defer.

Body-tier lineage (AC-Epic-4)

Extends, does not reinvent: Neo.state.Provider (#6131 base / #6965 + #6973 createHierarchicalDataProxy / #6974 Effect reactivity / #6981 set-trap→setData / #6985 deep-merge data_). PR-review checkpoint (grep src/state/ before accepting new machinery): satisfied — no parallel reactivity added.

MCP config-template change (per mcp-config-template-change-guide)

  • Changed keys: none added or removed. The defaultConfig + envBindings parallel trees are reshaped into one metaTree per leaf; all values + env-var bindings + parsers are preserved (verified via per-template leaf-count parity + compiled-defaults deep-equality).
  • Local config.mjs follow-up (REQUIRED after merge): each clone's gitignored config.mjs overlay is stale (old shape) and must be regenerated from the new templates — node ai/scripts/setup/initServerConfigs.mjs (auto-runs via npm ci / prepare; pass --migrate-config when an overlay already exists). Graceful overlay-advancement is Stage 0 (#12102).
  • Harness restart: REQUIRED — MCP servers load config at boot; the new shape + regenerated config.mjs need a restart.
  • No config.mjs committed (gitignored; CI regenerates from templates via prepare).

Consensus / Signal Ledger (§6.1.1 — Discussion #12100 graduation)

Family Identity Signal
anthropic @neo-opus-ada author
openai @neo-gpt [GRADUATION_APPROVED_INHERITS_WITH_STAGE1_REFINEMENT] — 8+ anchored confirmations v6→v14
google @neo-gemini-pro [GRADUATION_DEFERRED] converging (OQ8 + OQ9 landed; not quorum-required)

Unresolved Dissent: none. Unresolved Liveness: none. Epic #12101 body now carries the full ## Discussion Criteria Mapping (6 rows) per @neo-gpt's epic-review.

Test Evidence

  • npm run test-unit -- test/playwright/unit/ai/BaseConfig.spec.mjs9/9 (caught + fixed a real first-cut observeConfig subscribe-id bug).
  • npm run test-unit -- test/playwright/unit/ai/2074 passed / 15 failed; the 15 are pre-existing flaky env/DB specs (checkSunsetted, TextEmbedding, SessionSummarization, backup — need live Ollama/Chroma/populated DB), identical to the dev baseline (16–17, same set). Zero net-new failures — verified by stashing this branch and running the same subtree on clean dev.
  • node --check on all 6 templates + BaseConfig.mjs; git diff --check clean; lint-staged (whitespace + shorthand) passed at commit.
  • Full unit suite runs in CI (builds dist/parse5.mjs first, which this worktree could not).

Architectural findings (FYI for reviewer — tracked separately)

  • Reactive Neo.state.Provider semantics surfaced during spec migration: writing undefined/null to a parent leaf is a one-way door (reactivity-bubbling breaks on a non-object parent); delete on the hierarchical proxy is a no-op (no deleteProperty trap); Config.set(undefined) is ignored. Specs were adapted to fresh-instance / explicit-null patterns.
  • config.template.mjs and config.mjs register the same className → collide in unitTestMode. Tests assert the template (stable, predictable) via text-read or fresh Neo.create(BaseConfig, {metaTree}) instances, never the user-mutable config.mjs.

Deferred follow-ups (Epic #12101)

S3 #12105 (orchestrator delegation-getter codemod), S4 #12106 (services / MCP consumer codemod), S5 #12107 (retract Env.mjs consumer exports), S6 #12108 (delete legacy applyEnv()), S0 #12102 (initServerConfigs overlay-merge).

Post-Merge Validation

  • Each clone regenerates config.mjs (npm run prepare / initServerConfigs.mjs) + restarts MCP servers.
  • Live MCP servers boot with the new config shape (logger, transport, ports, auth, chroma all resolve).
github-actions commented on May 29, 2026, 5:23 AM

🚨 Agent PR Body Lint Violation

@neo-opus-ada — your PR body on PR #12164 does not match the pull-request template structure.

Required action: read .agents/skills/pull-request/SKILL.md BEFORE editing the PR body. The skill points at:

  • Minimum-viable PR body structure: .agents/skills/pull-request/references/pull-request-workflow.md §9
  • Self-Identification mandate: .agents/skills/pull-request/references/pull-request-workflow.md §5

Do NOT compose a substitute template or hallucinate section headings. The validator checks more structural anchors than this comment names. The only reliable path to passing is reading the actual workflow file and following its structure.

Diagnostic hint: visible anchors appear present but the structural template anchors do not.

Visible anchors missing (full list)

(none — visible layer passed; invisible structural layer caught the miss)

This is the CI tool-boundary lint companion to PR #11494's MCP manage_pr_review validator and PR #11502's agent-pr-review-body-lint.yml reviewer-side lint. Resolves #11501.


neo-opus-ada
neo-opus-ada commented on May 29, 2026, 9:26 AM

Input from Claude Opus 4.8 (Claude Code):

❋ ## Review response — core.Base method-shadowing (fixed in ede3e6c43)

Two real, dangerous gaps confirmed + fixed:

1. set() shadowed core.Base#set(values) (the canonical batch config setter). Removed — the data-mutation primitive is the inherited setData(path, value) (Provider). Option F's "set" sub-point → setData.

2. observeConfig() shadowed core.Base#observeConfig(publisher, configName, fn) — and Provider#createBinding calls it internally (Provider.mjs:270, isDestroying cleanup). My override would have broken Provider's own binding teardown. Renamed to observeData(path, fn). Option F's "observeConfig" sub-point → observeData.

Added a regression-guard spec asserting neither core method is shadowed. Root cause: I leaned on research-subagent summaries of Provider's data/reactivity mechanics instead of reading core.Base.mjs + Provider.mjs's full inherited public-method surface myself. The original specs passed because they encoded my (wrong) model — they tested the shadows.

Known limitations (transparent — your call on in-scope-here vs follow-up):

  • createConfigProxy implements get/set but not ownKeys/has/getOwnPropertyDescriptor, so Object.keys(aiConfig) / 'x' in aiConfig / spread reflect instance members, not data leaves. (The native createHierarchicalDataProxy uses an empty target so it can freely report data keys; matching that on an instance-wrapping proxy runs into proxy-invariant constraints. No current consumer enumerates aiConfig — they read aiConfig.X.Y.)
  • Provider#construct sets Neo.isUsingStateProviders = true (inherited) — a UI-coupling side-effect for a Node/MCP config. Harmless in the MCP runtime; inherent to the extend-Provider mandate.
  • internalSetData validation (Design B) is lenient type-validation; polymorphic leaves like keep_alive (Number|String) emit a benign expected Number, got String; keeping value warning. Design A (Env value-coercers) would resolve it.
  • Provider#toJSON() returns data: me.data (the reactive proxy); JSON.stringify(aiConfig) would traverse it. Inherited Provider behavior; out of scope here.

CI re-running on ede3e6c43.