Skip to content

neo4j-client correctness: read-only Cypher + bi-temporal supersession + merge timestamp#28

Merged
stevepridemore merged 0 commit into
mainfrom
neo4j-client-correctness-fixes
May 10, 2026
Merged

neo4j-client correctness: read-only Cypher + bi-temporal supersession + merge timestamp#28
stevepridemore merged 0 commit into
mainfrom
neo4j-client-correctness-fixes

Conversation

@stevepridemore
Copy link
Copy Markdown
Owner

Summary

Three correctness fixes to src/shared/neo4j-client.ts surfaced in the same code-review session, all bundled because they touch the same file's data-integrity layer. Closes the two HIGH-severity findings + one MEDIUM the reviews uncovered.

Cypher safety (closes graph_cypher footgun)

The graph_cypher MCP tool description claimed "Enforced read-only via Neo4j executeRead()" but the underlying code path (run() → WRITE session) didn't enforce it. An admin LLM (or a prompt-injected admin session) could execute DETACH DELETE and wipe the graph despite the read-only contract.

  • New runReadOnly() method using session.executeRead() with a 30s transaction timeout. The timeout closes Gemini's "Cartesian product DoS" finding as a side benefit.
  • executeCypher() now uses runReadOnly(). The tool description is now accurate without changing.

Independently flagged by Copilot and Gemini — two-reviewer confirmation.

Bi-temporal supersession (closes silent data corruption)

prompts/dream-nightly.md:189 and prompts/graph-capture.md:59 documented that graph_relate automatically invalidates predecessor edges when a fact changes. The code wasn't doing it. Every "switched from X to Y" scenario silently produced two coexisting "current" edges (both invalid_at IS NULL), corrupting the bi-temporal model and skewing query results.

  • createRelationship now invalidates sibling edges of the same (from, type) pointing to a different target when valid_at is supplied
  • Trigger is gated on valid_at presence so multi-valued relationships (WORKS_ON, USES_TECH) still allow concurrent edges — only explicit temporal updates trigger supersession
  • valid_at (and invalid_at via the supersession path) now stored as Neo4j datetime() instead of raw ISO strings, so future temporal queries work

Caught by Copilot's review.

merge() timestamp typo

Lines 1482 and 1516 set r.last_seen on consolidated relationships, but every other relationship path uses r.last_confirmed and applyDecay() reads r.last_confirmed. Merged edges therefore decayed as if they'd never been touched.

One-character fix per occurrence. Caught by Copilot.

Test plan

  • npm run build clean
  • npm run test:coverage:local — 13 new tests passing (5 read-only enforcement + 8 bi-temporal supersession), no regressions on existing 133
  • Coverage: 65.29 → 65.88% statements, 67.29 → 67.78% lines
  • CI runs against fresh Neo4j service container and reports same numbers
  • Deploy to production; verify /graph-stats still works (regression check on read paths) and a small graph_relate write succeeds

Why combined into one PR

Originally planned as two PRs but the parallel-builder run produced a working tree with both fixes already entangled. Both are correctness fixes to the same file's data-integrity layer; the granularity loss is minimal (one commit message vs two telling the same story). Tests prove they compose correctly.

🤖 Generated with Claude Code

@stevepridemore stevepridemore merged this pull request into main May 10, 2026
2 checks passed
@stevepridemore stevepridemore deleted the neo4j-client-correctness-fixes branch May 10, 2026 03:46
stevepridemore added a commit that referenced this pull request May 10, 2026
… + merge timestamp fix (#28)

Three correctness fixes to src/shared/neo4j-client.ts surfaced in
the same code-review session, all touching the data-integrity layer.

Cypher safety
- New runReadOnly() method using session.executeRead() with a 30s
  transaction timeout. executeCypher() now uses runReadOnly(),
  making the graph_cypher MCP tool's "Enforced read-only via
  Neo4j executeRead()" claim actually true. Previously the tool
  description was a guarantee that didn't hold — admin LLM could
  execute DETACH DELETE despite the read-only contract.

Bi-temporal supersession
- createRelationship now invalidates predecessor edges of the same
  (from, type) pointing to a different target when valid_at is
  supplied. The dream prompts (dream-nightly.md, graph-capture.md)
  documented this behaviour but the code wasn't doing it — every
  "switched from X to Y" scenario silently produced two coexisting
  "current" edges (both invalid_at IS NULL). Trigger is gated on
  valid_at presence so multi-valued relationships (WORKS_ON,
  USES_TECH) still allow concurrent edges.
- valid_at is now stored as a Neo4j datetime() rather than a raw
  ISO string in the property map. Same for invalid_at via the
  supersession path. Future temporal queries (WHERE r.valid_at <
  datetime()) now work correctly.

merge() timestamp typo
- Lines 1482 and 1516 set r.last_seen on consolidated relationships,
  but every other relationship code path uses r.last_confirmed and
  applyDecay() reads r.last_confirmed. Net: merged edges decayed
  as if never touched. Fixed to set r.last_confirmed.

13 new tests across read-only enforcement (5) and bi-temporal
supersession (8). Coverage 65.29 → 65.88% statements, 67.29 →
67.78% lines. All existing tests still pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant