LearnNewsExamplesServices
Frontmatter
id12491
titleconfig.template graph DB-path leaf: declarative via safe-by-construction toggle (memory-core NEO_MEMORY_DB_PATH)
stateClosed
labels
enhancementai
assigneesneo-opus-grace
createdAtJun 4, 2026, 1:10 PM
updatedAtJun 4, 2026, 2:44 PM
githubUrlhttps://github.com/neomjs/neo/issues/12491
authorneo-opus-grace
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 4, 2026, 2:44 PM

config.template graph DB-path leaf: declarative via safe-by-construction toggle (memory-core NEO_MEMORY_DB_PATH)

neo-opus-grace
neo-opus-grace commented on Jun 4, 2026, 1:10 PM

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'), // 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 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

  • storagePaths.graph resolves by construction (formula on the toggle); no inline process.env (A4-clean); the ~10 consumers are unchanged.
  • Safe-by-construction: under UNIT_TEST_MODE, storagePaths.graph === :memory: for every consumer; it cannot resolve the prod sqlite.
  • Maintenance scripts (run without UNIT_TEST_MODE) resolve graphProd — unaffected.
  • NEO_MEMORY_DB_PATH row removed from the SSOT-lint BASELINE (lint green; no new violation / stale row).
  • (consider) DatabaseService/GraphService fail-closed guard (defense-in-depth — this case currently has none).
  • #12451 umbrella body updated: the env-relocation reshape note is superseded by by-construction resolution.
  • Nested-path-formula feasibility verified (or the documented fallback applied).

Scope

STATIC graph DB-path only. The dynamic collection-names (collections.memory / collections.sessionNEO_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.

tobiu referenced in commit dc93ead - "refactor(ai): resolve memory-core graph DB-path declaratively via a config formula (#12491) (#12494) on Jun 4, 2026, 2:44 PM
tobiu closed this issue on Jun 4, 2026, 2:44 PM