Context
ai/mcp/server/memory-core/config.template.mjs:122 storagePaths.graph is an inline-process.env leaf default — an ADR 0019 §3 A4 instance ([live: #12451]), one of the 3 remaining baselined by the SSOT lint (#12474):
graph: leaf(process.env.UNIT_TEST_MODE === 'true' ? ':memory:' : path.resolve(cwd, '.neo-ai-data/sqlite/memory-core-graph.sqlite'), 'NEO_MEMORY_DB_PATH', 'string')
The static sub-case of #12451's declarative-reshape half. Sibling to the chroma database leaf (#12480, shipped via #12481). Dynamic collection-names are a separate harder leaf (see Scope).
The Fix — the config resolves the active path BY CONSTRUCTION (ADR 0019); consumers unchanged
Design correction (ADR 0019 read-gate + consumer V-B-A): my first draft copied the chroma "consumer selects via the toggle" shape. That is wrong here — chroma had a single consumer (ChromaManager); storagePaths.graph is read at ~10 sites (GraphService, DatabaseService, Server, 5 maintenance scripts, analyzeNlTelemetry). Per-consumer selection = 10 edit-sites + bleed-risk if one is missed. ADR 0019 §3 (A4 / A8 → "test-mode by construction") + §5 ("read the resolved leaf at the use site") require the selection to live in the config, so every consumer reads storagePaths.graph unchanged:
storagePaths: {
graphProd : leaf(path.resolve(cwd, '.neo-ai-data/sqlite/memory-core-graph.sqlite'), 'NEO_MEMORY_DB_PATH', 'string'),
graphTest : leaf(':memory:', 'NEO_MEMORY_DB_PATH_TEST', 'string'),
useTestDatabase: leaf(false, 'UNIT_TEST_MODE', 'boolean')
}
formulas: {
'storagePaths.graph': data => data.storagePaths.useTestDatabase ? data.storagePaths.graphTest : data.storagePaths.graphProd
}Consumers keep reading AiConfig.storagePaths.graph (now the formula result) — zero consumer edits, test-mode resolved by construction, no inline process.env (A4-clean), and the formula selects already-resolved leaves (it does not re-read env → not A7). Maintenance scripts run without UNIT_TEST_MODE → resolve graphProd (correct — they operate on the real DB).
Why NOT env-relocation (supersedes #12451's reshape note): #12451's umbrella body + the lint BASELINE note prescribe relocating the test override to playwright.config.unit.mjs (env). That is the shape the chroma reshape rejected — and worse here, playwright.config.unit.mjs in-process env mutation doesn't reliably reach workers (#12480), and this path has no fail-closed guard. By-construction config resolution is the safe shape.
Feasibility to verify during impl: that a nested-path formula key ('storagePaths.graph') resolves through the hierarchical proxy. If unsupported, fall back to a sibling resolved key + a single (scripted) consumer migration — but the by-construction principle holds either way.
Contract Ledger
| Surface |
Type |
Contract |
storagePaths.graphProd |
config leaf |
Prod graph sqlite path. Literal default; env override NEO_MEMORY_DB_PATH. No inline process.env. |
storagePaths.graphTest |
config leaf |
Test path :memory:. Literal default; env override NEO_MEMORY_DB_PATH_TEST. |
storagePaths.useTestDatabase |
config leaf |
Selection toggle. Default false; env UNIT_TEST_MODE (boolean). |
storagePaths.graph |
formula |
Reactive: useTestDatabase ? graphTest : graphProd. Consumers read this unchanged — the single resolution point. |
| (consider) fail-closed guard |
invariant |
DatabaseService/GraphService refuse graphProd under UNIT_TEST_MODE — defense-in-depth, parity with chroma (which this case lacks). |
Acceptance Criteria
Scope
STATIC graph DB-path only. The dynamic collection-names (collections.memory / collections.session — NEO_MEMORY_COLLECTION_NAME / NEO_SESSION_COLLECTION_NAME, test-…-${Date.now()}-${Math.random()} per-run uniqueness) need a per-worker bootstrap (a static config leaf cannot provide runtime uniqueness) → a separate, harder leaf, deferred (as #12451's "Wrinkle" section flags).
Refs #12451 (reshape umbrella). Part of Epic #12456 (AiConfig reactive Provider SSOT cleanup). Design per ADR 0019 §3 (A4/A8) + §5.
Authored by Claude Opus 4.8 (Claude Code), /lead-role. MC session f5432f03-d2b6-4bc0-a7b5-9fbdafe4b7b9.
Context
ai/mcp/server/memory-core/config.template.mjs:122storagePaths.graphis an inline-process.envleaf default — an ADR 0019 §3 A4 instance ([live: #12451]), one of the 3 remaining baselined by the SSOT lint (#12474):graph: leaf(process.env.UNIT_TEST_MODE === 'true' ? ':memory:' : path.resolve(cwd, '.neo-ai-data/sqlite/memory-core-graph.sqlite'), 'NEO_MEMORY_DB_PATH', 'string')The static sub-case of #12451's declarative-reshape half. Sibling to the chroma database leaf (#12480, shipped via #12481). Dynamic collection-names are a separate harder leaf (see Scope).
The Fix — the config resolves the active path BY CONSTRUCTION (ADR 0019); consumers unchanged
Design correction (ADR 0019 read-gate + consumer V-B-A): my first draft copied the chroma "consumer selects via the toggle" shape. That is wrong here — chroma had a single consumer (
ChromaManager);storagePaths.graphis read at ~10 sites (GraphService,DatabaseService,Server, 5 maintenance scripts,analyzeNlTelemetry). Per-consumer selection = 10 edit-sites + bleed-risk if one is missed. ADR 0019 §3 (A4 / A8 → "test-mode by construction") + §5 ("read the resolved leaf at the use site") require the selection to live in the config, so every consumer readsstoragePaths.graphunchanged:storagePaths: { graphProd : leaf(path.resolve(cwd, '.neo-ai-data/sqlite/memory-core-graph.sqlite'), 'NEO_MEMORY_DB_PATH', 'string'), // prod, declarative graphTest : leaf(':memory:', 'NEO_MEMORY_DB_PATH_TEST', 'string'), // test, declarative useTestDatabase: leaf(false, 'UNIT_TEST_MODE', 'boolean') // toggle (env-arg form) } // `graph` resolves by construction — a genuine reactive computed value (ADR 0019 §5.3), reactive on the toggle: formulas: { 'storagePaths.graph': data => data.storagePaths.useTestDatabase ? data.storagePaths.graphTest : data.storagePaths.graphProd }Consumers keep reading
AiConfig.storagePaths.graph(now the formula result) — zero consumer edits, test-mode resolved by construction, no inlineprocess.env(A4-clean), and the formula selects already-resolved leaves (it does not re-read env → not A7). Maintenance scripts run withoutUNIT_TEST_MODE→ resolvegraphProd(correct — they operate on the real DB).Why NOT env-relocation (supersedes #12451's reshape note): #12451's umbrella body + the lint BASELINE note prescribe relocating the test override to
playwright.config.unit.mjs(env). That is the shape the chroma reshape rejected — and worse here,playwright.config.unit.mjsin-process env mutation doesn't reliably reach workers (#12480), and this path has no fail-closed guard. By-construction config resolution is the safe shape.Feasibility to verify during impl: that a nested-path formula key (
'storagePaths.graph') resolves through the hierarchical proxy. If unsupported, fall back to a sibling resolved key + a single (scripted) consumer migration — but the by-construction principle holds either way.Contract Ledger
storagePaths.graphProdNEO_MEMORY_DB_PATH. No inlineprocess.env.storagePaths.graphTest:memory:. Literal default; env overrideNEO_MEMORY_DB_PATH_TEST.storagePaths.useTestDatabasefalse; envUNIT_TEST_MODE(boolean).storagePaths.graphuseTestDatabase ? graphTest : graphProd. Consumers read this unchanged — the single resolution point.DatabaseService/GraphServicerefusegraphProdunderUNIT_TEST_MODE— defense-in-depth, parity with chroma (which this case lacks).Acceptance Criteria
storagePaths.graphresolves by construction (formula on the toggle); no inlineprocess.env(A4-clean); the ~10 consumers are unchanged.UNIT_TEST_MODE,storagePaths.graph===:memory:for every consumer; it cannot resolve the prod sqlite.UNIT_TEST_MODE) resolvegraphProd— unaffected.NEO_MEMORY_DB_PATHrow removed from the SSOT-lintBASELINE(lint green; no new violation / stale row).DatabaseService/GraphServicefail-closed guard (defense-in-depth — this case currently has none).Scope
STATIC graph DB-path only. The dynamic collection-names (
collections.memory/collections.session—NEO_MEMORY_COLLECTION_NAME/NEO_SESSION_COLLECTION_NAME,test-…-${Date.now()}-${Math.random()}per-run uniqueness) need a per-worker bootstrap (a static config leaf cannot provide runtime uniqueness) → a separate, harder leaf, deferred (as #12451's "Wrinkle" section flags).Refs #12451 (reshape umbrella). Part of Epic #12456 (AiConfig reactive Provider SSOT cleanup). Design per ADR 0019 §3 (A4/A8) + §5.
Authored by Claude Opus 4.8 (Claude Code), /lead-role. MC session
f5432f03-d2b6-4bc0-a7b5-9fbdafe4b7b9.