---
id: 12491
title: 'config.template graph DB-path leaf: declarative via safe-by-construction toggle (memory-core NEO_MEMORY_DB_PATH)'
state: CLOSED
labels:
  - enhancement
  - ai
assignees:
  - neo-claude-opus
createdAt: '2026-06-04T11:10:14Z'
updatedAt: '2026-06-04T12:44:16Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12491'
author: neo-claude-opus
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-06-04T12:44:16Z'
---
# config.template graph DB-path leaf: declarative via safe-by-construction toggle (memory-core NEO_MEMORY_DB_PATH)

## 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):

```js
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:

```js
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.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`.


## Timeline

- 2026-06-04T11:10:15Z @neo-claude-opus added the `enhancement` label
- 2026-06-04T11:10:15Z @neo-claude-opus added the `ai` label
- 2026-06-04T11:10:16Z @neo-claude-opus assigned to @neo-claude-opus
- 2026-06-04T11:49:53Z @neo-claude-opus cross-referenced by PR #12494
- 2026-06-04T11:53:50Z @neo-claude-opus cross-referenced by #12451
- 2026-06-04T12:01:26Z @neo-claude-opus referenced in commit `d2b9c7d` - "test(ai): derive lintConfigTemplateSsot baselined-fixture from BASELINE[0] (#12491)

Dropping the NEO_MEMORY_DB_PATH baseline row (this PR's reshape) broke the lint's own 'baselined violation is suppressed' spec, which hardcoded that env — the same churn chroma hit (NEO_CHROMA_DATABASE). Derive the fixture from a live BASELINE row so future reshapes don't break it."
- 2026-06-04T12:44:16Z @tobiu referenced in commit `dc93ead` - "refactor(ai): resolve memory-core graph DB-path declaratively via a config formula (#12491) (#12494)

* refactor(ai): resolve memory-core graph DB-path declaratively via a config formula (#12491)

Replace the inline process.env.UNIT_TEST_MODE ? ':memory:' : prod leaf ternary (ADR 0019 §3 A4) in storagePaths.graph with declarative graphProd/graphTest leaves + a useTestDatabase toggle; resolve the active storagePaths.graph by construction via a reactive Provider formula. The ~10 graph-path consumers read the single resolved value unchanged. Drops the NEO_MEMORY_DB_PATH SSOT-lint baseline row.

* test(ai): derive lintConfigTemplateSsot baselined-fixture from BASELINE[0] (#12491)

Dropping the NEO_MEMORY_DB_PATH baseline row (this PR's reshape) broke the lint's own 'baselined violation is suppressed' spec, which hardcoded that env — the same churn chroma hit (NEO_CHROMA_DATABASE). Derive the fixture from a live BASELINE row so future reshapes don't break it.

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-06-04T12:44:16Z @tobiu closed this issue
- 2026-06-04T12:53:32Z @neo-claude-opus cross-referenced by #12499
- 2026-06-04T13:06:46Z @neo-claude-opus cross-referenced by PR #12500

