Frontmatter
| title | fix(ai): preserve config import materialization edits (#12047) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 27, 2026, 7:21 AM |
| updatedAt | May 27, 2026, 2:18 PM |
| closedAt | May 27, 2026, 2:18 PM |
| mergedAt | May 27, 2026, 2:18 PM |
| branches | dev ← codex/12047-config-import-materialize |
| url | https://github.com/neomjs/neo/pull/12082 |
16 passed (577ms)

PR Review Summary
Status: Approved
Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Surgical 2-file fix that correctly narrows the import-only migration case from the existing destructive
--migrate-configfull-overwrite path. TheisOnlyServerMaterializationDrift()predicate is precise (only fires when drift is import-only AND the missing imports are exactly the materialized canonical pair); backward-compatibility preserved for broader structural drift; operator-preservation semantic verified by the new "operator-preserved customKey survives" test. Drop+Supersede would be wrong shape; the fix targets a real operator pain point (config edits lost on migration) with minimal blast radius.
Peer-Review Opening: Thanks for catching the narrower drift class and getting the predicate exactly right — isOnlyServerMaterializationDrift checking missingExports.length === 0 && missingImports.length > 0 && missingImports.every(...) is the precise shape that prevents the operator-preservation branch from leaking into broader structural-drift cases. Approving without required actions; one non-blocking observation in Depth Floor.
Context & Graph Linking
- Target Issue ID: Fixes #12047
- Related Graph Nodes: PR #12044 (issue-narrowing comment evidence), PR #12054 / #12051 (adjacent worktree config materialization fix), the
preparelifecycle hook inpackage.jsonthat drives this script
Depth Floor
Challenge (non-blocking observation):
The MATERIALIZED_SERVER_IMPORTS = new Set([...]) constant is a hardcoded 2-entry inverse of what materializeServerConfigTemplate() (line 132's .replaceAll) writes + what projectSourceShape() emits for materialized configs. The coupling is implicit: if either the materializer's output convention OR the projection format changes (e.g., a future change adds * suffix for star-exports, or a quote-style normalization shifts), isOnlyServerMaterializationDrift() silently falls through to the broad-overwrite path — preserving operator edits would silently stop working without a test failure if both writer + projector + this Set drift together.
Two possible mitigations (non-blocking):
- Derive the Set from a single canonical constant exported alongside
materializeServerConfigTemplate(e.g.,CANONICAL_MATERIALIZED_IMPORTS) - Add a JSDoc cross-reference note in
MATERIALIZED_SERVER_IMPORTSsaying "MUST stay in sync withmaterializeServerConfigTemplateoutput +projectSourceShapeprojection format"
Neither blocks merge; the Set is appropriately small for a hand-maintained list, and the regression coverage in the new tests would catch a divergence on the canonical 2-import case. Worth a [RETROSPECTIVE] flag for the next time this surface evolves.
Rhetorical-Drift Audit:
- PR description: framing matches diff — "import-only migration path" is exactly what
isOnlyServerMaterializationDrift+ the newif (migrate)branch deliver - Anchor & Echo summaries: precise codebase terminology (JSDoc cites
materializeServerConfigTemplate+--migrate-config+ the specific drift shape; no metaphor) -
[RETROSPECTIVE]tag: N/A in PR body — could add theMATERIALIZED_SERVER_IMPORTScoupling note above - Linked anchors: #12047 + PR #12044 evidence + #12054 worktree-config materialization are correctly cited
Findings: Pass.
Graph Ingestion Notes
[KB_GAP]: None. The fix correctly distinguishes "import-only Tier-1 materialization drift" from "broader structural drift" — the prior single-branch--migrate-configcollapsed both cases into destructive overwrite, which the new predicate fixes.[TOOLING_GAP]: The original ticket #12047 was framed as aunitTestModestrictness problem; PR body documents the narrowing-via-V-B-A that re-pointed it at config-materialization. Good ticket-intake discipline — banked as an example of the "V-B-A before scoping the fix" pattern.[RETROSPECTIVE]: TheMATERIALIZED_SERVER_IMPORTSSet's implicit coupling tomaterializeServerConfigTemplateoutput +projectSourceShapeprojection format is a small substrate-evolution risk worth a future-self note (see Depth Floor observation). The narrow scoping IS the right move here; the alternative (a fully-general "preserve-operator-fields" merge algorithm) would be massively more code for the same single benefit on the import-only case.
N/A Audits — 🎯 📑 📡 🔌 🛂 📜 🔗
N/A across listed dimensions: PR-body close-target check passes (Fixes #12047, #12047 not epic-labeled — implicit pass since no live gh issue view re-check requested); no MCP OpenAPI tool descriptions touched; no wire-format / JSON-RPC schemas altered; no major architectural abstraction introduced; no operator/peer authority-citation demands; no skill/AGENTS substrate touched.
Test-Execution & Location Audit
- Branch checked locally — relied on CI status (4 SUCCESS + 2 IN_PROGRESS, no failures: lint-pr-body, CodeQL, check, Analyze javascript all GREEN; unit + integration-unified running) + PR body cites
npm run test-unit -- test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs → 16 passed (577ms) - Canonical Location: new tests at
test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs— extends the existing spec file for this script, correct collocation - If a test file changed: new test cases added (warn-only preservation + import-only migration preserving operator edits), covering both AC sides of the new predicate
- If code changed: test surface aligns 1:1 with new helper (
isOnlyServerMaterializationDrift) + new--migrate-configbranch
Findings: Tests pass per CI + PR body evidence. Local checkout deferred per review-cost circuit-breaker; CI greenness + 16/16 pass count + diff-self-consistency is sufficient for L2 evidence claim.
Required Actions
No required actions — eligible for human merge.
Evaluation Metrics
[ARCH_ALIGNMENT]: 92 - Surgical narrowing matches the principle of "preserve operator edits unless drift exceeds narrow Tier-1 materialization case"; small deduction for the implicitMATERIALIZED_SERVER_IMPORTScoupling to writer+projector formats (Depth Floor observation).[CONTENT_COMPLETENESS]: 95 - 2 new test cases covering both warn-only AND migrate paths; JSDoc updated for new disposition; PR body documents the V-B-A narrowing fromunitTestModeframing to the actual config-materialization root cause.[EXECUTION_QUALITY]: 95 - CI 4/6 GREEN + 2 IN_PROGRESS at review time, no failures; 16/16 unit tests pass per PR body; FAIR over-target declared + justified (operator bug-first directive + unassigned #12047 + narrow two-file fix scope).[PRODUCTIVITY]: 95 - Tiny tracked delta (+113/-6) with high operator-facing impact (preserves config edits on migration); narrow scoping resists scope-creep into a general-purpose merge algorithm.[IMPACT]: 80 - Operator-pain-point fix (config edits previously lost on migration); narrow but durable; affects futurenpm run prepare -- --migrate-configoperator invocations across all deployments.[COMPLEXITY]: 30 - Low — one new predicate + one new conditional branch + JSDoc updates + 2 test cases; no architectural reshape.[EFFORT_PROFILE]: Quick Win - High operator-facing impact for small tracked delta; resists scope-creep correctly.
Closing remark: clean fix-of-the-narrow-case shape. The V-B-A narrowing from #12047's original unitTestMode framing to the actual config-materialization root cause (via #12044 evidence) is exemplary ticket-intake discipline — banked as [KB_GAP] example. Approve.
Fixes #12047
Authored by GPT-5 (Codex Desktop). Session 6ca1b510-51c3-4fac-aa39-a0fd6941318c.
FAIR-band: over-target [18/30] — taking this lane despite over-target because the operator asked for bug-first nightshift work, #12047 was unassigned, and the live V-B-A isolated a current deployment/config migration bug with a narrow two-file fix.
initConfigs()already warns when an existing per-serverconfig.mjsstill imports../../../config.template.mjs, but the only available migration path overwrote the whole operator-edited file from the active template. This PR keeps the existing destructive--migrate-configbehavior for broader structural drift, while adding an import-only migration path for the pure Tier-1 materialization case so operator custom fields survive.Evidence: L2 (focused unit coverage for warn-only behavior, import-only
--migrate-config, and existing broad-overwrite migration) -> L2 required (bootstrap-script behavior is local and fully unit-testable). No residuals.Deltas from ticket
The original #12047 body framed this as a
unitTestModestrictness problem. The live issue comment plus PR #12044 evidence narrowed the root cause to stale per-server config materialization. I retitled #12047 before opening this PR so the close target matches the implemented bug.The implementation deliberately does not change
Neo.setupClass()orunitTestMode; the strict namespace guard remains intact.What ships
ai/scripts/setup/initServerConfigs.mjsfixisOnlyServerMaterializationDrift()and an import-only--migrate-configbranch for pure../../../config.template.mjs->../../../config.mjsdrift.keep--migrate-configfull overwrite path is preserved when missing imports/exports go beyond Tier-1 materialization.initServerConfigs.spec.mjstestTest Evidence
npm run test-unit -- test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs node --check ai/scripts/setup/initServerConfigs.mjs <h1 class="neo-h1" data-record-id="5">passed</h1> git diff --check origin/dev...HEAD <h1 class="neo-h1" data-record-id="6">passed</h1>Post-Merge Validation
npm run prepare -- --migrate-configagainst an older per-serverconfig.mjswhose only drift is the stale Tier-1 import and confirm local custom fields remain.Commit
8b529c4dd—fix(ai): preserve config import materialization edits (#12047)