diff --git a/.cursor/skills/mcp-memnet/SKILL.md b/.cursor/skills/mcp-memnet/SKILL.md index 5cb8cc2d..17b7fb8f 100644 --- a/.cursor/skills/mcp-memnet/SKILL.md +++ b/.cursor/skills/mcp-memnet/SKILL.md @@ -40,7 +40,7 @@ session_open(map) → cue / find → pin_map → reason → mutate → pin_map 2. **Cue** — `kind` / `locators` (`qname=`, `path=`, `goal=`, …) / `keyword` / nickname `cue`. Empty cue = **session outline** (0.11). Ego unknown: `find(limit=…)` then `pin_map` from labels+props. Prefer one live `TSK_*`. When MATCH_L \(|Q|>1\), CueConflict — do not pick one root. A locator/keyword miss is CueMiss / Peak_L, not CueConflict. 3. **`pin_map`** — one \(S\) per generate. MCP `session=` selects the stratum. Drop the prior map next turn. leftover `anchor=` / `anchors=` are leftover nicknames. 4. **`mutate`** — sparse GraphElement `CREATE` / `MATCH`…`SET`/`DELETE`. No leftover `id:'NEW'` mint. Under RSV pass `llm_id`. -5. Persist if needed: `session_save` (file) or live cabinet (0.7 Agens / 0.14 Neo4j). Expire-time save is opt-in (`MEMNET_SAVE_ON_EXPIRE`). Agents MUST NOT talk Bolt. +5. Persist if needed: `session_save` (file) or AgensGraph adapter (0.7: adapter + operator round trip, no runtime caller). Neo4j is retired. Expire-time save is opt-in (`MEMNET_SAVE_ON_EXPIRE`). Agents MUST NOT talk Bolt. **MCP missing:** skip MemNet; plain Markdown only. diff --git a/.cursor/skills/mcp-memnet/references/mcp-policy.md b/.cursor/skills/mcp-memnet/references/mcp-policy.md index c31b855f..4627d2c1 100644 --- a/.cursor/skills/mcp-memnet/references/mcp-policy.md +++ b/.cursor/skills/mcp-memnet/references/mcp-policy.md @@ -8,7 +8,7 @@ Cursor (stdio) → memnet-mcp - Product **0.19.3**. Cue then `pin_map`. Write **`mutate`**. leftover `add`/`update` / `query_warm` / `anchor=` named leftover. - Multitask **MUST NOT** use in-process MCP for a shared session. -- Live Agens claimed (0.7); Neo4j live claimed (0.14); RSV + Path-B ingest + `snap_model` + `export_pin_map` shipped. +- AgensGraph 0.7 is adapter + operator round trip (no runtime caller). Neo4j is retired (#187). RSV + Path-B ingest + `snap_model` + `export_pin_map` shipped. - Novel-writer MCP is dropped. ## mcp.json (this repo / local) diff --git a/.cursor/skills/mcp-memnet/references/tool-parameters.md b/.cursor/skills/mcp-memnet/references/tool-parameters.md index 9a3105d2..f92d5758 100644 --- a/.cursor/skills/mcp-memnet/references/tool-parameters.md +++ b/.cursor/skills/mcp-memnet/references/tool-parameters.md @@ -59,8 +59,8 @@ Parse **`stdout`**. Join `wire_lines` with `\n`. | `MEMNET_MCP_TRANSPORT` | `tcp` when sharing a serve graph | | `MEMNET_SERVE_HOST` / `MEMNET_SERVE_PORT` | Serve bind (library default `127.0.0.1:18765`) | | `MEMNET_SESSION` | Default session after open/load | -| `MEMNET_AGENSGRAPH_URL` | Live Agens (0.7) | -| `MEMNET_NEO4J_URL` | Neo4j extra; live claimed **0.14**. Both URLs need `MEMNET_DURABLE_BACKEND` | +| `MEMNET_AGENSGRAPH_URL` | AgensGraph adapter (0.7: operator round trip, no runtime caller) | +| `MEMNET_NEO4J_*` | Retired (#187). Ignored. Engine starts | ## wire_lines (product) diff --git a/.cursor/skills/memnet-multitask/SKILL.md b/.cursor/skills/memnet-multitask/SKILL.md index 8f227e08..a050e627 100644 --- a/.cursor/skills/memnet-multitask/SKILL.md +++ b/.cursor/skills/memnet-multitask/SKILL.md @@ -110,8 +110,8 @@ Path-B: **`ingest_*`** into the current session (locator ids; **no** leftover NE | Neighbourhood RSV | **Shipped** | | Path-B ingest | **Shipped** | | CapsPolicy ACL | **Shipped opt-in** (`session_acl_enable`) | -| Live AgensGraph | **Claimed 0.7** when URL set | -| Neo4j live | **Claimed 0.14** | +| AgensGraph | **0.7** adapter + operator round trip (no runtime caller) when URL set | +| Neo4j | **Retired** (#187). `MEMNET_NEO4J_*` ignored | | HostSearch locators | Extra **0.17** (`RagHostHook`; no `rag_query`) | | Peak_L | Extra **0.18** (last-resort; not default goldfish) | | Pin-map export / catalog Snap | Extra **0.19** / **0.15** | diff --git a/.cursor/skills/sysml-memnet-documentation/SKILL.md b/.cursor/skills/sysml-memnet-documentation/SKILL.md index 4e9a424b..d2344949 100644 --- a/.cursor/skills/sysml-memnet-documentation/SKILL.md +++ b/.cursor/skills/sysml-memnet-documentation/SKILL.md @@ -32,7 +32,7 @@ token_guardrails: | ## Prerequisites -1. Install **0.19**: `pip install 'memnet-llm[mcp]'` (Hatch **0.19.3**; pin `==0.19.3` after PyPI upload, else last wheel is **0.19.0**). Optional `[neo4j]` (live claimed 0.14; drivers only). Contributors: `pip install -e ".[mcp]"`. +1. Install **0.19**: `pip install 'memnet-llm[mcp]'` (Hatch **0.19.3**; pin `==0.19.3` after PyPI upload, else last wheel is **0.19.0**). Neo4j extra is retired (#187). Contributors: `pip install -e ".[mcp]"`. 2. MemNet tools in the catalog. If absent: edit `.sysml` only. 3. SysML map: `parts/common/memnet/memnet/examples/schema.sysml.example.txt`. diff --git a/AGENTS.md b/AGENTS.md index 1b1a21e1..8952cf05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Catalog and load order: [`docs/README.md`](docs/README.md) (identity / `grammar/ Pure Python package (`memnet-llm`, Python >= 3.11). No external services, database, or Docker — the graph is in-memory. Standard lint/test/build commands are the authoritative recipe in `.github/workflows/ci.yml`; run/quick-start flow is in `README.md`. - **Virtualenv**: the startup update script installs everything (editable, with `dev,mcp` extras) into `.venv` at the repo root. **Activate it first** each session: `source .venv/bin/activate`. The `memnet` and `memnet-mcp` console scripts and `pytest`/`ruff` only exist inside `.venv`. System Python is externally managed, so do not `pip install` into it. -- **Tests / lint**: `pytest` (runs fully in-process; `tests/conftest.py` sets `MEMNET_TEST_INLINE=1`, no serve daemon needed). `ruff check` + `ruff format --check` over `parts/common/memnet parts/memnet-mcp/software tests`. One test (`agensgraph_live`) is skipped unless `MEMNET_AGENSGRAPH_URL` points at an external AgensGraph server. +- **Tests / lint**: `pytest` (runs fully in-process; `tests/conftest.py` sets `MEMNET_TEST_INLINE=1`, no serve daemon needed). `ruff check` + `ruff format --check` over `parts/common/memnet parts/memnet-mcp/software tests`. One test (`agensgraph_live`) is skipped unless `MEMNET_AGENSGRAPH_URL` points at an external AgensGraph server. Neo4j is not a backend. `MEMNET_NEO4J_*` is ignored; the engine starts. - **CLI path**: every `memnet` subcommand except `serve` needs a running serve process. Start it with `export MEMNET_IPC_SOCKET=/tmp/memnet.sock && memnet serve --ipc` (TCP fallback: `memnet serve`). Non-obvious: serve proxies argv, not your shell env — pass the minted `--session mn_...` explicitly (do not rely on `MEMNET_SESSION`). - **MCP path**: single-agent `memnet-mcp` (stdio) is in-process and needs no serve. `session_open` requires a map. Product write is **`mutate`**. leftover `add`/`update` remain leftover façades. MCP args are `wire_lines`/`map_lines` and `session` (the id), not `session_id`. - **Tip access portal**: ops sidecar `ops/tip_access_portal` (`memnet-tip-portal`), not the Hatch wheel. Keyed tip MCP only (`https://memnet.139-59-255-181.nip.io/mcp`). Env placeholders in `ops/tip_access_portal/.env.example`. nginx `auth_request` teach: `docs/operations/tip-memnet-access-portal.md`. tip≠face. diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f5e96d..152a8220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This project uses Semantic Versioning as **interpreted for MemNet**: package `a. ## [Unreleased] ### Changed +- **Honesty `c` — retire Neo4j; narrow AgensGraph 0.7 (#187)** — Neo4j cabinet (`Neo4jAdapter`, `Neo4jLibraryPort`, `liveNeo4jClaimed`) moved to `sysml-models/models/archive.sysml` (`MemNetArchive`, off `ProjectMemNet` load). Factory `bindsExactlyOneOf="fake|agensgraph"`. Removed the Neo4j adapter module, the `neo4j` extra, and the `neo4j_live` marker. `MEMNET_NEO4J_*` is ignored (one log warning); the engine starts. **0.7** claim is adapter + operator round trip (no runtime caller). No new hydrate/flush caller. No Hatch bump. ## [0.19.14] - 2026-09-24 diff --git a/README.md b/README.md index 475d6c00..f8b327cd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mission working memory for LLMs. A named session graph (GQL **node**/vertex, **edge**/relationship, **property**) that agents **pin** and **mutate** — not a notepad in chat, and not the search library. -MemNet sits **between** LLM call pipelines and data search (MN-REQ-00). Corpus lookup stays on the host (grep, ingest, optional RAG); it may propose **locators**. In the session, kinds/tags are overlapping **cues**; recall is **serial** — cue, then a bounded `pin_map` neighbourhood. It is not GraphRAG, not a vector store, and not AgensGraph/Neo4j. +MemNet sits **between** LLM call pipelines and data search (MN-REQ-00). Corpus lookup stays on the host (grep, ingest, optional RAG); it may propose **locators**. In the session, kinds/tags are overlapping **cues**; recall is **serial** — cue, then a bounded `pin_map` neighbourhood. It is not GraphRAG, not a vector store, and not AgensGraph. This repo ships the engine + generic MCP only. **Product shape:** [`docs/SHAPE.md`](docs/SHAPE.md). **Pinned role:** working set of **a few technical documents** (atoms and locators, not PDF bytes) plus live `TSK`/`USR`/`MOD`, re-read fast. Tens of MiB typical; **hundreds of MiB still in role**; gigabytes = RAG/cabinet. @@ -13,10 +13,9 @@ Package **`memnet-llm`** (CLI **`memnet`**). Python ≥ 3.11. Hatch **0.19.11**; ```bash pip install memnet-llm # or pin published: pip install memnet-llm==0.19.6 -# optional extras (drivers only — not AgensGraph/Neo4j servers): +# optional extras (drivers only — not an AgensGraph server): # pip install 'memnet-llm[mcp]' # pip install 'memnet-llm[agensgraph]' -# pip install 'memnet-llm[neo4j]' # contributors: pip install -e ".[dev,mcp]" ``` @@ -72,7 +71,7 @@ Handoff between modules/agents is the **`sessionId`** (treat it as a secret capa **Transport:** in-process MCP default (one graph per process). Shared graph: `memnet serve --ipc` (`MEMNET_IPC_SOCKET`) or TCP `memnet serve` (`127.0.0.1:18765`). Multitask / parallel workers need a shared serve — not default in-process. -**Durable:** optional clients `memnet-llm[agensgraph]` and `memnet-llm[neo4j]`; cabinets are external and not vendored. **0.7** AgensGraph live hydrate/flush proven (`liveCabinetClaimed`); CI skips unless `MEMNET_AGENSGRAPH_URL` is set. Neo4j is the same hydrate/flush seam (`Neo4jAdapter`); extra **0.14** claims `liveNeo4jClaimed=true` (live round-trip yes; hid flush; leftover-nickname hydrate after hid miss). Extra **0.16**: optional library database (`MEMNET_NEO4J_LIBRARY_DATABASE`) on the same URL emits locators only. Skip unless `MEMNET_NEO4J_URL`. +**Durable:** optional client `memnet-llm[agensgraph]`; the cabinet is external and not vendored. **0.7** is adapter + operator round trip (no runtime caller) (`liveCabinetClaimed`); CI skips unless `MEMNET_AGENSGRAPH_URL` is set. Neo4j is retired (#187). `MEMNET_NEO4J_*` is ignored (one log warning); the engine still starts. ## Deferred (honest) diff --git a/docs/LLM-GUIDE.md b/docs/LLM-GUIDE.md index f002348b..03ebda21 100644 --- a/docs/LLM-GUIDE.md +++ b/docs/LLM-GUIDE.md @@ -96,7 +96,7 @@ Interact only with **relevant slices** of the session — never dump the graph. 5. **Settle** finished work (`status=settled`, `recycle=delete_on_settle`) — HiAgent replace of the old subgoal. 6. (Occasionally) prune recyclable rows. -Process death / TTL: `session save` / `session load` is the offered file durable. Expire-time save is **off by default**. Set ``MEMNET_SAVE_ON_EXPIRE=1`` so `session save --file` still writes after TTL (then the live id is dropped). Set ``MEMNET_EXPIRE_SNAPSHOT_DIR`` as well to auto-write `{session_id}.snap` on purge/get expire. After expire, restore a sid you already hold with MCP `session_load(session=sid)` (no serve-host file path). `serve_status` reports whether expire-save is armed (`save_on_expire`, `expire_snapshot_dir_set`). Fake hydrate/flush is always-on CI. Live AgensGraph hydrate/flush is **0.7** when `MEMNET_AGENSGRAPH_URL` points at an operator cabinet — not required for default in-process work. Optional Neo4j client (`memnet-llm[neo4j]`) uses the same hydrate/flush owner; extra **0.14** claims live (`liveNeo4jClaimed=true`; live round-trip yes; hid flush; leftover-nickname hydrate after hid miss; skip unless `MEMNET_NEO4J_URL`). Extra **0.16** optional `MEMNET_NEO4J_LIBRARY_DATABASE` is locator-only on the same process. Agents still MUST NOT talk Bolt. **0.8** teach: product shape [`SHAPE.md`](SHAPE.md); version map [`ROADMAP.md`](ROADMAP.md). +Process death / TTL: `session save` / `session load` is the offered file durable. Expire-time save is **off by default**. Set ``MEMNET_SAVE_ON_EXPIRE=1`` so `session save --file` still writes after TTL (then the live id is dropped). Set ``MEMNET_EXPIRE_SNAPSHOT_DIR`` as well to auto-write `{session_id}.snap` on purge/get expire. After expire, restore a sid you already hold with MCP `session_load(session=sid)` (no serve-host file path). `serve_status` reports whether expire-save is armed (`save_on_expire`, `expire_snapshot_dir_set`). Fake hydrate/flush is always-on CI. AgensGraph **0.7** is adapter + operator round trip (no runtime caller) when `MEMNET_AGENSGRAPH_URL` points at an operator cabinet — not required for default in-process work, and not a runtime caller. Neo4j is retired. `MEMNET_NEO4J_*` is ignored; the engine still starts. Agents still MUST NOT talk Bolt. **0.8** teach: product shape [`SHAPE.md`](SHAPE.md); version map [`ROADMAP.md`](ROADMAP.md). Repeat. Each new turn starts with `pin_map(q)` (or empty-q outline). Drop the previous map from the pack. @@ -258,7 +258,7 @@ memnet query pin-map --cue … TCP `memnet serve` (`127.0.0.1:18765`) remains the Multitask / LAN fallback (MN-REQ-06.3). -See `docs/grammar/` for targets. Durable online GQL store adapter = **M2.5** (0.7: live hydrate/flush proven against an external cabinet; Fake + URL skip in CI). +See `docs/grammar/` for targets. Durable online GQL store adapter = **M2.5** (0.7: adapter + operator round trip, no runtime caller, against an external cabinet; Fake + URL skip in CI). Neo4j is not a backend. ### Common failure modes diff --git a/docs/README.md b/docs/README.md index c1f15f55..a11eeb35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,8 +40,8 @@ Durable store **behind** sessions. Index: [`cabinet/README.md`](cabinet/README.m | Doc | Role | |-----|------| -| [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md) | AgensGraph adapter: 0.7 live hydrate/flush; Fake + URL skip | -| [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md) | MemNet ↔ Neo4j; live claimed (0.14); two namespaces (0.16) | +| [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md) | AgensGraph adapter: 0.7 adapter + operator round trip (no runtime caller); Fake + URL skip | +| [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md) | Retired Neo4j cabinet (#187). `MEMNET_NEO4J_*` ignored | ## Extras — `extras/` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e664bfb2..be31b794 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,7 +4,7 @@ **Audience:** product developers. Dialect teach = **GQL** ([`grammar/gql-wire-profile.md`](grammar/gql-wire-profile.md)). Product shape: [`SHAPE.md`](SHAPE.md). British English. -**Package now:** Hatch **0.19.14** (`memnet.__version__`). Last published PyPI wheel is **`memnet-llm==0.19.14`** (2026-09-24). Numbered extras **0.10–0.19** are in this package (unchanged). **0.19.14** honesty `c` is TTL expire restore by known sid. **0.19.11** honesty `c` was the tip roll for [#168](https://github.com/chouswei/MemNet/pull/168) (catalog cross-session satisfy locators + `CousinSysMLEdge` `mustNotInventUploadBind`; do not claim tip=face). **0.19.10** honesty `c` keeps CueConflict for MATCH_L `|Q|>1` only; codebook miss emits CueMiss / Peak_L. **0.19.9** honesty `c` maps Path-B SysML `connection` / `link` to `:CON` (teach already said CON; ingest had mapped defs to PRT). **0.19.8** honesty `c` mints leftover nicknames on **all** snapshot records (not only catalog PKG) and accepts camelCase product relations (`inFile`) so `session_save` → `session_load` round-trips mission graphs. GraphGlot parse front is on master (#109 @ 73a63c9b). Extra **0.14** claims `liveNeo4jClaimed=true`. **1.0** is still unclaimed (0.5–0.8). +**Package now:** Hatch **0.19.14** (`memnet.__version__`). Last published PyPI wheel is **`memnet-llm==0.19.14`** (2026-09-24). Numbered extras **0.10–0.19** are in this package (unchanged). **0.19.14** honesty `c` is TTL expire restore by known sid. **0.19.11** honesty `c` was the tip roll for [#168](https://github.com/chouswei/MemNet/pull/168) (catalog cross-session satisfy locators + `CousinSysMLEdge` `mustNotInventUploadBind`; do not claim tip=face). **0.19.10** honesty `c` keeps CueConflict for MATCH_L `|Q|>1` only; codebook miss emits CueMiss / Peak_L. **0.19.9** honesty `c` maps Path-B SysML `connection` / `link` to `:CON` (teach already said CON; ingest had mapped defs to PRT). **0.19.8** honesty `c` mints leftover nicknames on **all** snapshot records (not only catalog PKG) and accepts camelCase product relations (`inFile`) so `session_save` → `session_load` round-trips mission graphs. GraphGlot parse front is on master (#109 @ 73a63c9b). Neo4j cabinet is retired (#187); **0.7** is adapter + operator round trip (no runtime caller). **1.0** is still unclaimed (0.5–0.8). **Last updated:** 2026-09-25 (`a.b.c` law locked; package **0.19.14** honesty `c` — TTL expire restore by known sid; extras **0.10–0.19** unchanged; Hatch **0.19.14**; last published PyPI **`memnet-llm==0.19.14`**; do not claim **1.0**; do not invent a **0.20** extra; do not claim tip=face). @@ -19,7 +19,7 @@ Package **`memnet-llm`** uses **`a.b.c`**. Coordinator lock 2026-08-23. This is ### `a` — claim / generation - **`0`** = **1.0** is unclaimed. -- **`1`** = coordinator claim of **0.5 + 0.6 + 0.7 + 0.8** (goldfish, honesty, live Agens, GQL teach). Not GraphRAG. Not a new engine. Not cabinet-only. +- **`1`** = coordinator claim of **0.5 + 0.6 + 0.7 + 0.8** (goldfish, honesty, Agens adapter + operator round trip with no runtime caller, GQL teach). Not GraphRAG. Not a new engine. Not cabinet-only. - **`a` moves only if the product contract breaks:** two operators (Recall = `pin_map` / Commit = `mutate`), GraphElement identity, GQL-only wire. - Do **not** bump `a` for extras, efficiency, N-server, or a new cabinet adapter. @@ -68,9 +68,9 @@ MemNet is **mission working memory** — the **memory plane of an agent harness* | | host Snap (RAG) cabinet Bolt locators only hydrate / flush - HostSearch locators (0.17) Agens live claimed (0.7) - outside MemNetSystem Neo4j live claimed (0.14) - 0.16 library DB (locators) + HostSearch locators (0.17) Agens 0.7: adapter + operator + outside MemNetSystem round trip (no runtime caller) + Neo4j retired (#187) ``` | Layer | Job | This repo? | @@ -78,7 +78,7 @@ MemNet is **mission working memory** — the **memory plane of an agent harness* | Outer harness | Loop, tools, env blob, eval tape | **No** | | Memory plane | Named session \(S\); goldfish Shape; sparse mutate; Path-B Absorb | **Yes** — engine + `memnet-mcp` | | Library RAG | Corpus → locators (Snap) | **0.17** HostSearch outside `MemNetSystem` (skip valid) | -| Cabinet | Persist one \(S\) | Agens **0.7 claimed**; Neo4j **0.14 claimed** | +| Cabinet | Persist one \(S\) | Agens **0.7**: adapter + operator round trip (no runtime caller). Neo4j retired (#187) | Handoff = **session id** (+ cue / write scope). Peers **re-`pin_map`** from labels+properties (or a kind/keyword cue). Chat is never SSOT. A durable store **backs** \(S\); it is not the handle and not the default teach surface. **MUST NOT** reframe MemNet as a Cypher proxy or as GraphRAG. @@ -96,7 +96,7 @@ Handoff = **session id** (+ cue / write scope). Peers **re-`pin_map`** from labe |---------|------|--------| | **0.5.0** | Goldfish leftover: paradox V1/V3/V4/V6; BoundedMatchFind (#73) seed-only `find`; multi-ego `pin_map` under one \(M\) + one LAW | **Shipped** (`v0.5.0`) | | **0.6.0** | Honesty: V5 LAW×N pytest; snapshot as offered durable; version-map docs | **Shipped** (`v0.6.0`) | -| **0.7.0** | Live AgensGraph hydrate/flush; `liveCabinetClaimed=true`. Server not vendored. Fake + skip unless `MEMNET_AGENSGRAPH_URL` | **Shipped** (`v0.7.0`) | +| **0.7.0** | Adapter + operator round trip (no runtime caller); `liveCabinetClaimed=true`. Server not vendored. Fake + skip unless `MEMNET_AGENSGRAPH_URL`. No runtime hydrate/flush caller | **Shipped** (`v0.7.0`); claim narrowed (#187) | | **0.8.0** | GQL-only **teach** + product **shape for people** (`SHAPE.md`, playbook, application-note contract, Multitask honesty). Docs only. **No** engine cut. Cabinet stays claimed | **Shipped** (`v0.8.0`) | | **0.9.0** | Neo4j `DurableStoreAdapter` client (`memnet-llm[neo4j]`); factory both-URL rule; [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md). Live round-trip claimed later as extra **0.14**. Cabinet extra, **not** a 1.0 gate | **Shipped** (`v0.9.0` era; extras later packaged as 0.19.0) | | **0.10–0.19** | Numbered extras (table below). Each row is one `b` (usage-method revision). Same pattern as 0.9: **not** 1.0 gates | **Packaged** (Hatch **0.19.14**; last published PyPI **`memnet-llm==0.19.14`**; extras first shipped as 0.19.0) | @@ -143,9 +143,9 @@ One concern per **`b`** (usage-method revision). Dependency order. **In package | **0.11.0** | **Session outline.** **Done / in package 0.19.0**. Empty cue is Recall of \(S\): kinds + hard-LIMIT exemplars (Neo4j Browser / `db.labels` + `LIMIT k` pattern). Model first if not yet in SysML, then code. Still one Recall. Name conflict among exemplars is CueConflict. | 0.10 (need pattern cue, not `--anchor`) | Dump \(S\); `getAllPages`; RAG search; third operator; `view=shell` as the outline (that is grain on a seed) | | **0.12.0** | **SameThingAbsorb implemented.** **Done / in package 0.19.0**. Agent-gated Commit after CueConflict. Pattern collapse (labels+props), not MERGE-by-id. Distinct from ImportAbsorb. SysML nest flag still `implemented=false` (Sysmler). | 0.10 CueConflict | Silent LLM merge in Recall; name-as-identity; third operator | | **0.13.0** | **Goldfish caller contract** (old 0.10). **Done / in package 0.19.0**. Playbook + pytest: stuffed history of maps is a fail; drop prior map rows; sparse Δ. | 0.10 teach | Patch OpenHands/SWE-agent; `rag_query`; raise \(M\); claim **1.0** | -| **0.14.0** | **Live Neo4j claimed** (old 0.11). **Done / in package 0.19.0**. Operator proof rpi5-syson @ d23cc71: `live_round_trip` yes; hid flush; leftover-nickname hydrate after hid miss. `liveNeo4jClaimed=true`. Skip unless `MEMNET_NEO4J_URL`. Hid stays off the wire. | 0.10 identity (else live claim cements leftover_MERGE_by_id) | Vendor Neo4j server; LLM↔Bolt; second database name (0.16); claim hydrate-by-hid proven on live | +| **0.14.0** | **Live Neo4j claimed** (old 0.11). Historical operator proof rpi5-syson @ d23cc71. **Retired** from the current package (#187): model on `MemNetArchive`; adapter and extra removed; `MEMNET_NEO4J_*` ignored. | 0.10 identity (else live claim cements leftover_MERGE_by_id) | Vendor Neo4j server; LLM↔Bolt; restore the cabinet; claim hydrate-by-hid proven on live | | **0.15.0** | **Catalog Snap** + session strata + model Snap (old 0.12). **Done / in package 0.19.0** (#124 @ 7767ed84). [`extras/memnet-session-strata.md`](extras/memnet-session-strata.md). Catalog cut's own fence: that extra does not own `liveNeo4jClaimed`. | ImportAbsorb pattern match (0.10); ingest locators as props | ANN rank sessions; Absorb whole \(S\); Layer; one session per REQ | -| **0.16.0** | **Two Neo4j namespaces** (old 0.13). **Done / in package 0.19.0** (#127 @ c32d4c52). Same process; cabinet vs library. Library port emits **locators only**, never `generate`. Skip library unless `MEMNET_NEO4J_LIBRARY_DATABASE`. `liveNeo4jClaimed` stays **true**. | 0.14 live claim | Fuse RRF/PPR into `pin_map`; `rag_query`; Snap-on-session; vendor Neo4j; LLM↔Bolt | +| **0.16.0** | **Two Neo4j namespaces** (old 0.13). Was in package 0.19.0 (#127 @ c32d4c52). **Retired** with the Neo4j cabinet (#187). | 0.14 live claim | Fuse RRF/PPR into `pin_map`; `rag_query`; Snap-on-session; restore Neo4j; LLM↔Bolt | | **0.17.0** | **HostSearch locators** (old 0.14). **Done / in package 0.19.0** (#129 @ 00e74dfb). `RagHostHook.implemented=true` **outside** `MemNetSystem`; locators into MutateGate / ingest; skip is valid. | 0.16 | `rag_query` MCP; Snap-on-session | | **0.18.0** | **Peak_L** last-resort (old 0.15). **Done / in package 0.19.0** (#128 @ dc464cd4). Topology cue on residual \(\rho^*\) when codebook miss; never default goldfish; V9 paradox pytest. | 0.5 find | Peak as default | | **0.19.0** | **Pin-map export** (old 0.16 / [#66](https://github.com/chouswei/MemNet/issues/66)). **Done / in package 0.19.0** (#123 @ 2c460e7d). Ingest ≠ export. | Shaped emit | Export as Absorb | @@ -172,7 +172,7 @@ Numbered fill is **0.10–0.19** above. This list is the overflow. **MUST NOT** | Item | Notes | |------|--------| -| Hosted AgensGraph as a product service | 0.7 proved **client** live; server not vendored | +| Hosted AgensGraph as a product service | 0.7 is the client adapter + operator round trip (no runtime caller); server not vendored | | First-class `PORT` NODE; SCHEMA vocab freeze | Grammar Open; ports stay properties | | Full ACL modes / roles / `session_token` | CapsPolicy cut already ships when enabled | | GraphGlot parse-front (#109 @ 73a63c9b) | **Shipped** as parse-front only on master. ProductGqlGate still runs after parse. Not a store. Layer stays retired. | @@ -189,7 +189,7 @@ Numbered fill is **0.10–0.19** above. This list is the overflow. **MUST NOT** **GQL:** agent teach/wire only. **MUST NOT** revive Layer teach. -**AgensGraph / Neo4j:** backing graphs for sessions — **not** a MemNet substitute and **not** the handoff handle. **MUST NOT** dual-write without a single sync owner. **MUST NOT** teach LLM ↔ Bolt as the miss path. +**AgensGraph:** backing graph for sessions — **not** a MemNet substitute and **not** the handoff handle. **0.7** is adapter + operator round trip (no runtime caller). **MUST NOT** dual-write without a single sync owner. **MUST NOT** teach LLM ↔ Bolt as the miss path. Neo4j is retired (#187). --- @@ -200,7 +200,7 @@ Numbered fill is **0.10–0.19** above. This list is the overflow. **MUST NOT** | **M1** | GQL wire profile SSOT; Layer archive; no Layer teach | **Done** (docs) | | **M2** | Engine/MCP: GQL accept + shaped `pin_map` emit; retire Layer/Tier A from product accept | **Done** | | **M3** | In-repo `LLM-GUIDE` + application-notes bodies → GQL examples | **Done** for **Layer retirement**. **0.10** TARGET teach rewrite (in package 0.19.0): cue/`pin_map` + pattern Commit; leftover `--anchor` / `id:'NEW'` named leftover. | -| **M2.5** | Durable store **behind** working memory (MemNet ↔ AgensGraph hydrate/flush; one sync owner) | **Done** (0.7 Agens; extra **0.14** Neo4j live claimed) | +| **M2.5** | Durable store **behind** working memory (MemNet ↔ AgensGraph hydrate/flush; one sync owner) | **Done** (0.7 adapter + operator round trip, no runtime caller). Neo4j retired (#187) | Durable: [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md), [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md). Numbered extras **0.10–0.19** above (in package **0.19.0**). @@ -270,7 +270,7 @@ Design: [`extras/memnet-host-search-nest.md`](extras/memnet-host-search-nest.md) | [`adr/ADR-001-gql-agent-wire.md`](adr/ADR-001-gql-agent-wire.md) | GQL wire; no Layer | | [`grammar/gql-wire-profile.md`](grammar/gql-wire-profile.md) | **M1 SSOT** | | [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md) | Durable GQL store adapter (**M2.5**) | -| [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md) | Second cabinet client (extra **0.14** live claimed) | +| [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md) | Retired Neo4j cabinet (#187) | | [`grammar/math-skeleton.md`](grammar/math-skeleton.md) | 0.5 Recall/Commit math | | [`extras/memnet-host-search-nest.md`](extras/memnet-host-search-nest.md) | Host search nest (0.17; skip valid) | | [`application-notes/examples/inverting-amplifier-gql-case-study.md`](application-notes/examples/inverting-amplifier-gql-case-study.md) | InvAmp GQL case study | diff --git a/docs/SHAPE.md b/docs/SHAPE.md index 3bfd3d71..a9fe39dd 100644 --- a/docs/SHAPE.md +++ b/docs/SHAPE.md @@ -37,7 +37,7 @@ From that problem, the product **must** be this shape: - **Session Shape** — compress \(S\) for the next LLM call (`max_rows` \(M\), hop \(k\)). - **Host Snap** — optional corpus → **locators** only. MUST NOT Snap-on-session (no ANN / `rag_query` of \(S\)). -5. **Durable cabinet behind, not instead** — an AgensGraph-class store may hydrate/flush \(S\). It is not the handoff handle and not the default teach surface. **0.7** proved live AgensGraph hydrate/flush; the server is not vendored. An optional Neo4j client sits on the **same** seam: LLM ↔ MemNet (GQL), MemNet ↔ Neo4j (Bolt hydrate/flush, one owner). Extra **0.14** claims live Neo4j (`liveNeo4jClaimed=true`; live round-trip yes; hid flush; leftover-nickname hydrate after hid miss). Extra **0.16** allows a second Neo4j **database name** on that process (library namespace, locators only; skip if unset). Fake-alone is not a durable claim. Talk: [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md), [`cabinet/neo4j-buffer.md`](cabinet/neo4j-buffer.md). +5. **Durable cabinet behind, not instead** — an AgensGraph-class store may hydrate/flush \(S\). It is not the handoff handle and not the default teach surface. **0.7** is adapter + operator round trip (no runtime caller); the server is not vendored. Neo4j is retired (#187). `MEMNET_NEO4J_*` is ignored. Fake-alone is not a durable claim. Talk: [`cabinet/agensgraph-buffer.md`](cabinet/agensgraph-buffer.md). 6. **This repo** — engine + generic `memnet-mcp` only. Novel-writer stays dropped. Transport: in-process first (single agent); **Multitask** uses TCP serve or streamable-http so workers share one \(S\). @@ -74,7 +74,7 @@ Do not call MemNet a “shaped RAG” or a “shaped Cypher proxy”. **0.8.0** is this shape **taught for people** in-repo: this file, GQL-only playbook, application-note contract, Multitask honesty (RSV + Path-B ingest shipped; full ACL modes still to-be). -**1.0.0** is **0.5 + 0.6 + 0.7 + 0.8** claimed — the shape is mature for people: one GQL dialect, goldfish `pin_map`, gated mutate, cue-then-shape (including find when there is no ego), optional **proven** cabinet so \(S\) can outlive a process. Not GraphRAG. Not cabinet-only. Map and **`a.b.c` law:** [`ROADMAP.md`](ROADMAP.md). **Honest install:** Hatch **0.19.11**; last published PyPI wheel is **`memnet-llm==0.19.6`** until this cut is uploaded (extras 0.10–0.19; Neo4j client from 0.9; live claimed as extra **0.14**; 0.19.11 honesty `c`: catalog cross-session satisfy locators + CousinSysMLEdge mustNotInventUploadBind). Optional extras `[mcp]`, `[agensgraph]`, `[neo4j]` (drivers only). **1.0** stays unclaimed. +**1.0.0** is **0.5 + 0.6 + 0.7 + 0.8** claimed — the shape is mature for people: one GQL dialect, goldfish `pin_map`, gated mutate, cue-then-shape (including find when there is no ego), optional **proven** cabinet so \(S\) can outlive a process. Not GraphRAG. Not cabinet-only. Map and **`a.b.c` law:** [`ROADMAP.md`](ROADMAP.md). **Honest install:** Hatch **0.19.11**; last published PyPI wheel is **`memnet-llm==0.19.6`** until this cut is uploaded (extras 0.10–0.19; Neo4j retired #187; 0.19.11 honesty `c`: catalog cross-session satisfy locators + CousinSysMLEdge mustNotInventUploadBind). Optional extras `[mcp]`, `[agensgraph]` (drivers only). **1.0** stays unclaimed. --- diff --git a/docs/cabinet/README.md b/docs/cabinet/README.md index 0af57f85..fe1561b5 100644 --- a/docs/cabinet/README.md +++ b/docs/cabinet/README.md @@ -5,7 +5,7 @@ Durable GQL **behind** sessions: hydrate / flush adapter, one sync owner. `hydra | Doc | Role | |-----|------| | [`agensgraph-buffer.md`](agensgraph-buffer.md) | AgensGraph adapter (0.7 round trip; `hydrateFlushCallers="tests"`) | -| [`neo4j-buffer.md`](neo4j-buffer.md) | Neo4j client (0.14 round trip; `hydrateFlushCallers="tests"`; 0.16 two namespaces) | +| [`neo4j-buffer.md`](neo4j-buffer.md) | Retired Neo4j cabinet (#187). Not a current backend | | [`storage-roles.d2`](storage-roles.d2) | Storage-role picture (working memory, file snapshot, cabinet ego) | Wire: [`../grammar/gql-wire-profile.md`](../grammar/gql-wire-profile.md). Index: [`../README.md`](../README.md). diff --git a/docs/cabinet/agensgraph-buffer.md b/docs/cabinet/agensgraph-buffer.md index 3016a39b..08bdf7a7 100644 --- a/docs/cabinet/agensgraph-buffer.md +++ b/docs/cabinet/agensgraph-buffer.md @@ -77,7 +77,7 @@ | Optional extra `memnet-llm[agensgraph]` (`psycopg`, not the DB server) | Landed | | serve / MCP bind `get_sync_owner(make_adapter_from_env())` once | Landed | | Live integration test | Skip unless `MEMNET_AGENSGRAPH_URL` set | -| Claim adapter / M2.5 shipped | **0.7** — live hydrate/flush proven; still not a hosted cabinet service | +| Claim adapter / M2.5 shipped | **0.7** — adapter + operator round trip (no runtime caller); still not a hosted cabinet service | Agents continue to use MemNet GQL `pin_map` / mutate only. Durable calls go through `DurableSyncOwner` / `SessionLifecycle.hydrate_from_durable` — never as the LLM primary path. @@ -101,12 +101,11 @@ Factory / startup semantics: | Env | Adapter bound by `get_sync_owner()` | |-----|-------------------------------------| | `MEMNET_DURABLE_FAKE` truthy | `FakeDurableAdapter` | -| else both AgensGraph and Neo4j URLs set | **error** unless `MEMNET_DURABLE_BACKEND` is `agensgraph` or `neo4j` | +| `MEMNET_NEO4J_*` set | **Ignored.** One log warning. Engine starts. No Neo4j bind (#187) | | else `MEMNET_AGENSGRAPH_URL` set | `AgensGraphAdapter` (client) | -| else `MEMNET_NEO4J_URL` set | `Neo4jAdapter` (client; extra **0.14** live claimed) | | else | `FakeDurableAdapter` (dev/test seam — not a production cabinet) | -Second cabinet (same ABC / owner / budget; **not** a live claim). What talks to what, Cypher vs Agens, steal/reject: [`neo4j-buffer.md`](neo4j-buffer.md). +Neo4j is retired. Historical talk: [`neo4j-buffer.md`](neo4j-buffer.md). `memnet serve` and `memnet-mcp` bind the owner once at process start using those rules. diff --git a/docs/cabinet/neo4j-buffer.md b/docs/cabinet/neo4j-buffer.md index 5d65475d..45f26449 100644 --- a/docs/cabinet/neo4j-buffer.md +++ b/docs/cabinet/neo4j-buffer.md @@ -1,9 +1,8 @@ # Between MemNet and Neo4j -**Status:** client landed; **live Neo4j round-trip claimed** (`liveNeo4jClaimed=true`; extra 0.14). That claim is the adapter plus the 0.14 operator round trip, not a runtime path. `DurableBuffer.hydrateFlushCallers="tests"`: no MCP, CLI, Commit, or TTL caller. `serve`, IPC, and MCP bind the owner at startup and do not invoke it. Live round-trip yes; hid flush; leftover-nickname hydrate after hid miss. Do **not** write hydrate-by-hid proven on live. Extra **0.16** (package 0.19.0): two named databases on one Neo4j process — **cabinet** (`MEMNET_NEO4J_DATABASE`) vs optional **library** (`MEMNET_NEO4J_LIBRARY_DATABASE`; skip if unset). Library port emits **locators only** (`generate=false`). Skip live pytest unless `MEMNET_NEO4J_URL` is set. Server not vendored. -**Audience:** product developers. -**Sibling:** AgensGraph 0.7 live cabinet [`agensgraph-buffer.md`](agensgraph-buffer.md). Same MUST NOTs. Same ABC / owner / budget. -**Shape:** cabinet **behind** the session, not instead of it ([`../SHAPE.md`](../SHAPE.md) §5). +**Status:** **Retired (#187).** The Neo4j model parts sit on `MemNetArchive` (`sysml-models/models/archive.sysml`), off `ProjectMemNet` load. The adapter module, the `neo4j` extra, and the `neo4j_live` marker are removed. `MEMNET_NEO4J_*` is ignored (one log warning). The engine starts. Do not install `memnet-llm[neo4j]`. The sections below are history, not a current backend. + +**Sibling:** AgensGraph 0.7 is adapter + operator round trip (no runtime caller): [`agensgraph-buffer.md`](agensgraph-buffer.md). MemNet sits **between** LLM call pipelines and a durable graph. [Neo4j](https://neo4j.com/) is one **external cabinet** on that far side. It is not goldfish, not agent wire, not GraphRAG, not a GraphQL facade, and not a vendored server. Agents talk GQL `pin_map` / mutate to **MemNet**. `hydrate` / `flush` exist on `DurableSyncOwner` / `SessionLifecycle` and are **not** on the agent path: no MCP tool, no CLI command, no Commit caller, and no TTL caller. Only tests invoke them. `serve`, IPC, and MCP bind the owner at startup and do not call it. **MUST NOT** teach LLM ↔ Bolt as the goldfish path or reframe MemNet as a Cypher proxy. diff --git a/docs/extras/memnet-efficiency-gap.md b/docs/extras/memnet-efficiency-gap.md index 2e93be13..5f833532 100644 --- a/docs/extras/memnet-efficiency-gap.md +++ b/docs/extras/memnet-efficiency-gap.md @@ -67,7 +67,7 @@ Path-B ingest pays the same Commit stack: `pin_map_ingest.py` `PinMapIngestBase. | Cost | Cite | What happens | |------|------|----------------| -| Hydrate | `durable/agensgraph.py` / `neo4j.py` `hydrate` | Loop `_HYDRATE_MATCH_KEYS = (_memnet_hid, "id")` — leftover **nickname `id` after hid miss**. Then a second query for edges among hydrated hids. Agens node walk is `OPTIONAL MATCH (ego)-[*0..depth]-(n)`. | +| Hydrate | `durable/agensgraph.py` `hydrate` | Loop `_HYDRATE_MATCH_KEYS = (_memnet_hid, "id")` — leftover **nickname `id` after hid miss**. Then a second query for edges among hydrated hids. Agens node walk is `OPTIONAL MATCH (ego)-[*0..depth]-(n)`. Neo4j hydrate is retired (#187). | | Flush | `flush` in both adapters | **One MERGE per node and per edge**, keyed on `_memnet_hid` (not `{id}`). Agens: one transaction of N statements. Neo4j: **auto-commit each** `session.run`. | | Slice rebuild | `durable/sync.py` `DurableSyncOwner.flush_from_session` | Rebuilds the slice with `context_pack` (ego neighbourhood), **not** a Commit delta. Hydrate upserts every returned record into the live session. | diff --git a/docs/grammar/gql-wire-profile.md b/docs/grammar/gql-wire-profile.md index 84a3db37..f1d554bc 100644 --- a/docs/grammar/gql-wire-profile.md +++ b/docs/grammar/gql-wire-profile.md @@ -269,7 +269,7 @@ Shaped subgraph = ordered openCypher-family lines (or isomorphic structured grap |-------|------|-------------------------| | **M1 (done)** | Conventions, MUST/MUST NOT, shaped-read contract, GQL-only teach | Engine code, app-note marathon, store adapter | | **M2 (done)** | `GqlCodec` accept; `PinMapShapedRead` emit; MutateGate GQL path; Layer/Tier A **retired** from product accept | — | -| **M2.5 (done, 0.7 / 0.14)** | Durable online GQL store adapter (MemNet ↔ AgensGraph hydrate/flush; one sync owner; optional Neo4j live claimed 0.14) — [`agensgraph-buffer.md`](../cabinet/agensgraph-buffer.md), [`neo4j-buffer.md`](../cabinet/neo4j-buffer.md) | Agent Bolt / LLM↔store direct teach; hosted cabinet | +| **M2.5 (done, 0.7)** | Durable online GQL store adapter (MemNet ↔ AgensGraph; one sync owner; 0.7 is adapter + operator round trip, no runtime caller). Neo4j retired (#187) — [`agensgraph-buffer.md`](../cabinet/agensgraph-buffer.md) | Agent Bolt / LLM↔store direct teach; hosted cabinet; a Neo4j backend | | **M3 (done, 0.8 docs)** | In-repo `LLM-GUIDE` + application-notes GQL examples | User-pack skill rewrite (sibling repo) | | **User-pack (parallel)** | `memnet-format` / `mcp-memnet` / … → GQL-only in `chouswei/cursor-user-skills` | **In flight separately** — not this repo’s M1–M3 gate | @@ -289,7 +289,7 @@ Shaped subgraph = ordered openCypher-family lines (or isomorphic structured grap | [`../ROADMAP.md`](../ROADMAP.md) | SemVer map SSOT | | [`math-skeleton.md`](math-skeleton.md) | 0.5 Recall/Commit math SSOT (operator domains; one \(S\) per generate) | | [`agensgraph-buffer.md`](../cabinet/agensgraph-buffer.md) | Durable GQL store adapter sketch (**M2.5**) | -| [`neo4j-buffer.md`](../cabinet/neo4j-buffer.md) | Second cabinet client (extra **0.14** live claimed) | +| [`neo4j-buffer.md`](../cabinet/neo4j-buffer.md) | Retired Neo4j cabinet (#187). Not a current backend | | [`../application-notes/examples/inverting-amplifier-gql-case-study.md`](../application-notes/examples/inverting-amplifier-gql-case-study.md) | Worked GQL encoding | | [`memnet-session-strata.md`](../extras/memnet-session-strata.md) | Named sessions as strata (not Layer wire) | | [`../../sysml-models/README.md`](../../sysml-models/README.md) | Nested `GqlCodec` / `PinMapShapedRead` | diff --git a/parts/common/memnet/memnet/durable/__init__.py b/parts/common/memnet/memnet/durable/__init__.py index 79765bbf..6bf4c2ce 100644 --- a/parts/common/memnet/memnet/durable/__init__.py +++ b/parts/common/memnet/memnet/durable/__init__.py @@ -2,10 +2,11 @@ MemNet sessions remain the agent SSOT handle. This package sits *behind* sessions (hydrate / flush, one sync owner). Agents keep talking GQL pin_map / -mutate to MemNet — not to AgensGraph / Neo4j / FakeDurableAdapter directly. +mutate to MemNet — not to AgensGraph / FakeDurableAdapter directly. -See docs/cabinet/agensgraph-buffer.md, docs/cabinet/neo4j-buffer.md, and -SysML DurableBuffer / AgensGraphAdapter / Neo4jAdapter. +Neo4j cabinet is archived (#187). ``MEMNET_NEO4J_*`` is ignored. + +See docs/cabinet/agensgraph-buffer.md and SysML DurableBuffer / AgensGraphAdapter. """ from __future__ import annotations @@ -18,11 +19,6 @@ from memnet.durable.agensgraph import AgensGraphAdapter from memnet.durable.factory import make_adapter_from_env, reset_adapter_factory_for_tests from memnet.durable.fake import FakeDurableAdapter, company_ego_fixture -from memnet.durable.neo4j import Neo4jAdapter -from memnet.durable.neo4j_library import ( - Neo4jLibraryClient, - make_library_client_from_env, -) from memnet.durable.sync import ( DurableSyncOwner, get_sync_owner, @@ -31,9 +27,6 @@ __all__ = [ "AgensGraphAdapter", - "Neo4jAdapter", - "Neo4jLibraryClient", - "make_library_client_from_env", "DurableStoreAdapter", "DurableSubgraph", "DurableSyncOwner", diff --git a/parts/common/memnet/memnet/durable/factory.py b/parts/common/memnet/memnet/durable/factory.py index 05501589..cc5705d9 100644 --- a/parts/common/memnet/memnet/durable/factory.py +++ b/parts/common/memnet/memnet/durable/factory.py @@ -2,67 +2,73 @@ from __future__ import annotations +import logging import os from memnet.durable.adapter import DurableStoreAdapter from memnet.durable.agensgraph import AgensGraphAdapter from memnet.durable.fake import FakeDurableAdapter -from memnet.durable.neo4j import Neo4jAdapter -from memnet.exceptions import MemNetError + +logger = logging.getLogger(__name__) # When set to "1"/"true", make_adapter_from_env returns Fake even if URL is set # (explicit local spike / CI). Without URL, Fake is also the default *seam* # stand-in — not a production durable cabinet. ENV_USE_FAKE = "MEMNET_DURABLE_FAKE" -ENV_BACKEND = "MEMNET_DURABLE_BACKEND" +_NEO4J_ENV_PREFIX = "MEMNET_NEO4J_" +_neo4j_retired_warned = False def _truthy(val: str | None) -> bool: return (val or "").strip().lower() in {"1", "true", "yes", "on"} +def warn_retired_neo4j_env() -> None: + """Ignore ``MEMNET_NEO4J_*``. One warning. Names only — never values. Never raise.""" + global _neo4j_retired_warned + if _neo4j_retired_warned: + return + present = sorted(key for key in os.environ if key.startswith(_NEO4J_ENV_PREFIX)) + if not present: + return + _neo4j_retired_warned = True + logger.warning( + "Ignoring retired Neo4j settings (%s). Neo4j cabinet is archived. Starting without it.", + ", ".join(present), + ) + + +def reset_neo4j_retired_warning_for_tests() -> None: + """Allow a later test to observe the one-shot warning again.""" + global _neo4j_retired_warned + _neo4j_retired_warned = False + + def make_adapter_from_env() -> DurableStoreAdapter: """Bind one DurableStoreAdapter for ``get_sync_owner()``. Semantics (serve / MCP bind the result once via ``get_sync_owner()``): - - ``MEMNET_DURABLE_FAKE`` truthy → Fake (even if URLs are set) - - else both AgensGraph and Neo4j URLs set → error unless - ``MEMNET_DURABLE_BACKEND`` is ``agensgraph`` or ``neo4j`` - (one sync owner; do not silently pick). Two Neo4j *database - names* on one URL are extra 0.16 (cabinet + optional library - port) and are not this conflict. + - ``MEMNET_NEO4J_*`` is ignored. One log warning. The engine starts. + SHALL NOT crash. SHALL NOT bind a Neo4j adapter. + - ``MEMNET_DURABLE_FAKE`` truthy → Fake (even if an AgensGraph URL is set) - else ``MEMNET_AGENSGRAPH_URL`` set → ``AgensGraphAdapter`` - - else ``MEMNET_NEO4J_URL`` set → ``Neo4jAdapter`` - else → Fake seam stand-in for tests/dev - Fake is **not** a production durable store. Agents still MUST NOT talk to - either adapter directly — only ``DurableSyncOwner`` / SessionLifecycle ports. + Factory binds exactly one of fake or AgensGraph. Fake is **not** a + production durable store. Agents still MUST NOT talk to the adapter + directly — only ``DurableSyncOwner`` / SessionLifecycle ports. """ + warn_retired_neo4j_env() if _truthy(os.environ.get(ENV_USE_FAKE)): seed = _truthy(os.environ.get("MEMNET_DURABLE_FAKE_SEED_COMPANY")) return FakeDurableAdapter(seed_company_ego=seed) agens = AgensGraphAdapter.from_env() - neo4j = Neo4jAdapter.from_env() - if agens is not None and neo4j is not None: - backend = (os.environ.get(ENV_BACKEND) or "").strip().lower() - if backend == "agensgraph": - return agens - if backend == "neo4j": - return neo4j - raise MemNetError( - "durable_backend_conflict", - "Both MEMNET_AGENSGRAPH_URL and MEMNET_NEO4J_URL are set. " - "Set MEMNET_DURABLE_BACKEND to 'agensgraph' or 'neo4j' " - "(one sync owner; do not silently pick).", - example="export MEMNET_DURABLE_BACKEND=neo4j", - ) if agens is not None: return agens - if neo4j is not None: - return neo4j return FakeDurableAdapter(seed_company_ego=False) def reset_adapter_factory_for_tests() -> None: """Hook for tests — factory is currently pure; reserved for future cache.""" + reset_neo4j_retired_warning_for_tests() diff --git a/parts/common/memnet/memnet/durable/neo4j.py b/parts/common/memnet/memnet/durable/neo4j.py deleted file mode 100644 index 2561f926..00000000 --- a/parts/common/memnet/memnet/durable/neo4j.py +++ /dev/null @@ -1,452 +0,0 @@ -"""Neo4jAdapter — client/adapter for an *external* Neo4j cabinet. - -Connection placeholders (never hardcode secrets): - -- MEMNET_NEO4J_URL e.g. bolt://127.0.0.1:7687 or neo4j://… -- MEMNET_NEO4J_USER -- MEMNET_NEO4J_PASSWORD -- MEMNET_NEO4J_DATABASE cabinet database (default: neo4j) -- MEMNET_NEO4J_LIBRARY_DATABASE optional library database (skip if unset) - -This module is a **client** only: it speaks Neo4j Cypher over the official -``neo4j`` Python driver to a store the operator already runs. It does -**not** vendor or require a Neo4j server binary in this repo. - -Wire stays MemNet GQL pin_map / mutate; the sync owner alone calls -hydrate/flush. Agents MUST NOT talk Bolt. - -Live Bolt round-trip is claimed as extra **0.14** (``liveNeo4jClaimed=true``; -live round-trip yes; hid flush; leftover-nickname hydrate after hid miss). -Extra **0.16** library namespace is ``neo4j_library`` (second database -name), not this adapter. Hid stays off the wire. Skip live pytest unless -``MEMNET_NEO4J_URL``. -""" - -from __future__ import annotations - -import os -import re -from dataclasses import dataclass -from typing import Any - -from memnet.durable.adapter import ( - DurableStoreAdapter, - DurableSubgraph, - HydrateBudget, - node_cabinet_keys, - record_cabinet_hid, - remap_edge_endpoints_to_nicknames, - resolve_endpoint_hid, -) -from memnet.durable.agensgraph import map_edge_row, map_node_row -from memnet.exceptions import MemNetError -from memnet.models import Record - -ENV_URL = "MEMNET_NEO4J_URL" -ENV_USER = "MEMNET_NEO4J_USER" -ENV_PASSWORD = "MEMNET_NEO4J_PASSWORD" -ENV_DATABASE = "MEMNET_NEO4J_DATABASE" - -DEFAULT_DATABASE = "neo4j" -_MEMNET_TAG_KEY = "_memnet_tag" -_MEMNET_HID_KEY = "_memnet_hid" -_SAFE_IDENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") -_SAFE_ID = re.compile(r"^[A-Za-z0-9_.:/@+-]+$") -_HYDRATE_MATCH_KEYS = (_MEMNET_HID_KEY, "id") - - -@dataclass(frozen=True) -class Neo4jConfig: - url: str - user: str | None = None - password: str | None = None - database: str | None = None - - @classmethod - def from_env(cls) -> Neo4jConfig | None: - url = (os.environ.get(ENV_URL) or "").strip() - if not url: - return None - return cls( - url=url, - user=(os.environ.get(ENV_USER) or "").strip() or None, - password=os.environ.get(ENV_PASSWORD) or None, - database=(os.environ.get(ENV_DATABASE) or "").strip() or None, - ) - - @property - def database_name(self) -> str: - return self.database or DEFAULT_DATABASE - - -def cypher_ident(name: str, *, kind: str = "label") -> str: - """Validate and return a Neo4j identifier (node label / rel type).""" - if not name or not _SAFE_IDENT.match(name): - raise MemNetError( - "neo4j_bad_ident", - f"Neo4j {kind} must be a simple identifier, got {name!r}", - ) - return name - - -def require_id(value: str, *, kind: str = "id") -> str: - """Reject unsafe MemNet ids before they reach Bolt parameters.""" - if not value or not _SAFE_ID.match(value): - raise MemNetError( - "neo4j_bad_id", - f"Neo4j {kind} must be a simple token, got {value!r}", - ) - return value - - -def props_for_set(fields: dict[str, str], *, skip: set[str]) -> dict[str, str]: - """String fields with safe keys — values travel as Bolt parameters.""" - out: dict[str, str] = {} - for key, val in fields.items(): - if key in skip or not _SAFE_IDENT.match(key): - continue - out[key] = str(val) - return out - - -def build_hydrate_nodes_cypher( - ego_id: str, - budget: HydrateBudget, - *, - match_key: str = _MEMNET_HID_KEY, -) -> tuple[str, dict[str, Any]]: - """Ego-bounded node walk: vertices within ``budget.depth`` of ego. - - Cabinet identity is ``_memnet_hid``. leftover nickname ``id`` is a property - filter only (``match_key='id'`` after a hid miss). Neo4j Cypher: ``labels(n)`` - (list), not AgensGraph ``label(n)``. - """ - if match_key not in _HYDRATE_MATCH_KEYS: - raise MemNetError( - "neo4j_bad_ident", - f"hydrate match_key must be {_MEMNET_HID_KEY!r} or 'id', got {match_key!r}", - ) - ego = require_id(ego_id, kind="ego id") - depth = max(0, int(budget.depth)) - limit = max(1, int(budget.max_nodes)) - query = ( - f"MATCH (ego {{{match_key}: $ego_id}})\n" - f"OPTIONAL MATCH (ego)-[*0..{depth}]-(n)\n" - "WITH DISTINCT n\n" - "WHERE n IS NOT NULL\n" - "RETURN labels(n) AS labels, properties(n) AS props\n" - f"LIMIT {limit}" - ) - return query, {"ego_id": ego} - - -def build_hydrate_edges_cypher( - ego_id: str, - budget: HydrateBudget, - node_ids: list[str] | None = None, -) -> tuple[str, dict[str, Any]]: - """Edges whose both endpoints lie inside the already-hydrated node set.""" - ego = require_id(ego_id, kind="ego id") - limit = max(0, int(budget.max_edges)) - skip = ( - f"MATCH (ego {{{_MEMNET_HID_KEY}: $ego_id}})\n" - "WHERE false\n" - "RETURN null AS rel_type, null AS props, null AS src, null AS dist\n" - "LIMIT 0" - ) - if limit == 0: - return skip, {"ego_id": ego} - ids = [require_id(i) for i in (node_ids or []) if i] - if not ids: - return skip, {"ego_id": ego} - query = ( - "MATCH (src)-[rel]->(dst)\n" - f"WHERE src.{_MEMNET_HID_KEY} IN $node_ids AND dst.{_MEMNET_HID_KEY} IN $node_ids\n" - "RETURN DISTINCT type(rel) AS rel_type, properties(rel) AS props, " - f"src.{_MEMNET_HID_KEY} AS src, dst.{_MEMNET_HID_KEY} AS dist\n" - f"LIMIT {limit}" - ) - return query, {"node_ids": ids} - - -def build_merge_node_cypher(record: Record) -> tuple[str, dict[str, Any]]: - """MERGE/SET one MemNet node record into Neo4j.""" - tag = cypher_ident(record.tag.upper(), kind="node label") - query = ( - f"MERGE (n:{tag} {{{_MEMNET_HID_KEY}: $hid}})\n" - f"SET n += $props, n.{_MEMNET_TAG_KEY} = $tag, n.{_MEMNET_HID_KEY} = $hid" - ) - hid = record_cabinet_hid(record) - if hid: - hid = require_id(hid, kind="hid") - props = props_for_set(dict(record.fields), skip={_MEMNET_HID_KEY, _MEMNET_TAG_KEY}) - return query, {"hid": hid, "props": props, "tag": record.tag.upper()} - - -def build_merge_edge_cypher( - record: Record, - *, - nodes: list[Record] | None = None, -) -> tuple[str, dict[str, Any]]: - """MERGE/SET one MemNet EDG record (relation → relationship type). - - Endpoints MATCH by the node cabinet hid, not leftover nickname ``id``. - """ - if record.tag.upper() != "EDG": - raise MemNetError( - "neo4j_bad_edge", - f"flush edge expects tag EDG, got {record.tag!r}", - ) - src = record.fields.get("src") or "" - dist = record.fields.get("dist") or "" - rel = record.fields.get("relation") or "" - if not src or not dist or not rel: - raise MemNetError( - "neo4j_bad_edge", - "flush edge requires fields src, dist, relation", - ) - rel_type = cypher_ident(rel, kind="relationship type") - src_hid = require_id(resolve_endpoint_hid(src, nodes or []), kind="src hid") - dist_hid = require_id(resolve_endpoint_hid(dist, nodes or []), kind="dist hid") - skip = {"src", "dist", "relation", _MEMNET_HID_KEY, _MEMNET_TAG_KEY} - props = props_for_set(dict(record.fields), skip=skip) - hid = record_cabinet_hid(record) - merge_edge = f"MERGE (a)-[r:{rel_type}]->(b)" - params: dict[str, Any] = { - "src": src_hid, - "dist": dist_hid, - "props": props, - "rel": rel, - } - if hid: - params["hid"] = require_id(hid, kind="hid") - merge_edge = f"MERGE (a)-[r:{rel_type} {{{_MEMNET_HID_KEY}: $hid}}]->(b)" - query = ( - f"MATCH (a {{{_MEMNET_HID_KEY}: $src}}), (b {{{_MEMNET_HID_KEY}: $dist}})\n" - f"{merge_edge}\n" - f"SET r += $props, r.{_MEMNET_TAG_KEY} = 'EDG', r.relation = $rel" - ) - return query, params - - -def first_label(labels: Any) -> str: - """Pick a single label from Neo4j ``labels(n)`` (list) or a scalar.""" - if labels is None: - return "" - if isinstance(labels, str): - return labels - if isinstance(labels, (list, tuple)): - for item in labels: - if item: - return str(item) - return "" - return str(labels) - - -class Neo4jAdapter(DurableStoreAdapter): - """Client adapter: hydrate/flush ego slices against external Neo4j.""" - - def __init__(self, config: Neo4jConfig) -> None: - self.config = config - self._driver: Any = None - - @classmethod - def from_env(cls) -> Neo4jAdapter | None: - cfg = Neo4jConfig.from_env() - if cfg is None: - return None - from memnet.durable.neo4j_library import library_database_name - - lib = library_database_name() - if lib and cfg.database_name == lib: - raise MemNetError( - "neo4j_library_same_as_cabinet", - "cabinet MEMNET_NEO4J_DATABASE must differ from MEMNET_NEO4J_LIBRARY_DATABASE.", - example="export MEMNET_NEO4J_LIBRARY_DATABASE=library", - ) - return cls(cfg) - - @property - def name(self) -> str: - return "neo4j" - - def hydrate(self, ego_id: str, budget: HydrateBudget) -> DurableSubgraph: - if not ego_id or not str(ego_id).strip(): - raise MemNetError("no_ego", "hydrate requires ego_id") - budget = budget or HydrateBudget() - self._ensure_driver() - node_rows: list[Any] = [] - for match_key in _HYDRATE_MATCH_KEYS: - nodes_cypher, nodes_params = build_hydrate_nodes_cypher( - ego_id, budget, match_key=match_key - ) - try: - node_rows = self._run(nodes_cypher, nodes_params) - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 — surface driver errors clearly - raise MemNetError( - "neo4j_query_failed", - f"Neo4j hydrate query failed: {type(exc).__name__}: {exc}", - example="Check MEMNET_NEO4J_URL / database / network", - ) from exc - if node_rows: - break - - nodes: list[Record] = [] - seen_nodes: set[str] = set() - for row in node_rows: - rec = map_node_row( - first_label(_row_get(row, 0, "labels")), - _row_get(row, 1, "props"), - ) - if rec is None: - continue - seen_key = rec.id or record_cabinet_hid(rec) - if not seen_key or seen_key in seen_nodes: - continue - seen_nodes.add(seen_key) - nodes.append(rec) - - edges_cypher, edges_params = build_hydrate_edges_cypher( - ego_id, budget, node_ids=node_cabinet_keys(nodes) - ) - try: - edge_rows = self._run(edges_cypher, edges_params) if budget.max_edges > 0 else [] - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_query_failed", - f"Neo4j hydrate query failed: {type(exc).__name__}: {exc}", - example="Check MEMNET_NEO4J_URL / database / network", - ) from exc - - edges: list[Record] = [] - seen_edges: set[str] = set() - relations: set[str] = set() - for row in edge_rows: - rec = map_edge_row( - _row_get(row, 0, "rel_type"), - _row_get(row, 1, "props"), - _row_get(row, 2, "src"), - _row_get(row, 3, "dist"), - ) - if rec is None or rec.id in seen_edges: - continue - seen_edges.add(rec.id) - edges.append(remap_edge_endpoints_to_nicknames(rec, nodes)) - rel = rec.fields.get("relation") or "" - if rel: - relations.add(rel) - - return DurableSubgraph( - ego_id=ego_id, - nodes=nodes, - edges=edges, - relations=relations, - ).bounded(budget) - - def flush(self, subgraph: DurableSubgraph) -> None: - if subgraph is None: - raise MemNetError("neo4j_bad_flush", "flush requires a DurableSubgraph") - self._ensure_driver() - statements: list[tuple[str, dict[str, Any]]] = [] - for rec in subgraph.nodes: - if not record_cabinet_hid(rec): - continue - statements.append(build_merge_node_cypher(rec)) - for rec in subgraph.edges: - if rec.tag.upper() != "EDG": - continue - statements.append(build_merge_edge_cypher(rec, nodes=subgraph.nodes)) - if not statements: - return - # Auto-commit each MERGE (Neo4j session.run). A later hydrate error - # cannot roll back a successful flush (AgensGraph 0.7 lesson). - try: - for cypher, params in statements: - self._run(cypher, params) - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_query_failed", - f"Neo4j flush failed: {type(exc).__name__}: {exc}", - example="Check MERGE permissions / endpoint ids exist", - ) from exc - - def close(self) -> None: - driver = self._driver - self._driver = None - if driver is not None: - try: - driver.close() - except Exception: # noqa: BLE001 — best-effort close - pass - - def _import_graphdatabase(self) -> Any: - try: - from neo4j import GraphDatabase - except ImportError as exc: - raise MemNetError( - "neo4j_unavailable", - "MEMNET_NEO4J_URL is set but the neo4j driver is not installed. " - "Install the optional client extra, or unset the URL and use " - "FakeDurableAdapter for local seam tests.", - example="pip install 'memnet-llm[neo4j]' # or: pip install neo4j", - ) from exc - return GraphDatabase - - def _ensure_driver(self) -> Any: - if self._driver is not None: - return self._driver - GraphDatabase = self._import_graphdatabase() - kwargs: dict[str, Any] = {} - if self.config.user is not None: - kwargs["auth"] = (self.config.user, self.config.password or "") - try: - driver = GraphDatabase.driver(self.config.url, **kwargs) - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_connect_failed", - f"Could not connect to Neo4j at configured URL: {type(exc).__name__}: {exc}", - example="Export MEMNET_NEO4J_URL to a reachable external cabinet", - ) from exc - self._driver = driver - return driver - - def _session(self) -> Any: - driver = self._ensure_driver() - try: - return driver.session(database=self.config.database_name) - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_connect_failed", - f"Could not open Neo4j session: {type(exc).__name__}: {exc}", - example="Check MEMNET_NEO4J_DATABASE (default neo4j)", - ) from exc - - def _run(self, cypher: str, params: dict[str, Any] | None = None) -> list[Any]: - with self._session() as session: - result = session.run(cypher, params or {}) - return list(result) - - -def _row_get(row: Any, index: int, key: str) -> Any: - """Support neo4j.Record, mapping rows, and tuple rows from mocks.""" - if isinstance(row, dict): - return row.get(key) - if hasattr(row, "get"): - try: - val = row.get(key) - if val is not None: - return val - except Exception: # noqa: BLE001 - pass - try: - return row[index] - except (IndexError, TypeError, KeyError): - return None diff --git a/parts/common/memnet/memnet/durable/neo4j_library.py b/parts/common/memnet/memnet/durable/neo4j_library.py deleted file mode 100644 index 8737cb4d..00000000 --- a/parts/common/memnet/memnet/durable/neo4j_library.py +++ /dev/null @@ -1,284 +0,0 @@ -"""Neo4j library namespace — locator-only read on a second database. - -Same Bolt URL / process as the cabinet ``Neo4jAdapter``. Extra **0.16**. - -- ``MEMNET_NEO4J_LIBRARY_DATABASE`` unset → skip (no second bind). -- If set, MUST differ from the cabinet database name - (``MEMNET_NEO4J_DATABASE``, default ``neo4j``). -- Emit locators only. Never ``generate``. Never hydrate session ``S``. -- Never flush session pins into the library database. - -Hid / elementId / ``_memnet_hid`` stay off the emit. Not a -``DurableStoreAdapter``. Extra **0.17** ``RagHostHook`` MAY consume -these locators; this port stays locators-only / ``generate=false``. -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from typing import Any - -from memnet.durable.neo4j import ( - DEFAULT_DATABASE, - ENV_DATABASE, - ENV_PASSWORD, - ENV_URL, - ENV_USER, - cypher_ident, -) -from memnet.exceptions import MemNetError - -ENV_LIBRARY_DATABASE = "MEMNET_NEO4J_LIBRARY_DATABASE" - -LOCATOR_KEYS = ("path", "document_id", "qname", "session", "url") -_OFF_EMIT = frozenset( - { - "_memnet_hid", - "_memnet_tag", - "hid", - "elementId", - "element_id", - "identity", - "generate", - } -) - - -def cabinet_database_name() -> str: - return (os.environ.get(ENV_DATABASE) or "").strip() or DEFAULT_DATABASE - - -def library_database_name() -> str | None: - name = (os.environ.get(ENV_LIBRARY_DATABASE) or "").strip() - return name or None - - -def locator_fields(props: dict[str, Any] | None) -> dict[str, str]: - """Keep locator properties only; strip hid / generate / cabinet stamps.""" - out: dict[str, str] = {} - if not props: - return out - for key, val in props.items(): - if key in _OFF_EMIT or str(key).startswith("_memnet"): - continue - if key not in LOCATOR_KEYS: - continue - text = "" if val is None else str(val).strip() - if text: - out[key] = text - return out - - -@dataclass(frozen=True) -class Neo4jLibraryConfig: - url: str - database: str - user: str | None = None - password: str | None = None - - @classmethod - def from_env(cls) -> Neo4jLibraryConfig | None: - url = (os.environ.get(ENV_URL) or "").strip() - name = library_database_name() - if not url or not name: - return None - cabinet = cabinet_database_name() - if name == cabinet: - raise MemNetError( - "neo4j_library_same_as_cabinet", - "MEMNET_NEO4J_LIBRARY_DATABASE must differ from the cabinet " - f"database ({cabinet!r}); one database is not two namespaces.", - example="export MEMNET_NEO4J_LIBRARY_DATABASE=library", - ) - cypher_ident(name, kind="database") - return cls( - url=url, - database=name, - user=(os.environ.get(ENV_USER) or "").strip() or None, - password=os.environ.get(ENV_PASSWORD) or None, - ) - - -def build_library_locator_cypher( - cue: str | None = None, - *, - limit: int = 50, -) -> tuple[str, dict[str, Any]]: - """MATCH corpus nodes; RETURN locator columns only. Never generate.""" - cap = max(1, int(limit)) - where = "WHERE n._memnet_tag IS NULL" - params: dict[str, Any] = {"limit": cap} - token = (cue or "").strip() - if token: - params["cue"] = token.lower() - where += ( - " AND (" - "toLower(toString(coalesce(n.path, ''))) CONTAINS $cue OR " - "toLower(toString(coalesce(n.document_id, ''))) CONTAINS $cue OR " - "toLower(toString(coalesce(n.qname, ''))) CONTAINS $cue OR " - "toLower(toString(coalesce(n.session, ''))) CONTAINS $cue OR " - "toLower(toString(coalesce(n.url, ''))) CONTAINS $cue" - ")" - ) - query = ( - f"MATCH (n)\n{where}\n" - "RETURN n.path AS path, n.document_id AS document_id, " - "n.qname AS qname, n.session AS session, n.url AS url\n" - "LIMIT $limit" - ) - return query, params - - -class Neo4jLibraryClient: - """Locator-only client for the library database. Not a cabinet adapter.""" - - generate = False - - def __init__(self, config: Neo4jLibraryConfig) -> None: - self.config = config - self._driver: Any = None - - @classmethod - def from_env(cls) -> Neo4jLibraryClient | None: - cfg = Neo4jLibraryConfig.from_env() - if cfg is None: - return None - return cls(cfg) - - @property - def name(self) -> str: - return "neo4j_library" - - @property - def database_name(self) -> str: - return self.config.database - - def emit_locators( - self, - cue: str | None = None, - *, - limit: int = 50, - ) -> list[dict[str, str]]: - """Read locator properties from the library namespace.""" - if self.generate: - raise MemNetError( - "neo4j_library_generate_forbidden", - "library port generate stays false", - ) - self._ensure_driver() - cypher, params = build_library_locator_cypher(cue, limit=limit) - try: - rows = self._run(cypher, params) - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_query_failed", - f"Neo4j library locator query failed: {type(exc).__name__}: {exc}", - example="Check MEMNET_NEO4J_LIBRARY_DATABASE / network", - ) from exc - locators: list[dict[str, str]] = [] - for row in rows: - loc = locator_fields(_row_as_mapping(row)) - if loc: - locators.append(loc) - return locators - - def hydrate(self, *_a: Any, **_k: Any) -> None: - raise MemNetError( - "neo4j_library_not_cabinet", - "library namespace MUST NOT hydrate session S; use Neo4jAdapter " - "on MEMNET_NEO4J_DATABASE.", - ) - - def flush(self, *_a: Any, **_k: Any) -> None: - raise MemNetError( - "neo4j_library_read_only", - "library namespace MUST NOT receive flushed session pins.", - ) - - def close(self) -> None: - driver = self._driver - self._driver = None - if driver is not None: - try: - driver.close() - except Exception: # noqa: BLE001 - pass - - def _import_graphdatabase(self) -> Any: - try: - from neo4j import GraphDatabase - except ImportError as exc: - raise MemNetError( - "neo4j_unavailable", - "MEMNET_NEO4J_LIBRARY_DATABASE is set but the neo4j driver is not installed.", - example="pip install 'memnet-llm[neo4j]'", - ) from exc - return GraphDatabase - - def _ensure_driver(self) -> Any: - if self._driver is not None: - return self._driver - GraphDatabase = self._import_graphdatabase() - kwargs: dict[str, Any] = {} - if self.config.user is not None: - kwargs["auth"] = (self.config.user, self.config.password or "") - try: - driver = GraphDatabase.driver(self.config.url, **kwargs) - except MemNetError: - raise - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_connect_failed", - f"Could not connect to Neo4j library URL: {type(exc).__name__}: {exc}", - example="Export MEMNET_NEO4J_URL to a reachable external process", - ) from exc - self._driver = driver - return driver - - def _session(self) -> Any: - driver = self._ensure_driver() - try: - return driver.session(database=self.config.database) - except Exception as exc: # noqa: BLE001 - raise MemNetError( - "neo4j_connect_failed", - f"Could not open Neo4j library database: {type(exc).__name__}: {exc}", - example="Check MEMNET_NEO4J_LIBRARY_DATABASE", - ) from exc - - def _run(self, cypher: str, params: dict[str, Any] | None = None) -> list[Any]: - with self._session() as session: - result = session.run(cypher, params or {}) - return list(result) - - -def make_library_client_from_env() -> Neo4jLibraryClient | None: - """Skip unless URL and library database name are both set.""" - return Neo4jLibraryClient.from_env() - - -def _row_as_mapping(row: Any) -> dict[str, Any]: - if isinstance(row, dict): - return row - if hasattr(row, "data") and callable(row.data): - try: - data = row.data() - if isinstance(data, dict): - return data - except Exception: # noqa: BLE001 - pass - if hasattr(row, "keys"): - try: - return {str(k): row[k] for k in row.keys()} - except Exception: # noqa: BLE001 - pass - out: dict[str, Any] = {} - for i, key in enumerate(LOCATOR_KEYS): - try: - out[key] = row[i] - except (IndexError, TypeError, KeyError): - continue - return out diff --git a/parts/common/memnet/memnet/rag_host_hook.py b/parts/common/memnet/memnet/rag_host_hook.py index c10e885d..c5ac1b37 100644 --- a/parts/common/memnet/memnet/rag_host_hook.py +++ b/parts/common/memnet/memnet/rag_host_hook.py @@ -1,8 +1,9 @@ """RagHostHook — host Snap locators outside MemNetSystem (extra 0.17). Given a cue, return locators or skip. Default = skip (fail-open). Optional -``MEMNET_HOST_SEARCH_URL`` (do not vendor a RAG server) and optional extra -0.16 ``MEMNET_NEO4J_LIBRARY_DATABASE`` locators. Shape stays ``pin_map`` of +``MEMNET_HOST_SEARCH_URL`` (do not vendor a RAG server). Neo4j library +locators are retired (#187); ``MEMNET_NEO4J_*`` is not a host source. +Shape stays ``pin_map`` of session S. Host search MUST NOT absorb. MUST NOT register MCP ``rag_query``. MUST NOT Snap-on-session (no ANN of S). Hid / generate / chunk bodies stay off the emit. @@ -22,8 +23,6 @@ from pathlib import Path from typing import Any, Protocol -from memnet.durable.neo4j_library import locator_fields as library_locator_fields -from memnet.durable.neo4j_library import make_library_client_from_env from memnet.gql import _emit_props from memnet.mutate_gate import MutateGate, MutateResult from memnet.pin_map_ingest import IngestResult, ingest_codebase, ingest_pcba, ingest_sysml @@ -153,7 +152,7 @@ def propose(self, cue: HostSearchCue) -> HostSearchResult: class EnvRagHostHook: - """Optional URL and/or 0.16 library locators. Miss / timeout → skip.""" + """Optional host URL. Miss / timeout → skip. Neo4j env is not a source.""" implemented = True @@ -162,7 +161,6 @@ def propose(self, cue: HostSearchCue) -> HostSearchResult: url = (os.environ.get(ENV_HOST_URL) or "").strip() if url: found.extend(_http_locators(url, cue)) - found.extend(_library_locators(cue)) deduped = _dedupe(found) if not deduped: return HostSearchResult( @@ -325,27 +323,6 @@ def _parse_host_json(raw: str) -> list[dict[str, str]]: return out -def _library_locators(cue: HostSearchCue) -> list[dict[str, str]]: - client = make_library_client_from_env() - if client is None: - return [] - try: - rows = client.emit_locators(cue.question, limit=cue.max_hits) - except Exception: # noqa: BLE001 — fail-open skip - return [] - finally: - try: - client.close() - except Exception: # noqa: BLE001 - pass - out: list[dict[str, str]] = [] - for row in rows: - loc = sanitise_locator(library_locator_fields(row)) - if loc: - out.append(loc) - return out - - def _maybe_ingest_path(session, loc: Mapping[str, str]) -> IngestResult | None: path_s = loc.get("path") or "" if not path_s: diff --git a/pyproject.toml b/pyproject.toml index c2b74e6d..3a08449c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,10 +52,6 @@ mcp = [ agensgraph = [ "psycopg[binary]>=3.1", ] -# Client driver only — does NOT install or vendor a Neo4j server. -neo4j = [ - "neo4j>=5.14", -] [project.scripts] memnet = "memnet.cli:main" @@ -94,7 +90,6 @@ pythonpath = [ markers = [ "scheme(id): integration scheme identifier from plan S01-S115", "agensgraph_live: needs MEMNET_AGENSGRAPH_URL pointing at external AgensGraph", - "neo4j_live: needs MEMNET_NEO4J_URL pointing at external Neo4j", ] [tool.ruff] diff --git a/sysml-models/README.md b/sysml-models/README.md index 96e037e1..a196e03c 100644 --- a/sysml-models/README.md +++ b/sysml-models/README.md @@ -10,11 +10,11 @@ Design authority: rebuilt requirements + ADR-001 (GQL agent wire) + `docs/gramma 1. **MemNet = mission working memory** — session-scoped NODE|EDGE buffer (brand SharedLlmMemory); **not** the search corpus, **not** GraphRAG. In-session recall = serial cue then `pin_map`; host RAG may propose locators only. 2. **Session as SSOT handle** — pass a mission SOMETHING by **session id only** (`SessionHandoff` / `SessionHandoffById`); module A→B pipe; peers re-`pin_map`; chat / MissionDock / HTTP never carry the graph. **sessionId = secret capability** (MUST NOT dump in chat/queue). -3. **Durable online GQL store** behind MemNet (`DurableBuffer` / AgensGraphAdapter + Neo4jAdapter) — **M2.5 / 0.7** Agens live hydrate/flush proven; Neo4j extra **0.14** `liveNeo4jClaimed=true` (live round-trip yes; hid flush; leftover-nickname hydrate after hid miss); extra **0.16** optional library database (locators only); cabinets external / not vendored. LLM↔store direct (Bolt) out of teach. +3. **Durable online GQL store** behind MemNet (`DurableBuffer` / AgensGraphAdapter) — **M2.5 / 0.7** claim is adapter + operator round trip (no runtime caller). Neo4j cabinet is on the archive shelf (#187). Cabinet external / not vendored. LLM↔store direct (Bolt) out of teach. 4. **Lead imports member working memory** — **happy path A** shared session → re-`pin_map` (no second store; **no ImportGuard**). Path B → `WorkingMemorySlice` through **optional** nested `ImportGuard` (`ImportGuardHook` shipped; `CheapLlmImportGuard` shipped #63) then `ImportAbsorb` (engine hard). Product verb = **import**. Colloquial "session merge" means this import only (no SessionMerge* types). Distinct from Cypher `MERGE` and micro id re-id `merge=true`. 5. **CapsPolicy ACL cut** — **as-is shipped** when session ACL is enabled: who, pin_map-vs-mutate, WorkerWriteScope hard reject, and optional SessionBind. `engineAclShipped=true`; ACL remains off by default. -**Sequence:** M1 (done) → M2 (done) → **M2.5** (0.7 live cabinet proven) → **M3** (0.8 in-repo playbook/app-note GQL rewrite, done). **1.0** = claim of 0.5–0.8. Design: **Snap this product model** (`ProjectMemNet`) into **multiple sessions** (catalog + package interiors); goldfish one interior. Not one ingest of the tree, not one session per file. [`docs/extras/memnet-session-strata.md`](../docs/extras/memnet-session-strata.md). +**Sequence:** M1 (done) → M2 (done) → **M2.5** (0.7 adapter + operator round trip, no runtime caller) → **M3** (0.8 in-repo playbook/app-note GQL rewrite, done). **1.0** = claim of 0.5–0.8. Design: **Snap this product model** (`ProjectMemNet`) into **multiple sessions** (catalog + package interiors); goldfish one interior. Not one ingest of the tree, not one session per file. [`docs/extras/memnet-session-strata.md`](../docs/extras/memnet-session-strata.md). ## Packages @@ -28,7 +28,7 @@ Design authority: rebuilt requirements + ADR-001 (GQL agent wire) + `docs/gramma | `models/behaviour.sysml` | `MemNetBehaviour` | HandoffById, SessionImportReceive, Multitask async, landed-client hydrate/flush | | `models/verify.sysml` | `MemNetVerification` | MN-VER-12-G00 + S01…S14; MN-VER-04-S01…S05; MN-VER-09-S01; MN-VER-13-S01; MN-VER-06-S01…S06 (S06 storage roles); MN-VER-01-S03 | | `models/root.sysml` | `ProjectMemNet` | Root imports (load last). MUST NOT import `MemNetArchive` | -| `models/archive.sysml` | `MemNetArchive` | ARCHIVE shelf (leftover_* / TierACodec / LegacyPipe*). **Off** `config.yaml` load | +| `models/archive.sysml` | `MemNetArchive` | ARCHIVE shelf (leftover_* / TierACodec / LegacyPipe* / retired Neo4j). **Off** `config.yaml` load | **One-page nest:** [outputs/product-nest-one-page.md](outputs/product-nest-one-page.md). @@ -48,7 +48,7 @@ MemNetSystem // SharedLlmMemory product └── DurableBuffer // one primary cabinet story APPLICATION LOOK CousinPointingContrast / HostSearchBridge (outside) -ARCHIVE LOOK MemNetArchive — leftover fog shelf; root does not import +ARCHIVE LOOK MemNetArchive — leftover fog + retired Neo4j (#187); root does not import OPS LOOK MemNetUsageDashboard — look only; not agent wire OPS FLEET MemNetOpsFleet — device MemNet services; one MemNet MCP at droplet (tip/ops; tip≠face) OPS ACCESS TipMemNetAccessPortal — invite + Google login + Bearer for keyed tip MCP (tip≠face; not sysmledge; portal sidecar) @@ -63,14 +63,14 @@ IMPLEMENTATION MemNetImplementation — SoftwareAllocate SSOT → live modul - **AgentMemory (SharedLlmMemory):** GraphStore, GqlCodec, **RecallCommit** (Recall: SessionOutline empty-q census + AgentShapedRead / PinMapShapedRead + BoundedMatchFind; Commit: MutateGate + RSV lease), SessionLifecycle - **MCP / CLI:** LLM ↔ MemNet only (not DurableBuffer as primary) -- **DurableBuffer:** AgensGraphAdapter **client** + 0.7 live hydrate/flush; Neo4jAdapter **client** (`liveNeo4jClaimed=true`, 0.14); cabinets external / not vendored +- **DurableBuffer:** AgensGraphAdapter **client**; 0.7 claim is adapter + operator round trip (no runtime caller); Neo4j retired to archive (#187); cabinet external / not vendored - **Multitask:** nested lead handoff + AsyncTaskDispatch + WorkerPool + import spine; MN-REQ-12 - **Path-B PinMapIngest:** all domains shipped (`memnet.pin_map_ingest`; CLI/MCP `ingest sysml|codebase|pcba|skills`). **CatalogSnap** 0.15: `memnet snap model` / MCP `snap_model` + `session_list` (`sessions|n/max`) + `session_close`. **PinMapExport** 0.19: cue `pin_map` GQL write-out (`memnet export pin-map`). Re-ingest / `.sysml` reverse (#66) not claimed. - **Optional soft policy:** `ImportGuard` nest (path B): `ImportGuardHook` shipped; `CheapLlmImportGuard` shipped (#63; env-gated); happy path A = re-pin without guard - **WorkerWriteScope:** CapsPolicy / MutateGate hard-rejects out-of-scope mutate when session ACL is enabled; overlap: serialise or **RSV** lease - **CapsPolicy ACL (as-is):** who / pin_map-vs-mutate / WorkerWriteScope hard reject / optional bind are shipped (`engineAclShipped=true`); MutateGate, PinMapShapedRead, and SessionHandoffEmit consult; ACL is off by default - **Out of scope:** novel-writer; EvidenceCentre / MissionDock / CompanyMemory / **HostSearchBridge** / **CousinPointingContrast** / **MemNetUsageDashboard** / **MemNetOpsFleet** / **TipMemNetAccessPortal** MUST NOT nest under MemNetSystem (optional host locators 0.17 / cousin pointing contrast / human usage look / device fleet with one droplet MemNet MCP tip/ops; product face is sysmledge / tip access portal is ops key gating, not that product) -- **ARCHIVE (off ProjectMemNet load):** leftover_* / TierACodec / LegacyPipe* honesty in `models/archive.sysml` (`MemNetArchive`). Root MUST NOT import it. Engine leftover codecs may remain on disk for tests; product teach is GQL only. +- **ARCHIVE (off ProjectMemNet load):** leftover_* / TierACodec / LegacyPipe* / retired Neo4j cabinet in `models/archive.sysml` (`MemNetArchive`). Root MUST NOT import it. Engine leftover codecs may remain on disk for tests; the Neo4j adapter module is removed. Product teach is GQL only. ### CapsPolicy ACL (as-is 0.8) diff --git a/sysml-models/models/archive.sysml b/sysml-models/models/archive.sysml index 35c5fc2a..2ebb6cfc 100644 --- a/sysml-models/models/archive.sysml +++ b/sysml-models/models/archive.sysml @@ -191,11 +191,92 @@ package MemNetArchive { attribute implemented : Boolean = false; } + // ===================================================================== + // Retired Neo4j cabinet (#187). Former product parts lived on + // DurableBuffer. No runtime hydrate/flush caller (agentReachable=false; + // hydrateFlushCallers="tests"). Bench: not faster at 80 / 229 / 504 + // nodes; JVM ~7 s / 811 MiB vs MemNet 0.87 s / 33 MiB; flush-then-hydrate + // did not return the flushed node set. MUST NOT nest on product path. + // ===================================================================== + + part def liveNeo4jClaimed { + doc /* + RETIRED archive — former honesty nest for extra 0.14 + liveNeo4jClaimed. Product path no longer claims it (#187). + MUST NOT nest on ProjectMemNet. + */ + attribute claimed : Boolean = false; + attribute productPathRetired : Boolean = true; + attribute mustNotNestOnProductPath : Boolean = true; + attribute productCommand : Boolean = false; + attribute retiredIssue : String = "187"; + } + + part def Neo4jAdapter { + doc /* + RETIRED archive — former cabinet client beside AgensGraphAdapter. + Ego hydrate/flush only. Opt-in was MEMNET_NEO4J_URL (extra + memnet-llm[neo4j]). Factory no longer binds it. Engine ignores + MEMNET_NEO4J_* and starts. MUST NOT nest on product path. + Historical 0.14 operator note (not a current claim): live round-trip + yes; hid flush; leftover-nickname hydrate after hid miss. + Hydrate-by-hid was not live-proven. + */ + part honesty : liveNeo4jClaimed; + attribute implemented : Boolean = false; + attribute productPathRetired : Boolean = true; + attribute mustNotNestOnProductPath : Boolean = true; + attribute productCommand : Boolean = false; + attribute liveNeo4jClaimed : Boolean = false; + attribute storageRole : String = "cabinet_ego"; + attribute hydrateByHidLiveProven : Boolean = false; + attribute serverVendored : Boolean = false; + attribute namespace : String = "cabinet"; + attribute phase : String = "retired"; + attribute retiredIssue : String = "187"; + } + + part def Neo4jLibraryPort { + doc /* + RETIRED archive — former extra 0.16 second Neo4j database on the + same process as Neo4jAdapter. Locator-only. Retired with the + cabinet (#187). MUST NOT nest on product path. MUST NOT hydrate + session S. RagHostHook no longer reads this port. + */ + attribute implemented : Boolean = false; + attribute productPathRetired : Boolean = true; + attribute mustNotNestOnProductPath : Boolean = true; + attribute productCommand : Boolean = false; + attribute generate : Boolean = false; + attribute skipUnlessNameSet : Boolean = true; + attribute hydrateSession : Boolean = false; + attribute flushSessionPins : Boolean = false; + attribute snapOnSession : Boolean = false; + attribute fuseRetrieveIntoPinMap : Boolean = false; + attribute storageRole : String = "library_locators"; + attribute rejectSameNameAsCabinet : Boolean = true; + attribute namespace : String = "library"; + attribute sameProcessAsCabinet : Boolean = true; + attribute retiredIssue : String = "187"; + } + + part def Neo4jLibraryLocatorRoadmap { + doc /* + RETIRED archive — former behaviour roadmap for library locators. + MUST NOT nest on product path. + */ + attribute productPathRetired : Boolean = true; + attribute mustNotNestOnProductPath : Boolean = true; + attribute productCommand : Boolean = false; + attribute retiredIssue : String = "187"; + } + part def LeftoverArchiveShelf { doc /* One shelf for leftover honesty. Not Schema. Not GraphStore. Not MemNetSystem. ProjectMemNet does not load this package. - Retired grammar, not a session archive. + Retired grammar, not a session archive. Retired Neo4j cabinet + sits on this shelf (#187). */ part leftoverLayer : TierACodec; part leftoverPipe : LegacyPipeImport; @@ -209,6 +290,10 @@ package MemNetArchive { part leftover_ingestIsIdRule : leftover_ingestIsIdRule; part leftover_read_get : leftover_read_get; part leftover_AssignedIdMap : leftover_AssignedIdMap; + part retiredNeo4jClaim : liveNeo4jClaimed; + part retiredNeo4j : Neo4jAdapter; + part retiredNeo4jLibrary : Neo4jLibraryPort; + part retiredNeo4jLibraryRoadmap : Neo4jLibraryLocatorRoadmap; } part leftoverArchiveShelf : LeftoverArchiveShelf; diff --git a/sysml-models/models/behaviour.sysml b/sysml-models/models/behaviour.sysml index 9f56ea19..d568eff5 100644 --- a/sysml-models/models/behaviour.sysml +++ b/sysml-models/models/behaviour.sysml @@ -224,17 +224,6 @@ package MemNetBehaviour { No MCP or CLI caller. */ } - attribute def EvLibrarySkipUnset { - doc /* - Extra 0.16: MEMNET_NEO4J_LIBRARY_DATABASE unset — no second bind. - */ - } - attribute def EvLibraryLocatorCue { - doc /* - Extra 0.16: cue the Neo4j library port for locator-only emit. - SHALL NOT hydrate S. SHALL NOT generate. - */ - } attribute def EvHostSearchCue { doc /* Extra 0.17: cue the outside RagHostHook (corpus Snap). SHALL NOT @@ -1407,13 +1396,11 @@ package MemNetBehaviour { Landed client (name kept): SessionLifecycle.hydrate_from_durable / flush_to_durable via DurableSyncOwner and DurableStoreAdapter (FakeDurableAdapter always; optional AgensGraphAdapter client with - liveCabinetClaimed; optional Neo4jAdapter client with - liveNeo4jClaimed=true, 0.14). MemNet remains sole agent-facing + liveCabinetClaimed). MemNet remains sole agent-facing memory; LLM ↔ DurableGraphStore direct (Bolt) is out of default - teach. Client seam landed; 0.7 Agens live hydrate/flush proven; - 0.14 Neo4j live round-trip yes; hid flush; leftover-nickname - hydrate after hid miss (MN-REQ-06.4). Extra 0.16 library - namespace is NOT this hydrate/flush path (locator emit only). + teach. Client seam landed. 0.7 claim is adapter + operator round + trip (no runtime caller) (MN-REQ-06.4). Neo4j cabinet is archived + (#187). Do not wire a new hydrate/flush caller. */ entry; then durableIdle; @@ -1455,41 +1442,7 @@ package MemNetBehaviour { then durableIdle; } - // --- Extra 0.16: Neo4j library locators (not hydrate/flush) --- - - state def Neo4jLibraryLocatorRoadmap { - doc /* - Same Neo4j process as cabinet; second database name. Skip when - MEMNET_NEO4J_LIBRARY_DATABASE is unset. Emit locators only. - MUST NOT hydrate S, MUST NOT flush session pins, MUST NOT generate. - */ - entry; - then libraryIdle; - - state libraryIdle { - doc /* Library name unset, or waiting for a locator cue. */ - } - transition idle_skip_unset - first libraryIdle - accept EvLibrarySkipUnset - then libraryIdle; - transition idle_to_emitting - first libraryIdle - accept EvLibraryLocatorCue - then emittingLocators; - - state emittingLocators { - doc /* Locator-only read of the library database. Not pin_map. */ - } - transition emit_ok - first emittingLocators - accept EvCommitOk - then libraryIdle; - transition emit_fail - first emittingLocators - accept EvError - then libraryIdle; - } + // Neo4jLibraryLocatorRoadmap: models/archive.sysml (#187). // --- Extra 0.17: host Snap locators (outside MemNetSystem) --- diff --git a/sysml-models/models/connections.sysml b/sysml-models/models/connections.sysml index 3ebc2712..3d684415 100644 --- a/sysml-models/models/connections.sysml +++ b/sysml-models/models/connections.sysml @@ -186,17 +186,15 @@ package MemNetConnections { item def DurableGraphStore { doc /* - Online durable property-graph store (AgensGraph-class or Neo4j-class) - behind MemNet. Landed client: hydrate into / flush from the live + Online durable property-graph store (AgensGraph-class) behind + MemNet. Landed client: hydrate into / flush from the live session an ego slice (DurableStoreAdapter / FakeDurableAdapter when no URL / optional AgensGraph client with liveCabinetClaimed; - optional Neo4j client with liveNeo4jClaimed=true (0.14); SessionLifecycle hydrate/flush). Factory binds exactly one of - fake, AgensGraph, or Neo4j. - Not a full copy of S. Not the file snapshot. - Extra 0.16: one Neo4j process MAY host two named databases — - cabinet (flushed S) vs library (corpus). Library is not this - hydrate/flush item. Live cabinet is external — not vendored. + fake or AgensGraph. 0.7 claim is adapter + operator round trip + (no runtime caller). Not a full copy of S. Not the file snapshot. + Neo4j cabinet is archived (#187). MEMNET_NEO4J_* is ignored. + Live cabinet is external — not vendored. LLM ↔ store direct (including Bolt) is out of default teach. One sync owner. MN-REQ-06.4. */ diff --git a/sysml-models/models/cousins.sysml b/sysml-models/models/cousins.sysml index 03bfeb6b..b8c67bce 100644 --- a/sysml-models/models/cousins.sysml +++ b/sysml-models/models/cousins.sysml @@ -9,8 +9,8 @@ Load: after requirements.sysml, before deploy.sysml (config.yaml). Root last. Recall/Commit operatorCount stays 2; cousin nest - cousinNestOwnsLiveNeo4jClaim stays false (product claim is - Neo4jAdapter.liveNeo4jClaimed on DurableBuffer, 0.14). + cousinNestOwnsLiveNeo4jClaim stays false (product Neo4j claim is + archived on MemNetArchive, #187; this nest does not own it). Python engine and leftover 0.9 store drop are out of this cut. */ package MemNetCousinContrast { @@ -308,8 +308,8 @@ package MemNetCousinContrast { not Pattern C tour, not Pattern D seeded RAG, not view=shell. Cousins confirm TARGET; they do not force a store-key back onto the wire. cousinNestOwnsLiveNeo4jClaim=false: this nest does not - own the product live Neo4j claim (that stays - Neo4jAdapter.liveNeo4jClaimed). GraphLite is a cabinet, not the product. Logseq pages + own a Neo4j claim (the product cabinet is archived, #187). + GraphLite is a cabinet, not the product. Logseq pages are the closest name-you-have cousin; SiYuan / NL2GQL are the leftover-id path. SysMLEdge is a distinct desk pin_map, not MemNet session strata and not SSOT. Downstream overlay family is diff --git a/sysml-models/models/deploy.sysml b/sysml-models/models/deploy.sysml index 1d28fd42..5575a440 100644 --- a/sysml-models/models/deploy.sysml +++ b/sysml-models/models/deploy.sysml @@ -3,8 +3,8 @@ Product framing (2026-08-13): MemNet = shared LLM memory (SharedLlmMemory); session id = pass-by-reference handle for a MissionWorkingSet; durable GQL - store (DurableBuffer / AgensGraphAdapter + optional Neo4jAdapter cabinet - + optional Neo4jLibraryPort) behind MemNet. + store (DurableBuffer / AgensGraphAdapter) behind MemNet. + Neo4jAdapter / Neo4jLibraryPort / liveNeo4jClaimed: models/archive.sysml. Nesting (coarse → fine): MemNetSystem // SharedLlmMemory product @@ -33,8 +33,8 @@ ├── TransportBoundary // InProcess / LocalIpc / TcpServe ├── MemNetMcpServer // MemNet tip/ops MCP (GQL); fleet: one at droplet; tip≠face │ └── McpFacade // TARGET cue/pattern commands only - ├── DurableBuffer // AgensGraphAdapter + Neo4jAdapter cabinet - │ // + optional Neo4jLibraryPort (0.16; skip unless name) + ├── DurableBuffer // AgensGraphAdapter; fake|agensgraph + │ // Neo4j cabinet: models/archive.sysml (#187) ├── PinMapRoadmap // PinMapIngest_* + CatalogSnap (0.15) + PinMapExport (0.19) └── MultitaskOperatingModel ├── MultitaskCoordinator // team lead @@ -289,19 +289,7 @@ package MemNet { // leftover_* / TierACodec / LegacyPipeImport part defs: models/archive.sysml // (MemNetArchive; off ProjectMemNet load). Product nest uses flags only. - - part def liveNeo4jClaimed { - doc /* - Honesty nest so find/pin_map see liveNeo4jClaimed. Product claim - is extra 0.14 (operator proof rpi5-syson): live round-trip yes; - hid flush; leftover-nickname hydrate after hid miss. That claim - is the adapter plus the 0.14 operator round trip only, not a - runtime path. serve, IPC, and MCP bind the owner at startup and - do not invoke it. Callers are DurableBuffer.hydrateFlushCallers. - Not leftover mint. Not a 1.0 gate. Hid stays off the wire. - */ - attribute claimed : Boolean = true; - } + // Neo4jAdapter / Neo4jLibraryPort / liveNeo4jClaimed: same shelf (#187). part def IdAllocator { doc /* @@ -1668,8 +1656,8 @@ package MemNet { via DurableStoreAdapter. Same GQL family. Client seam landed (memnet.durable: FakeDurableAdapter always; optional AgensGraphAdapter under memnet-llm[agensgraph]; SessionLifecycle.hydrate_from_durable / - flush_to_durable; one DurableSyncOwner). Live cabinet is external / - operator-proven (0.7 hydrate/flush) — not vendored in-repo. + flush_to_durable; one DurableSyncOwner). 0.7 claim is adapter + + operator round trip (no runtime caller). Server not vendored. MUST NOT dual-write without a single sync owner. MUST NOT be the LLM primary path (LLM ↔ MemNet only). Ego slice, not a full copy of S. storageRole cabinet_ego. Callers are @@ -1682,120 +1670,43 @@ package MemNet { // Client hydrate/flush adapter seam (Fake + optional AgensGraph client). attribute storageRole : String = "cabinet_ego"; attribute implemented : Boolean = true; - // liveCabinetClaimed: 0.7 hydrate/flush proof. The claim is the - // adapter plus the operator round trip only, not a runtime path. - // serve, IPC, and MCP bind the owner at startup and do not invoke - // it. Callers are DurableBuffer.hydrateFlushCallers. + // liveCabinetClaimed: 0.7 claim is adapter + operator round trip + // (no runtime caller). serve, IPC, and MCP bind the owner at startup + // and do not invoke it. Callers are DurableBuffer.hydrateFlushCallers. attribute liveCabinetClaimed : Boolean = true; attribute phase : String = "M2.5"; } - part def Neo4jAdapter { - doc /* - Alternative cabinet client beside AgensGraphAdapter. Ego - hydrate/flush only. Opt-in via MEMNET_NEO4J_URL (extra - memnet-llm[neo4j]; official driver only). Factory binds exactly - one of fake, AgensGraph, or Neo4j (one DurableSyncOwner; - bindsExactlyOneOf). Not the session file snapshot. Not - Neo4jLibraryPort. Does not run on Commit. Does not run on TTL - (MN-REQ-01.3). Cabinet is external; this repo does not vendor a - Neo4j server. Callers are DurableBuffer.hydrateFlushCallers. - liveNeo4jClaimed=true (extra 0.14): live round-trip yes; hid - flush; leftover-nickname hydrate after hid miss. - Hydrate-by-hid is not live-proven. pytest.mark.neo4j_live skips - unless MEMNET_NEO4J_URL. Agents MUST NOT talk Bolt. - Cabinet namespace = MEMNET_NEO4J_DATABASE (default neo4j). - Extra 0.16 library is a sibling port, not this adapter. - MUST NOT bind the library database name as the cabinet. - MN-REQ-06.4. - */ - port hydrateOut : DurableHydrateOutPort; - port flushIn : DurableFlushInPort; - port gqlIn : GqlWireInPort; - port gqlOut : GqlWireOutPort; - part honesty : liveNeo4jClaimed; - attribute implemented : Boolean = true; - // liveNeo4jClaimed: extra 0.14 operator round trip only, not a - // runtime path. serve, IPC, and MCP bind the owner at startup and - // do not invoke it. Callers are DurableBuffer.hydrateFlushCallers. - attribute liveNeo4jClaimed : Boolean = true; - attribute storageRole : String = "cabinet_ego"; - attribute hydrateByHidLiveProven : Boolean = false; - attribute serverVendored : Boolean = false; - attribute namespace : String = "cabinet"; - attribute phase : String = "M2.5"; - } - - part def Neo4jLibraryPort { - doc /* - Extra 0.16: second named Neo4j database on the SAME process as - Neo4jAdapter (same MEMNET_NEO4J_URL). Library = corpus graph. - Skip unless MEMNET_NEO4J_LIBRARY_DATABASE is set (no second bind). - Locator-only read (LibraryLocator). generate=false. MUST NOT - hydrate session S, MUST NOT flush session pins into the library - database, MUST NOT fuse RRF / PPR / Leiden / ANN into pin_map, - MUST NOT Snap-on-session. Not a DurableStoreAdapter. Extra 0.17 - MAY consume these locators via RagHostHook outside MemNetSystem; - this port stays locators-only / generate=false (not goldfish). - Hid / elementId / _memnet_hid stay off the emit. - Engine-real: reject when MEMNET_NEO4J_LIBRARY_DATABASE equals the - cabinet database name (same process, two names). - */ - port locatorOut : LibraryLocatorOutPort; - attribute implemented : Boolean = true; - attribute generate : Boolean = false; - attribute skipUnlessNameSet : Boolean = true; - attribute hydrateSession : Boolean = false; - attribute flushSessionPins : Boolean = false; - attribute snapOnSession : Boolean = false; - attribute fuseRetrieveIntoPinMap : Boolean = false; - attribute storageRole : String = "library_locators"; - attribute rejectSameNameAsCabinet : Boolean = true; - attribute namespace : String = "library"; - attribute sameProcessAsCabinet : Boolean = true; - } - part def DurableBuffer { doc /* Optional durable property-graph buffer behind MemNet SharedLlmMemory. Connects to GraphStore / SessionLifecycle via hydrate/flush — not to - LLM agents as primary. Children are alternative cabinet clients; - factory binds exactly one adapter (one sync owner). AgensGraph live - cabinet proven (0.7). Neo4j live claimed (0.14): live round-trip - yes; hid flush; leftover-nickname hydrate after hid miss. TARGET: - no grammar store key; MERGE is a per-write lookup of labels + LLM agents as primary. Factory binds exactly one adapter (one sync + owner): fake or AgensGraph (bindsExactlyOneOf="fake|agensgraph"). + 0.7 claim is adapter + operator round trip (no runtime caller). + TARGET: no grammar store key; MERGE is a per-write lookup of labels + props / rel type+ends, not a primary key. leftoverMergeById=true records 0.9 nickname MERGE and invented by_id. elementId stays in - the cabinet, off the wire. Cabinets stay external / not vendored. - Skip live Neo4j pytest unless MEMNET_NEO4J_URL. Extra 0.16: - optional library namespace is a second database name on the same - Neo4j URL (Neo4jLibraryPort); skip unless that name is set. Two - cabinet URLs still require MEMNET_DURABLE_BACKEND. Factory SHALL - NOT treat two database names as two URLs. Factory binds exactly - one of fake, AgensGraph, or Neo4j (bindsExactlyOneOf). An - unconfigured URL binds FakeDurableAdapter. hydrateFlushCallers - names who calls hydrate/flush. MN-REQ-06.4. + the cabinet, off the wire. Cabinet stays external / not vendored. + Neo4jAdapter, Neo4jLibraryPort, and liveNeo4jClaimed are retired + to MemNetArchive (#187). MEMNET_NEO4J_* on a host is ignored + (retiredNeo4jEnvIgnored). One log warning. The engine starts. + SHALL NOT crash. SHALL NOT bind a Neo4j adapter. An unconfigured + URL binds FakeDurableAdapter. hydrateFlushCallers names who calls + hydrate/flush. Do not wire a new caller. MN-REQ-06.4. */ part agens : AgensGraphAdapter; - part neo4j : Neo4jAdapter; - part library : Neo4jLibraryPort; port hydrateOut : DurableHydrateOutPort; port flushIn : DurableFlushInPort; attribute storageRole : String = "cabinet_ego"; attribute hydrateFlushCallers : String = "tests"; - attribute bindsExactlyOneOf : String = "fake|agensgraph|neo4j"; + attribute bindsExactlyOneOf : String = "fake|agensgraph"; attribute clientLanded : Boolean = true; - // liveCabinetClaimed: 0.7 operator round trip only, not a runtime - // path. serve, IPC, and MCP bind the owner at startup and do not - // invoke it. Callers are hydrateFlushCallers. + attribute retiredNeo4jEnvIgnored : Boolean = true; + // liveCabinetClaimed: 0.7 claim is adapter + operator round trip + // (no runtime caller). serve, IPC, and MCP bind the owner at startup + // and do not invoke it. Callers are hydrateFlushCallers. attribute liveCabinetClaimed : Boolean = true; - // liveNeo4jClaimed: 0.14 operator round trip only, not a runtime - // path. serve, IPC, and MCP bind the owner at startup and do not - // invoke it. Callers are hydrateFlushCallers. - attribute liveNeo4jClaimed : Boolean = true; - attribute twoNamespaces : Boolean = true; - attribute sameNeo4jProcess : Boolean = true; - attribute twoUrlsStillNeedBackendPick : Boolean = true; attribute mergeByLabelsAndProps : Boolean = true; attribute leftoverMergeById : Boolean = true; attribute mergeIsPerWriteLookup : Boolean = true; @@ -2679,7 +2590,7 @@ package MemNet { doc /* SharedLlmMemory product. Nest: AgentMemory (via core; RecallCommit two-operator cut) + Transport + MCP (LLM ↔ MemNet) + DurableBuffer - (cabinet hydrate/flush + optional 0.16 library locator port) + (cabinet hydrate/flush; fake|agensgraph; Neo4j archived #187) PinMapRoadmap (PinMapIngest_* + CatalogSnap 0.15 + PinMapExport 0.19) + MultitaskOperatingModel (Coordinator → HandoffEmit / AsyncTaskDispatch / SessionImportReceive; WorkerPool). @@ -2760,8 +2671,8 @@ package MemNet { part def RagHostHook { doc /* - Optional host corpus plug-in (grep / docs MCP / optional 0.16 - library locators / fail-open URL). Product compression = Snap + Optional host corpus plug-in (grep / docs MCP / fail-open URL). + Neo4j library locators are retired (#187). Product compression = Snap (topics / locators from the library haystack). Locator-only, fail-open, MUST NOT mutate the session, MUST NOT appear as memnet-mcp rag_query, MUST NOT ANN-index session S @@ -2786,8 +2697,8 @@ package MemNet { MUST NOT be a part of MemNetSystem. Snap = RagHostHook locators; Shape stays PinMapShapedRead. Hard commit is existing MutateGate / PinMapIngest — no LocatorCommit wrapper. MUST NOT Snap-on-session. - Extra 0.16 library locators MAY feed locatorIn; hook consumes - them as locators only (library generate stays false). + Neo4j library locators are retired (#187). Hook consumes host + locators only (generate stays off the emit). */ attribute designOnly : Boolean = false; part hook : RagHostHook; diff --git a/sysml-models/models/implementation.sysml b/sysml-models/models/implementation.sysml index 8e74e4a0..784621c2 100644 --- a/sysml-models/models/implementation.sysml +++ b/sysml-models/models/implementation.sysml @@ -198,14 +198,6 @@ package MemNetImplementation { attribute path : String = "parts/common/memnet/memnet/durable/agensgraph.py"; attribute pyName : String = "memnet.durable.agensgraph"; } - part def Neo4jAdapterMod { - attribute path : String = "parts/common/memnet/memnet/durable/neo4j.py"; - attribute pyName : String = "memnet.durable.neo4j"; - } - part def Neo4jLibraryPortMod { - attribute path : String = "parts/common/memnet/memnet/durable/neo4j_library.py"; - attribute pyName : String = "memnet.durable.neo4j_library"; - } part def RagHostHookMod { attribute path : String = "parts/common/memnet/memnet/rag_host_hook.py"; attribute pyName : String = "memnet.rag_host_hook"; @@ -284,8 +276,6 @@ package MemNetImplementation { part exportMod : PinMapExportMod; part catalogMod : CatalogSnapMod; part agensMod : AgensGraphAdapterMod; - part neo4jMod : Neo4jAdapterMod; - part libraryMod : Neo4jLibraryPortMod; part ragHookMod : RagHostHookMod; part schemaMod : SchemaRegistryMod; part snapMod : SnapshotStoreMod; @@ -438,14 +428,6 @@ package MemNetImplementation { end logical ::> memNetSystem.durable.agens; end code ::> agensMod; } - allocation neo4jToMod : SoftwareAllocate { - end logical ::> memNetSystem.durable.neo4j; - end code ::> neo4jMod; - } - allocation libraryToMod : SoftwareAllocate { - end logical ::> memNetSystem.durable.library; - end code ::> libraryMod; - } allocation ragHookToMod : SoftwareAllocate { end logical ::> hostSearch.hook; end code ::> ragHookMod; diff --git a/sysml-models/models/requirements.sysml b/sysml-models/models/requirements.sysml index cfcb55ce..735ac062 100644 --- a/sysml-models/models/requirements.sysml +++ b/sysml-models/models/requirements.sysml @@ -525,7 +525,7 @@ package MemNetRequirements { if exemplars collide on a name cue (two roots stay two). SHALL NOT: third operator; required property id; elementId on the wire; invent a store key; SameThingAbsorb as this cut; - revive Layer; flip liveNeo4jClaimed. + revive Layer; restore the retired Neo4j cabinet. */ attribute requirementId : String = "MN-REQ-04.9"; } @@ -636,39 +636,31 @@ package MemNetRequirements { requirement def MN_REQ_06_4_DurableStoreBehindMemNet { doc /* - When a durable online GQL store (AgensGraph-class or Neo4j-class) - is present, MemNet SHALL remain the sole agent-facing shared - memory. The store SHALL sit behind MemNet (hydrate/flush). LLM ↔ - store direct (Bolt / driver) SHALL NOT be the default teach path. + When a durable online GQL store (AgensGraph-class) is present, + MemNet SHALL remain the sole agent-facing shared memory. The + store SHALL sit behind MemNet (hydrate/flush). LLM ↔ store + direct (Bolt / driver) SHALL NOT be the default teach path. M2.5 client hydrate/flush adapter seam IS landed (Fake always; - optional AgensGraph client with liveCabinetClaimed; optional - Neo4j client with liveNeo4jClaimed=true after operator live - round-trip (0.14: live round-trip yes; hid flush; - leftover-nickname hydrate after hid miss); one DurableSyncOwner). + optional AgensGraph client with liveCabinetClaimed; one + DurableSyncOwner). Factory binds exactly one of fake or + AgensGraph (bindsExactlyOneOf="fake|agensgraph"). hydrateFlushCallers="tests" on DurableBuffer: no non-test caller of hydrate/flush. serve, IPC, and MCP bind the owner - at startup and SHALL NOT be read as invoking it. - liveCabinetClaimed and liveNeo4jClaimed are the adapter plus - the operator round trip only (Agens 0.7, Neo4j 0.14), not a - runtime path. - Live external cabinet SHALL NOT be claimed shipped until - operator-proven. Neither AgensGraph nor Neo4j servers SHALL be - vendored in-repo. Two cabinet URLs SHALL require an explicit - backend pick (MUST NOT silently bind two owners). Live Neo4j - tests SHALL skip unless MEMNET_NEO4J_URL. Hid SHALL stay off - the wire. Extra 0.16: a second Neo4j *database name* on the - same Neo4j URL is allowed as the library namespace (cabinet - vs library). SHALL NOT treat two database names as two cabinet - URLs. When the library database name is set, it SHALL differ - from the cabinet database name (same process, two names; - rejectSameNameAsCabinet). Library port SHALL emit locators - only (generate=false). SHALL NOT hydrate S from the library - database; SHALL NOT flush session pins into the library - database. SHALL NOT fuse retrieve rankers (RRF / PPR / Leiden - / ANN) into Recall / pin_map. HostSearch / RagHostHook SHALL - remain outside MemNetSystem. Extra 0.17: RagHostHook.implemented - SHALL be true outside MemNetSystem (skip is valid); SHALL NOT - nest the hook under MemNetSystem; SHALL NOT add rag_query MCP. + at startup and SHALL NOT be read as invoking it. SHALL NOT + wire a new hydrate/flush caller. The 0.7 claim is adapter + + operator round trip (no runtime caller). liveCabinetClaimed + records that operator round trip. The AgensGraph server SHALL + NOT be vendored in-repo. Neo4jAdapter, Neo4jLibraryPort, and + liveNeo4jClaimed are retired on MemNetArchive (#187). When + MEMNET_NEO4J_URL or any other MEMNET_NEO4J_* variable is set, + the engine SHALL ignore it and start. One log warning is + enough. SHALL NOT crash. SHALL NOT bind a Neo4j adapter. + Hid SHALL stay off the wire. SHALL NOT fuse retrieve rankers + (RRF / PPR / Leiden / ANN) into Recall / pin_map. HostSearch / + RagHostHook SHALL remain outside MemNetSystem. Extra 0.17: + RagHostHook.implemented SHALL be true outside MemNetSystem + (skip is valid); SHALL NOT nest the hook under MemNetSystem; + SHALL NOT add rag_query MCP. SHALL NOT read a Neo4j library. */ attribute requirementId : String = "MN-REQ-06.4"; } diff --git a/sysml-models/models/verify.sysml b/sysml-models/models/verify.sysml index 43e36dc7..3577c7f3 100644 --- a/sysml-models/models/verify.sysml +++ b/sysml-models/models/verify.sysml @@ -17,10 +17,10 @@ GraphGlot parse front + product gate (MN-VER-09-S01). RecallCommit modelled (MN-VER-13-S01); leftover goldfish claimed in 0.5. SameThingAbsorb is a Commit rule (MN-VER-13-S02); Recall SHALL NOT absorb. - DurableBuffer honesty (MN-VER-06-S01): Agens liveCabinetClaimed; - Neo4j client implemented with liveNeo4jClaimed=true (0.14); - extra 0.16 library port locator-only (generate=false; - rejectSameNameAsCabinet); RagHostHook.implemented=true required + DurableBuffer honesty (MN-VER-06-S01): Agens liveCabinetClaimed + is adapter + operator round trip (no runtime caller); + bindsExactlyOneOf=fake|agensgraph; Neo4j cabinet archived (#187); + MEMNET_NEO4J_* ignored; RagHostHook.implemented=true required outside MemNetSystem (hostSearchInsideSystem stays false). Human usage look (MN-VER-06-S02): MemNetUsageDashboard look-only outside MemNetSystem; HTTP/UI parked; no manage/mutate ports. @@ -432,10 +432,10 @@ package MemNetVerification { MN-REQ-09.5 — GraphGlot is the official-grammar parse front (implemented only because Python calls graphglot). ProductGqlGate is a separate part after parse (WITH / UNWIND / CALL / unbounded RETURN / Layer). No TCK - claim. Product gate still after parse. liveNeo4jClaimed stays true - (0.14). Recall/Commit still two operators. RagHostHook stays outside - MemNetSystem. Peak_L stays last-resort under RelativeSeed only. - Extra 0.16 library locators-only (generate=false). Hid / elementId / + claim. Product gate still after parse. Neo4j cabinet stays + archived (#187); this cut does not restore it. Recall/Commit still + two operators. RagHostHook stays outside MemNetSystem. Peak_L stays + last-resort under RelativeSeed only. Hid / elementId / _memnet_hid stay off CLI/MCP/GQL emit. */ attribute verificationId : String = "MN-VER-09-S01"; @@ -460,9 +460,7 @@ package MemNetVerification { .productGate.rejectUnboundedReturn == true and system.core.transport.inProcess.memory.sessions.gqlWire .productGate.rejectLayer == true - and system.durable.neo4j.liveNeo4jClaimed == true - and system.durable.liveNeo4jClaimed == true - and system.durable.library.generate == false + and system.durable.retiredNeo4jEnvIgnored == true and system.core.transport.inProcess.memory.sessions.recallCommit .operatorCount == 2 and system.core.transport.inProcess.memory.sessions.recallCommit @@ -515,8 +513,8 @@ package MemNetVerification { Q; ShapeWalk walks from Q under one M. requireStoreKeyAnchor=false; inventOnEmpty=false. Empty q is SessionOutline (emptyCueIsOutline); leftover_empty_seed_skip is leftover, not TARGET. leftover copy-id / - require_anchor is leftover. operatorCount stays 2. Product - liveNeo4jClaimed is true (0.14; not this RelativeSeed cut). + require_anchor is leftover. operatorCount stays 2. Neo4j cabinet + stays archived (#187; not this RelativeSeed cut). */ attribute verificationId : String = "MN-VER-04-S02"; @@ -587,7 +585,7 @@ package MemNetVerification { and system.mcp.tools.pinMap.walk.requireStoreKeyAnchor == false and system.core.transport.inProcess.memory.sessions.recallCommit .operatorCount == 2 - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true } } } @@ -679,7 +677,7 @@ package MemNetVerification { and system.mcp.tools.leftoverEmptySeedSkipCommandNested == false and system.core.transport.inProcess.memory.sessions.recallCommit .commit.sameThing.implemented == false - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true } } } @@ -691,8 +689,8 @@ package MemNetVerification { PinMapShapedRead). implemented=true (engine path). defaultGoldfish=false. Codebook miss only; empty q stays outline. ρ* not raw contains degree. Two peaks CueConflict. Not a third - operator. operatorCount stays 2. Product liveNeo4jClaimed is true - (0.14; not this Peak cut). Peak_L is this verification's subject, + operator. operatorCount stays 2. Neo4j cabinet stays archived + (#187; not this Peak cut). Peak_L is this verification's subject, not HostSearch. RagHostHook.implemented=true outside MemNetSystem (0.17 law); hostSearchInsideSystem=false. */ @@ -757,7 +755,7 @@ package MemNetVerification { .recall.shapedRead.pinMap.defaultGoldfish == false and system.core.transport.inProcess.memory.sessions.recallCommit .recall.shapedRead.pinMap.codebookMissOnly == true - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true } } } @@ -818,8 +816,8 @@ package MemNetVerification { requireAnchorId=false; createRequiresId=false; idFirstRequired=false; IdAllocator.implemented=false. Honesty leftover: leftoverByIdIndex (leftover_by_id), leftoverRequireAnchor, leftoverIdFirst, - leftoverNewMint, leftover_ingestIsIdRule. Product - liveNeo4jClaimed is true (0.14). No IdentityHandle nest — + leftoverNewMint, leftover_ingestIsIdRule. Neo4j cabinet stays + archived (#187). No IdentityHandle nest — leftover named parts beside flags. Recall/Commit nest not thickened. Hid stays off the wire. */ @@ -880,7 +878,7 @@ package MemNetVerification { .commit.noReplacementStoreKey == true and system.durable.mergeIsPerWriteLookup == true and system.durable.noReplacementStoreKey == true - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true and system.pinMaps.interface.leftoverLocatorMint == true and system.multitask.coordinator.importReceive.absorb.idPolicy .remint.leftoverInventedMint == true @@ -899,8 +897,8 @@ package MemNetVerification { cue-without-store-key and neighbourhood emit. Cue |Q|>1 stays RelativeSeed bounded + honest; no third operator. operatorCount stays 2. Cousin nest cousinNestOwnsLiveNeo4jClaim stays false - (product claim is Neo4jAdapter.liveNeo4jClaimed / DurableBuffer - 0.14). Catalog Snap SHALL NOT ANN-rank sessions, + (product Neo4j claim is archived, #187; this nest does not own it). + Catalog Snap SHALL NOT ANN-rank sessions, Absorb a whole S, or mint one session per REQ. Contrast MUST NOT nest under MemNetSystem. */ @@ -1005,8 +1003,8 @@ package MemNetVerification { leftover_read_get, NEW, AssignedIdMap, by_id, MERGE-by-id, require_anchor, allocate_from_locator, and id_policy keyed on store-key MUST NOT nest as product commands (leftover_* engine - honesty; TARGET implemented=false). Product liveNeo4jClaimed is - true (0.14; not this command-list cut). No Layer revival. + honesty; TARGET implemented=false). Neo4j cabinet stays archived + (#187; not this command-list cut). No Layer revival. */ attribute verificationId : String = "MN-VER-07-S01"; @@ -1107,7 +1105,7 @@ package MemNetVerification { .leftoverIdPolicyIsProductCommand == false and system.multitask.coordinator.importReceive.absorb.idPolicy .remint.productCommand == false - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true } } } @@ -1115,21 +1113,16 @@ package MemNetVerification { verification def MN_VER_06_S01_DurableCabinetHonesty { doc /* MN-REQ-06.4 — DurableBuffer nest honesty (ImportGuard-style flags). - AgensGraphAdapter.liveCabinetClaimed remains the 0.7 live cabinet. - Neo4jAdapter.implemented is the client; liveNeo4jClaimed=true - after 0.14 operator proof (live round-trip yes; hid flush; - leftover-nickname hydrate after hid miss). Extra 0.16: cabinet - vs library are two named databases on one Neo4j process; - library.generate=false; skipUnlessNameSet; hydrateSession=false; - fuseRetrieveIntoPinMap=false; rejectSameNameAsCabinet=true - (library namespace differs from cabinet). One DurableBuffer; - one sync owner. HostSearchBridge stays outside MemNetSystem; + AgensGraphAdapter.liveCabinetClaimed is the 0.7 claim: adapter + + operator round trip (no runtime caller). Factory binds exactly + one of fake or AgensGraph. Neo4jAdapter / Neo4jLibraryPort / + liveNeo4jClaimed are on MemNetArchive (#187). MEMNET_NEO4J_* is + ignored (retiredNeo4jEnvIgnored). One DurableBuffer; one sync + owner. HostSearchBridge stays outside MemNetSystem; RagHostHook.implemented=true (0.17) with fail-open locators; - hostSearchInsideSystem stays false. Skip unless - MEMNET_NEO4J_URL. Server not vendored. - hydrateFlushCallers="tests": no non-test caller. The live claims - are the adapter plus the operator round trip only, not a runtime - path. serve, IPC, and MCP bind the owner and do not invoke it. + hostSearchInsideSystem stays false. Server not vendored. + hydrateFlushCallers="tests": no non-test caller. serve, IPC, and + MCP bind the owner and do not invoke it. Do not wire a new caller. */ attribute verificationId : String = "MN-VER-06-S01"; @@ -1141,26 +1134,10 @@ package MemNetVerification { require constraint { system.durable.agens.implemented == true and system.durable.agens.liveCabinetClaimed == true - and system.durable.neo4j.implemented == true - and system.durable.neo4j.liveNeo4jClaimed == true and system.durable.hydrateFlushCallers == "tests" - and system.durable.neo4j.honesty.claimed == true - and system.durable.neo4j.namespace == "cabinet" + and system.durable.bindsExactlyOneOf == "fake|agensgraph" and system.durable.liveCabinetClaimed == true - and system.durable.liveNeo4jClaimed == true - and system.durable.twoNamespaces == true - and system.durable.sameNeo4jProcess == true - and system.durable.twoUrlsStillNeedBackendPick == true - and system.durable.library.generate == false - and system.durable.library.skipUnlessNameSet == true - and system.durable.library.hydrateSession == false - and system.durable.library.flushSessionPins == false - and system.durable.library.fuseRetrieveIntoPinMap == false - and system.durable.library.rejectSameNameAsCabinet == true - and system.durable.library.namespace == "library" - and system.durable.library.sameProcessAsCabinet == true - and system.durable.neo4j.namespace - != system.durable.library.namespace + and system.durable.retiredNeo4jEnvIgnored == true and system.core.transport.inProcess.memory.sessions.recallCommit .commit.mutate.libraryIngestThisCut == true and system.core.transport.inProcess.memory.sessions.recallCommit @@ -1436,8 +1413,8 @@ package MemNetVerification { find emit Q honestly; CueConflict on shaped emit when |Q|>1 (Q listed, cardinality visible; not a product command). Same name is a candidate only. SameThingAbsorb MAY collapse under Commit after that mark, - distinct from ImportAbsorb. operatorCount stays 2. Product - liveNeo4jClaimed is true (0.14; not this absorb cut). + distinct from ImportAbsorb. operatorCount stays 2. Neo4j cabinet + stays archived (#187; not this absorb cut). */ attribute verificationId : String = "MN-VER-13-S02"; @@ -1450,7 +1427,7 @@ package MemNetVerification { require constraint { system.core.transport.inProcess.memory.sessions.recallCommit .operatorCount == 2 - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true and system.core.transport.inProcess.memory.sessions.recallCommit .recall.absorbs == false and system.core.transport.inProcess.memory.sessions.recallCommit @@ -1531,8 +1508,8 @@ package MemNetVerification { Look = pin_map; join = Absorb slice. MUST NOT ANN-rank sessions; Absorb whole S; dump one model into one session; Layer; one session per REQ; invent a store key; hid on the wire. operatorCount stays 2. - Catalog Snap SHALL NOT own the live Neo4j claim (this cut's fence). - Product DurableBuffer liveNeo4jClaimed is true (0.14). + Catalog Snap SHALL NOT restore the retired Neo4j cabinet + (#187; this cut's fence). Cross-cut satisfy locators on the catalog (MN-REQ-11.17.1); interiors have no dangling dest; SysMLEdge pin_map is not this catalog. */ @@ -1573,7 +1550,7 @@ package MemNetVerification { and system.mcp.tools.sessionList.requiresStoreKeyId == false and system.core.transport.inProcess.memory.sessions.recallCommit .operatorCount == 2 - and system.durable.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true } } } @@ -1638,13 +1615,13 @@ package MemNetVerification { verification def MN_VER_06_S06_StorageRoles { doc /* storageRole only where bytes outlive the call: working_memory, - file_snapshot, cabinet_ego, library_locators. Explicit save and - expire-save are one blob. Factory binds exactly one of fake, - AgensGraph, or Neo4j. hydrateFlushCallers="tests": no non-test - caller of hydrate/flush. serve, IPC, and MCP bind the owner and - do not invoke it. The live claims are the adapter plus the - operator round trip only. Hydrate-by-hid is not live-proven. - Archive stays off load. No behaviour change. + file_snapshot, cabinet_ego. Explicit save and expire-save are one + blob. Factory binds exactly one of fake or AgensGraph. + hydrateFlushCallers="tests": no non-test caller of hydrate/flush. + serve, IPC, and MCP bind the owner and do not invoke it. The 0.7 + claim is adapter + operator round trip (no runtime caller). + Neo4j cabinet is archived. MEMNET_NEO4J_* is ignored. + Archive stays off load. */ attribute verificationId : String = "MN-VER-06-S06"; @@ -1664,13 +1641,9 @@ package MemNetVerification { .explicitSaveAndExpireSaveOneBlob == true and system.durable.storageRole == "cabinet_ego" and system.durable.agens.storageRole == "cabinet_ego" - and system.durable.neo4j.storageRole == "cabinet_ego" - and system.durable.bindsExactlyOneOf == "fake|agensgraph|neo4j" + and system.durable.bindsExactlyOneOf == "fake|agensgraph" and system.durable.hydrateFlushCallers == "tests" - and system.durable.library.storageRole == "library_locators" - and system.durable.neo4j.hydrateByHidLiveProven == false - and system.durable.neo4j.serverVendored == false - and system.durable.neo4j.liveNeo4jClaimed == true + and system.durable.retiredNeo4jEnvIgnored == true and system.leftoverArchiveOffLoad == true } } diff --git a/sysml-models/outputs/durable-hydrate-flush-case-study.md b/sysml-models/outputs/durable-hydrate-flush-case-study.md index 0a410999..dcfdcbf9 100644 --- a/sysml-models/outputs/durable-hydrate-flush-case-study.md +++ b/sysml-models/outputs/durable-hydrate-flush-case-study.md @@ -15,7 +15,7 @@ Show how a mission or company ego **survives process death**: settled / durable | Concern | Model element | |---------|----------------| -| Parts | `DurableBuffer`, `AgensGraphAdapter`, `Neo4jAdapter`, `Neo4jLibraryPort`, `SessionLifecycle` hydrate/flush ports | +| Parts | `DurableBuffer`, `AgensGraphAdapter`, `SessionLifecycle` hydrate/flush ports. Neo4j parts are on `MemNetArchive` (#187) | | Connections | `DurableHydrateFlow`, `DurableFlushFlow` | | Behaviour | `DurableHydrateFlushRoadmap` (`EvHydrateFromDurable`, `EvFlushToDurable`) | | Items | `DurableGraphStore` (connections), `MissionWorkingSet` / `SharedLlmMemory` | @@ -72,9 +72,9 @@ flowchart LR | | As-is | Target / leftover | |--|-------|-------------------| -| Client adapter | **Landed** — `DurableStoreAdapter`, `FakeDurableAdapter`, optional `AgensGraphAdapter` (`memnet-llm[agensgraph]`; `liveCabinetClaimed=true` in 0.7), optional `Neo4jAdapter` (`memnet-llm[neo4j]`; `liveNeo4jClaimed=true` in 0.14), `DurableSyncOwner`, `SessionLifecycle.hydrate_from_durable` / `flush_to_durable` | Keep Fake as CI seam | +| Client adapter | **Landed** — `DurableStoreAdapter`, `FakeDurableAdapter`, optional `AgensGraphAdapter` (`memnet-llm[agensgraph]`; 0.7 claim is adapter + operator round trip, no runtime caller), `DurableSyncOwner`, `SessionLifecycle.hydrate_from_durable` / `flush_to_durable`. Neo4j adapter removed (#187) | Keep Fake as CI seam | | Behaviour | `DurableHydrateFlushRoadmap` + engine hydrate/flush ports | Same | -| Live cabinet | Agens external / operator-proven (0.7); Neo4j extra 0.14 claimed (live round-trip yes; hid flush; leftover-nickname hydrate after hid miss); **not** vendored; skip live marks unless URL | Not a hosted product service | +| Live cabinet | Agens external; 0.7 claim is adapter + operator round trip (no runtime caller); **not** vendored; skip the live mark unless `MEMNET_AGENSGRAPH_URL`. Neo4j retired (#187); `MEMNET_NEO4J_*` ignored | Not a hosted product service | | Satisfy | MN-REQ-06.4 on `DurableBuffer` | Live claim is operator URL + pytest mark | ## 6. Related diff --git a/sysml-models/outputs/product-nest-one-page.md b/sysml-models/outputs/product-nest-one-page.md index 7ff153a6..d86f7bca 100644 --- a/sysml-models/outputs/product-nest-one-page.md +++ b/sysml-models/outputs/product-nest-one-page.md @@ -14,7 +14,7 @@ MemNetSystem // SharedLlmMemory product │ ├── Path A // shared session → re-pin (no import nest) │ └── Path B // slice → ImportGuard (optional soft) │ // → ImportAbsorb (hard) -└── DurableBuffer // cabinet_ego; factory binds exactly one of fake, AgensGraph, or Neo4j +└── DurableBuffer // cabinet_ego; factory binds exactly one of fake or AgensGraph APPLICATION LOOK CousinPointingContrast (eight cousins; SysMLEdge is a distinct pin_map / not MemNet SSOT; overlay family @@ -22,7 +22,8 @@ APPLICATION LOOK CousinPointingContrast (eight cousins; SysMLEdge downstream bound desk is working model SSOT), HostSearchBridge, … ARCHIVE LOOK MemNetArchive (models/archive.sysml) — leftover_* / - TierACodec / LegacyPipe* shelf; ProjectMemNet MUST NOT import + TierACodec / LegacyPipe* / retired Neo4j cabinet (#187); + ProjectMemNet MUST NOT import OPS LOOK MemNetUsageDashboard — human look only; not agent wire OPS FLEET MemNetOpsFleet — device MemNet services; one MemNet MCP at droplet (tip/ops; tip≠face; product face is sysmledge) OPS ACCESS TipMemNetAccessPortal — Szu-Wei invite, Google login, Bearer for keyed tip MCP at droplet WWW (tip≠face; not sysmledge; portal sidecar; look-only service/client status) @@ -53,6 +54,6 @@ Honesty that leftovers exist lives on the **ARCHIVE** shelf, not on `ProjectMemN ## Review (TTL file + nest) -Session TTL drops **RAM**. Explicit `session_save` and expire-save write **one** file blob. Expire-save is **off** unless `MEMNET_SAVE_ON_EXPIRE`. Disk file stays until the user deletes it; `session_load` restores RAM (`session_load(session=)` resolves the expire-dir snap when the caller already holds the sid; `loadByKnownSid`). DurableBuffer / Neo4j is a cabinet ego slice, not this file (`MN-VER-01-S03`, `MN-VER-06-S06`). +Session TTL drops **RAM**. Explicit `session_save` and expire-save write **one** file blob. Expire-save is **off** unless `MEMNET_SAVE_ON_EXPIRE`. Disk file stays until the user deletes it; `session_load` restores RAM (`session_load(session=)` resolves the expire-dir snap when the caller already holds the sid; `loadByKnownSid`). DurableBuffer is a cabinet ego slice, not this file (`MN-VER-01-S03`, `MN-VER-06-S06`). Neo4j is archived (#187). ARCHIVE leftover fog remains **off** `ProjectMemNet` load (`leftoverFogNested=false`, `leftoverArchiveOffLoad=true`). OPS `MemNetUsageDashboard` remains look-only (`httpImplemented=false`, `tipIsFace=false`, `agentWire=false`). OPS `MemNetOpsFleet` remains outside `MemNetSystem` (`mcpCount=1` = one MemNet MCP at droplet, `nServerFederation=false`, `productInventFace=sysmledge`, `tipIsFace=false`). OPS `TipMemNetAccessPortal` remains outside `MemNetSystem` (`tipIsFace=false`, `keyedWwwMcp=true`, `unauthenticatedWwwMcp=false`, `isSysmlEdgeProduct=false`, `inventOnly=false`, `portalWebImplemented=true`, `sellsInvent2Green=false`, `statusLookOnly=true`, `unparksUsageDashboard=false`). IMPLEMENTATION `MemNetLlmWheel` remains one Hatch package for many hosts (`oneWheelManyHosts=true`, `sysmlEdgeInWheel=false`). IMPLEMENTATION tracker (`ImplementationTracker`) watches allocate rows (`missingPathFailsCi`; `sysmlEdgeTracked=false`). diff --git a/sysml-models/outputs/ssot-to-code-allocate-map.md b/sysml-models/outputs/ssot-to-code-allocate-map.md index f6eca1bb..2f44e952 100644 --- a/sysml-models/outputs/ssot-to-code-allocate-map.md +++ b/sysml-models/outputs/ssot-to-code-allocate-map.md @@ -42,8 +42,6 @@ | exportToMod | `memNetSystem.pinMaps.export` | `exportMod` | `parts/common/memnet/memnet/pin_map_export.py` | `memnet.pin_map_export` | true | | catalogToMod | `memNetSystem.pinMaps.catalog` | `catalogMod` | `parts/common/memnet/memnet/catalog_snap.py` | `memnet.catalog_snap` | true | | agensToMod | `memNetSystem.durable.agens` | `agensMod` | `parts/common/memnet/memnet/durable/agensgraph.py` | `memnet.durable.agensgraph` | true | -| neo4jToMod | `memNetSystem.durable.neo4j` | `neo4jMod` | `parts/common/memnet/memnet/durable/neo4j.py` | `memnet.durable.neo4j` | true | -| libraryToMod | `memNetSystem.durable.library` | `libraryMod` | `parts/common/memnet/memnet/durable/neo4j_library.py` | `memnet.durable.neo4j_library` | true | | ragHookToMod | `hostSearch.hook` | `ragHookMod` | `parts/common/memnet/memnet/rag_host_hook.py` | `memnet.rag_host_hook` | true | | mcpToolsToMod | `memNetSystem.mcp.tools` | `mcpFacadeMod` | `parts/memnet-mcp/software/memnet_mcp/mcp_facade.py` | `memnet_mcp.mcp_facade` | true | | mcpToolsToServer | `memNetSystem.mcp.tools` | `wheel.mcp` | `parts/memnet-mcp/software/memnet_mcp/server.py` | `memnet_mcp.server` | true | diff --git a/sysml-models/outputs/system-design-notes.md b/sysml-models/outputs/system-design-notes.md index 5d27660a..33da3888 100644 --- a/sysml-models/outputs/system-design-notes.md +++ b/sysml-models/outputs/system-design-notes.md @@ -132,7 +132,7 @@ reserve and Path-B ingest are **shipped**. | SessionHandoffFlow | Coordinator → Worker | Target | | WorkingMemorySliceFlow | Worker → `coordinator.importReceive.guard` | Target path B | | ImportGuardDecisionFlow | Guard → Absorb (nested) | Target | -| DurableHydrate/FlushFlow | DurableBuffer adapters ↔ SessionLifecycle | M2.5; Agens 0.7; Neo4j 0.14 claimed | +| DurableHydrate/FlushFlow | DurableBuffer adapters ↔ SessionLifecycle | M2.5; Agens 0.7 adapter + operator round trip (no runtime caller); Neo4j archived (#187) | | InProcess / TCP flows | MCP/CLI ↔ engine | Wired | ## Target ↔ as-is modules (engine) diff --git a/tests/test_durable_store.py b/tests/test_durable_store.py index 1036f8d7..5c7f8376 100644 --- a/tests/test_durable_store.py +++ b/tests/test_durable_store.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging import os import pytest @@ -17,7 +18,6 @@ make_adapter_from_env, reset_sync_owner_for_tests, ) -from memnet.durable import neo4j as neo4j_cypher from memnet.durable.agensgraph import ( AgensGraphAdapter, AgensGraphConfig, @@ -28,7 +28,7 @@ map_edge_row, map_node_row, ) -from memnet.durable.neo4j import Neo4jAdapter, Neo4jConfig, first_label +from memnet.durable.factory import reset_neo4j_retired_warning_for_tests from memnet.exceptions import MemNetError from memnet.models import Record from memnet.pin_map_composer import PinMapComposer @@ -41,7 +41,6 @@ ] _LIVE = bool((os.environ.get("MEMNET_AGENSGRAPH_URL") or "").strip()) -_NEO4J_LIVE = bool((os.environ.get("MEMNET_NEO4J_URL") or "").strip()) @pytest.fixture(autouse=True) @@ -54,7 +53,6 @@ def _reset_owner(): def test_adapter_is_abc_contract(): assert issubclass(FakeDurableAdapter, DurableStoreAdapter) assert issubclass(AgensGraphAdapter, DurableStoreAdapter) - assert issubclass(Neo4jAdapter, DurableStoreAdapter) def test_fake_hydrate_respects_budget(): @@ -146,43 +144,54 @@ def test_make_adapter_from_env_agens_when_url(monkeypatch): assert isinstance(adapter, AgensGraphAdapter) -def test_make_adapter_from_env_neo4j_when_url(monkeypatch): +def test_retired_neo4j_env_is_ignored_and_engine_starts(monkeypatch, caplog): + """A deployed host may still export MEMNET_NEO4J_*. Ignore and start.""" monkeypatch.delenv("MEMNET_AGENSGRAPH_URL", raising=False) monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.delenv("MEMNET_DURABLE_BACKEND", raising=False) + monkeypatch.setenv("MEMNET_NEO4J_USER", "neo4j") + monkeypatch.setenv("MEMNET_NEO4J_PASSWORD", "do-not-log") + monkeypatch.setenv("MEMNET_NEO4J_DATABASE", "memnet") + monkeypatch.setenv("MEMNET_NEO4J_LIBRARY_DATABASE", "library") + monkeypatch.setenv("MEMNET_DURABLE_BACKEND", "neo4j") monkeypatch.delenv("MEMNET_DURABLE_FAKE", raising=False) - adapter = make_adapter_from_env() - assert isinstance(adapter, Neo4jAdapter) - - -def test_make_adapter_from_env_fake_overrides_url(monkeypatch): - monkeypatch.setenv("MEMNET_AGENSGRAPH_URL", "postgresql://localhost/memnet") - monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.setenv("MEMNET_DURABLE_FAKE", "1") - adapter = make_adapter_from_env() + reset_neo4j_retired_warning_for_tests() + reset_sync_owner_for_tests() + with caplog.at_level(logging.WARNING): + adapter = make_adapter_from_env() + owner = get_sync_owner() assert isinstance(adapter, FakeDurableAdapter) - - -def test_make_adapter_from_env_both_urls_conflict(monkeypatch): + assert owner is get_sync_owner() + warnings = [r.getMessage() for r in caplog.records if r.levelno >= logging.WARNING] + assert len(warnings) == 1 + assert "MEMNET_NEO4J_URL" in warnings[0] + assert "MEMNET_NEO4J_PASSWORD" in warnings[0] + assert "do-not-log" not in warnings[0] + assert "bolt://" not in warnings[0] + # Second start does not raise and does not warn again. + caplog.clear() + again = make_adapter_from_env() + assert isinstance(again, FakeDurableAdapter) + assert not [r for r in caplog.records if r.levelno >= logging.WARNING] + + +def test_retired_neo4j_env_does_not_block_agens(monkeypatch, caplog): monkeypatch.setenv("MEMNET_AGENSGRAPH_URL", "postgresql://localhost/memnet") monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") monkeypatch.delenv("MEMNET_DURABLE_BACKEND", raising=False) monkeypatch.delenv("MEMNET_DURABLE_FAKE", raising=False) - with pytest.raises(MemNetError) as ei: - make_adapter_from_env() - assert ei.value.code == "durable_backend_conflict" + reset_neo4j_retired_warning_for_tests() + with caplog.at_level(logging.WARNING): + adapter = make_adapter_from_env() + assert isinstance(adapter, AgensGraphAdapter) + assert any("MEMNET_NEO4J_URL" in r.getMessage() for r in caplog.records) -def test_make_adapter_from_env_both_urls_backend_pick(monkeypatch): +def test_make_adapter_from_env_fake_overrides_url(monkeypatch): monkeypatch.setenv("MEMNET_AGENSGRAPH_URL", "postgresql://localhost/memnet") monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.delenv("MEMNET_DURABLE_FAKE", raising=False) - monkeypatch.setenv("MEMNET_DURABLE_BACKEND", "neo4j") - adapter = make_adapter_from_env() - assert isinstance(adapter, Neo4jAdapter) - monkeypatch.setenv("MEMNET_DURABLE_BACKEND", "agensgraph") + monkeypatch.setenv("MEMNET_DURABLE_FAKE", "1") adapter = make_adapter_from_env() - assert isinstance(adapter, AgensGraphAdapter) + assert isinstance(adapter, FakeDurableAdapter) def test_agens_hydrate_unavailable_without_psycopg(monkeypatch): @@ -386,336 +395,8 @@ def test_agens_live_flush_hydrate_round_trip(memnet_temp): assert any(e.fields.get("relation") == "ABOUT" for e in loaded.edges) -def test_neo4j_from_env(monkeypatch): - monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.setenv("MEMNET_NEO4J_USER", "neo4j") - monkeypatch.setenv("MEMNET_NEO4J_PASSWORD", "test") - monkeypatch.setenv("MEMNET_NEO4J_DATABASE", "memnet") - adapter = Neo4jAdapter.from_env() - assert adapter is not None - assert adapter.name == "neo4j" - assert adapter.config.database_name == "memnet" - assert adapter.config.user == "neo4j" - - -def test_neo4j_hydrate_unavailable_without_driver(monkeypatch): - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - - def _boom() -> None: - raise MemNetError( - "neo4j_unavailable", - "neo4j missing (simulated)", - example="pip install neo4j", - ) - - monkeypatch.setattr(adapter, "_import_graphdatabase", _boom) - with pytest.raises(MemNetError) as ei: - adapter.hydrate("COM_acme", HydrateBudget()) - assert ei.value.code == "neo4j_unavailable" - - -def test_neo4j_connect_failed_is_clear(monkeypatch): - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:1")) - - class _GraphDatabase: - @staticmethod - def driver(*_a, **_k): - raise OSError("connection refused (simulated)") - - monkeypatch.setattr(adapter, "_import_graphdatabase", lambda: _GraphDatabase) - with pytest.raises(MemNetError) as ei: - adapter.hydrate("COM_acme", HydrateBudget()) - assert ei.value.code == "neo4j_connect_failed" - - -def test_neo4j_build_hydrate_nodes_cypher(): - cypher, params = neo4j_cypher.build_hydrate_nodes_cypher( - "COM_acme", HydrateBudget(max_nodes=12, depth=2) - ) - assert params["ego_id"] == "COM_acme" - assert "{_memnet_hid: $ego_id}" in cypher - assert "MATCH (ego {id:" not in cypher - assert "*0..2" in cypher - assert "LIMIT 12" in cypher - assert "labels(n)" in cypher - assert "label(n)" not in cypher - assert "properties(n)" in cypher - - -def test_neo4j_build_hydrate_edges_cypher_filters_ids(): - cypher, params = neo4j_cypher.build_hydrate_edges_cypher( - "COM_acme", - HydrateBudget(max_edges=20, depth=2), - node_ids=["COM_acme", "TSK_mission_q3"], - ) - assert params["node_ids"] == ["COM_acme", "TSK_mission_q3"] - assert "src._memnet_hid IN $node_ids" in cypher - assert "type(rel)" in cypher - assert "label(rel)" not in cypher - - -def test_neo4j_build_hydrate_edges_zero_or_empty(): - cypher, _params = neo4j_cypher.build_hydrate_edges_cypher( - "COM_acme", HydrateBudget(max_edges=0) - ) - assert "LIMIT 0" in cypher - cypher, _params = neo4j_cypher.build_hydrate_edges_cypher( - "COM_acme", HydrateBudget(max_edges=20) - ) - assert "LIMIT 0" in cypher - - -def test_neo4j_build_merge_node_and_edge_cypher(): - fixture = company_ego_fixture() - node = next(n for n in fixture.nodes if n.tag == "COM") - task = next(n for n in fixture.nodes if n.tag == "TSK") - edge = fixture.edges[0] - n_cypher, n_params = neo4j_cypher.build_merge_node_cypher(node) - assert "MERGE (n:COM {_memnet_hid: $hid})" in n_cypher - assert "MERGE (n:COM {id:" not in n_cypher - assert n_params["hid"] == node.hid - assert n_params["props"]["name"] == "Acme" - assert n_params["props"]["id"] == "COM_acme" - assert "_memnet_tag" in n_cypher - - e_cypher, e_params = neo4j_cypher.build_merge_edge_cypher(edge, nodes=fixture.nodes) - assert "_memnet_hid" in e_cypher - assert "MATCH (a {id:" not in e_cypher - assert e_params["src"] == task.hid - assert e_params["dist"] == node.hid - - -def test_neo4j_rejects_unsafe_ident_and_id(): - with pytest.raises(MemNetError) as ei: - neo4j_cypher.build_merge_node_cypher(Record(tag="COM;DROP", fields={"id": "COM_acme"})) - assert ei.value.code == "neo4j_bad_ident" - with pytest.raises(MemNetError) as ei: - neo4j_cypher.build_hydrate_nodes_cypher("COM_acme' OR 1=1", HydrateBudget()) - assert ei.value.code == "neo4j_bad_id" - - -def test_first_label_from_neo4j_list(): - assert first_label(["COM", "Entity"]) == "COM" - assert first_label("TSK") == "TSK" - assert first_label([]) == "" - - -def test_neo4j_hydrate_maps_mocked_rows(monkeypatch): - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - monkeypatch.setattr(adapter, "_ensure_driver", lambda: object()) - - def _run(_cypher: str, params=None): - if "labels(n)" in _cypher: - return [ - { - "labels": ["COM"], - "props": {"id": "COM_acme", "name": "Acme", "_memnet_tag": "COM"}, - }, - { - "labels": ["TSK"], - "props": { - "id": "TSK_mission_q3", - "goal": "Q3 mission", - "status": "settled", - "_memnet_tag": "TSK", - }, - }, - ] - return [ - { - "rel_type": "ABOUT", - "props": {"id": "E_about_q3", "relation": "ABOUT", "_memnet_tag": "EDG"}, - "src": "TSK_mission_q3", - "dist": "COM_acme", - } - ] - - monkeypatch.setattr(adapter, "_run", _run) - g = adapter.hydrate("COM_acme", HydrateBudget(max_nodes=10, max_edges=10, depth=2)) - assert {n.id for n in g.nodes} == {"COM_acme", "TSK_mission_q3"} - assert len(g.edges) == 1 - assert "ABOUT" in g.relations - - -def test_neo4j_flush_emits_merge_statements(monkeypatch): - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - seen: list[tuple[str, dict]] = [] - monkeypatch.setattr(adapter, "_ensure_driver", lambda: object()) - - def _run(cypher: str, params=None): - seen.append((cypher, params or {})) - return [] - - monkeypatch.setattr(adapter, "_run", _run) - fixture = company_ego_fixture() - ego = next(n for n in fixture.nodes if n.tag == "COM") - task = next(n for n in fixture.nodes if n.tag == "TSK") - adapter.flush(fixture) - cyphers = [c for c, _p in seen] - assert any("MERGE (n:COM {_memnet_hid: $hid})" in c for c in cyphers) - assert not any("MERGE (n:COM {id:" in c for c in cyphers) - assert any("MERGE (n:TSK" in c for c in cyphers) - about = next((c, p) for c, p in seen if "MERGE (a)-[r:ABOUT" in c) - assert about[1]["src"] == task.hid - assert about[1]["dist"] == ego.hid - assert cyphers.index(next(c for c in cyphers if "MERGE (n:COM" in c)) < cyphers.index( - next(c for c in cyphers if "MERGE (a)-[r:ABOUT" in c) - ) - - -def test_neo4j_recorded_session_stub(monkeypatch): - """Always-on Bolt stub: session.run is recorded; no live server.""" - - class _Session: - def __init__(self) -> None: - self.queries: list[str] = [] - - def __enter__(self): - return self - - def __exit__(self, *_a): - return False - - def run(self, query: str, params=None, **_k): - self.queries.append(query) - if "labels(n)" in query: - return [ - { - "labels": ["COM"], - "props": {"id": "COM_acme", "name": "Acme", "_memnet_tag": "COM"}, - } - ] - return [] - - def close(self) -> None: - return None - - session = _Session() - - class _Driver: - def session(self, database=None): - assert database == "neo4j" - return session - - def close(self) -> None: - return None - - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - monkeypatch.setattr(adapter, "_ensure_driver", lambda: _Driver()) - g = adapter.hydrate("COM_acme", HydrateBudget(max_nodes=5, max_edges=5, depth=1)) - assert any(n.id == "COM_acme" for n in g.nodes) - assert any("labels(n)" in q for q in session.queries) - - -def test_neo4j_hydrate_after_flush_uses_hid(monkeypatch): - """Official fixture: flush then hydrate by the same hid that was written.""" - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - monkeypatch.setattr(adapter, "_ensure_driver", lambda: object()) - fixture = company_ego_fixture(ego_id="COM_acme_live_neo4j") - ego = next(n for n in fixture.nodes if n.id == fixture.ego_id) - task = next(n for n in fixture.nodes if n.tag == "TSK") - seen: list[tuple[str, dict]] = [] - - def _run(cypher: str, params=None): - params = params or {} - seen.append((cypher, params)) - if "labels(n)" in cypher: - if params.get("ego_id") != ego.hid: - return [] - return [ - { - "labels": ["COM"], - "props": { - "id": "COM_acme_live_neo4j", - "name": "Acme", - "_memnet_tag": "COM", - "_memnet_hid": ego.hid, - }, - }, - { - "labels": ["TSK"], - "props": { - "id": "TSK_mission_q3", - "goal": "Q3 mission", - "status": "settled", - "_memnet_tag": "TSK", - "_memnet_hid": task.hid, - }, - }, - ] - if "type(rel)" in cypher: - node_ids = params.get("node_ids") or [] - if ego.hid not in node_ids or task.hid not in node_ids: - return [] - return [ - { - "rel_type": "ABOUT", - "props": { - "id": "E_about_q3", - "relation": "ABOUT", - "_memnet_tag": "EDG", - }, - "src": task.hid, - "dist": ego.hid, - } - ] - return [] - - monkeypatch.setattr(adapter, "_run", _run) - adapter.flush(fixture) - merge_com = next(c for c, _p in seen if "MERGE (n:COM" in c) - assert "MERGE (n:COM {_memnet_hid: $hid})" in merge_com - assert "MERGE (n:COM {id:" not in merge_com - assert next(p["hid"] for c, p in seen if "MERGE (n:COM" in c) == ego.hid - about_params = next(p for c, p in seen if "MERGE (a)-[r:ABOUT" in c) - assert about_params["src"] == task.hid - assert about_params["dist"] == ego.hid - - seen.clear() - loaded = adapter.hydrate(ego.hid, HydrateBudget(max_nodes=20, max_edges=20, depth=2)) - ego_match = next(c for c, _p in seen if "labels(n)" in c) - assert "{_memnet_hid: $ego_id}" in ego_match - assert "MATCH (ego {id:" not in ego_match - assert any(n.id == "COM_acme_live_neo4j" for n in loaded.nodes) - assert any(e.fields.get("relation") == "ABOUT" for e in loaded.edges) - about = next(e for e in loaded.edges if e.fields.get("relation") == "ABOUT") - assert about.fields.get("dist") == "COM_acme_live_neo4j" - assert about.fields.get("src") == "TSK_mission_q3" - - -def test_neo4j_hydrate_nickname_after_hid_miss(monkeypatch): - """Leftover: nickname as property ``id`` only after hid MATCH is empty.""" - adapter = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687")) - monkeypatch.setattr(adapter, "_ensure_driver", lambda: object()) - keys: list[str] = [] - - def _run(cypher: str, params=None): - if "labels(n)" in cypher: - if "{_memnet_hid:" in cypher.split("OPTIONAL", 1)[0]: - keys.append("hid") - return [] - keys.append("id") - return [ - { - "labels": ["COM"], - "props": { - "id": "COM_acme_live_neo4j", - "name": "Acme", - "_memnet_tag": "COM", - "_memnet_hid": "_el2", - }, - } - ] - return [] - - monkeypatch.setattr(adapter, "_run", _run) - g = adapter.hydrate("COM_acme_live_neo4j", HydrateBudget(max_nodes=5, max_edges=0)) - assert keys == ["hid", "id"] - assert any(n.id == "COM_acme_live_neo4j" for n in g.nodes) - - def test_agens_hydrate_after_flush_uses_hid(monkeypatch): - """Same hid story as Neo4j: fixture flush then hydrate by written hid.""" + """Fixture flush then hydrate by the written hid.""" adapter = AgensGraphAdapter(AgensGraphConfig(url="postgresql://localhost/memnet")) seen: list[str] = [] @@ -788,20 +469,3 @@ def _exec(_conn, cypher: str): assert any(n.id == "COM_acme_live_m25" for n in loaded.nodes) about_e = next(e for e in loaded.edges if e.fields.get("relation") == "ABOUT") assert about_e.fields.get("dist") == "COM_acme_live_m25" - - -@pytest.mark.neo4j_live -@pytest.mark.skipif(not _NEO4J_LIVE, reason="MEMNET_NEO4J_URL not set") -def test_neo4j_live_flush_hydrate_round_trip(memnet_temp): - """Optional: exercise external Neo4j cabinet when URL is exported.""" - adapter = Neo4jAdapter.from_env() - assert adapter is not None - fixture = company_ego_fixture(ego_id="COM_acme_live_neo4j") - ego = next(n for n in fixture.nodes if n.id == fixture.ego_id) - try: - adapter.flush(fixture) - loaded = adapter.hydrate(ego.hid, HydrateBudget(max_nodes=20, max_edges=20, depth=2)) - finally: - adapter.close() - assert any(n.id == fixture.ego_id for n in loaded.nodes) - assert any(e.fields.get("relation") == "ABOUT" for e in loaded.edges) diff --git a/tests/test_neo4j_library.py b/tests/test_neo4j_library.py deleted file mode 100644 index d628c1b4..00000000 --- a/tests/test_neo4j_library.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Extra 0.16: Neo4j cabinet vs library namespaces (same process, two DBs).""" - -from __future__ import annotations - -import inspect -import os - -import pytest - -from memnet.durable.adapter import DurableStoreAdapter -from memnet.durable.neo4j import Neo4jAdapter, Neo4jConfig -from memnet.durable.neo4j_library import ( - Neo4jLibraryClient, - Neo4jLibraryConfig, - build_library_locator_cypher, - locator_fields, - make_library_client_from_env, -) -from memnet.exceptions import MemNetError -from memnet.pin_map_composer import PinMapComposer - -_NEO4J_LIVE = bool((os.environ.get("MEMNET_NEO4J_URL") or "").strip()) - - -def test_library_skips_when_name_unset(monkeypatch): - monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.delenv("MEMNET_NEO4J_LIBRARY_DATABASE", raising=False) - monkeypatch.delenv("MEMNET_NEO4J_DATABASE", raising=False) - assert make_library_client_from_env() is None - assert Neo4jLibraryConfig.from_env() is None - - -def test_library_skips_when_url_unset(monkeypatch): - monkeypatch.delenv("MEMNET_NEO4J_URL", raising=False) - monkeypatch.setenv("MEMNET_NEO4J_LIBRARY_DATABASE", "library") - assert make_library_client_from_env() is None - - -def test_cabinet_and_library_databases_are_distinct(monkeypatch): - monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.setenv("MEMNET_NEO4J_DATABASE", "neo4j") - monkeypatch.setenv("MEMNET_NEO4J_LIBRARY_DATABASE", "library") - cabinet = Neo4jAdapter.from_env() - library = Neo4jLibraryClient.from_env() - assert cabinet is not None - assert library is not None - assert cabinet.config.database_name == "neo4j" - assert library.database_name == "library" - assert cabinet.config.database_name != library.database_name - assert not isinstance(library, DurableStoreAdapter) - assert library.generate is False - - -def test_same_database_name_is_rejected(monkeypatch): - monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") - monkeypatch.setenv("MEMNET_NEO4J_DATABASE", "neo4j") - monkeypatch.setenv("MEMNET_NEO4J_LIBRARY_DATABASE", "neo4j") - with pytest.raises(MemNetError) as ei: - Neo4jLibraryClient.from_env() - assert ei.value.code == "neo4j_library_same_as_cabinet" - with pytest.raises(MemNetError) as ei2: - Neo4jAdapter.from_env() - assert ei2.value.code == "neo4j_library_same_as_cabinet" - - -def test_library_emit_is_locators_only_stub(): - client = Neo4jLibraryClient(Neo4jLibraryConfig(url="bolt://127.0.0.1:7687", database="library")) - rows = [ - { - "path": "docs/SHAPE.md", - "document_id": "DOC_shape", - "qname": "", - "session": "", - "url": "", - "_memnet_hid": "el1", - "generate": True, - "hid": "secret", - } - ] - client._run = lambda *_a, **_k: rows # type: ignore[method-assign] - client._ensure_driver = lambda: None # type: ignore[method-assign] - locators = client.emit_locators("shape", limit=8) - assert locators == [{"path": "docs/SHAPE.md", "document_id": "DOC_shape"}] - assert "generate" not in locators[0] - assert "_memnet_hid" not in locators[0] - assert client.generate is False - - -def test_locator_fields_strips_hid(): - got = locator_fields( - { - "path": "/tmp/a.md", - "_memnet_hid": "el9", - "elementId": "4:abc", - "generate": "true", - "note": "chunk body", - } - ) - assert got == {"path": "/tmp/a.md"} - - -def test_library_must_not_hydrate_or_flush(): - client = Neo4jLibraryClient(Neo4jLibraryConfig(url="bolt://127.0.0.1:7687", database="library")) - with pytest.raises(MemNetError) as h: - client.hydrate("COM_acme") - assert h.value.code == "neo4j_library_not_cabinet" - with pytest.raises(MemNetError) as f: - client.flush(None) - assert f.value.code == "neo4j_library_read_only" - - -def test_library_locator_cypher_has_no_generate(): - cypher, params = build_library_locator_cypher("qname", limit=7) - assert params["limit"] == 7 - assert params["cue"] == "qname" - assert "generate" not in cypher.lower() - assert "rrf" not in cypher.lower() - assert "ppr" not in cypher.lower() - assert "_memnet_hid" not in cypher - assert "RETURN n.path" in cypher - assert "n._memnet_tag IS NULL" in cypher - - -def test_pin_map_does_not_fuse_rrf_ppr(): - source = inspect.getsource(PinMapComposer) - lowered = source.lower() - for token in ("rrf", "ppr", "leiden", "reciprocal_rank", "generate("): - assert token not in lowered - cfg_src = inspect.getsource(Neo4jAdapter) - assert "rrf" not in cfg_src.lower() - - -def test_library_session_uses_library_database(): - client = Neo4jLibraryClient(Neo4jLibraryConfig(url="bolt://127.0.0.1:7687", database="library")) - cabinet = Neo4jAdapter(Neo4jConfig(url="bolt://127.0.0.1:7687", database="neo4j")) - - class _Sess: - def __enter__(self): - return self - - def __exit__(self, *_a): - return False - - def run(self, *_a, **_k): - return [] - - opened: list[str] = [] - - class _Driver: - def session(self, database="neo4j"): - opened.append(database) - return _Sess() - - def close(self): - return None - - client._driver = _Driver() - cabinet._driver = _Driver() - client._run("RETURN 1", {}) - cabinet._run("RETURN 1", {}) - assert opened == ["library", "neo4j"] - - -@pytest.mark.neo4j_live -@pytest.mark.skipif(not _NEO4J_LIVE, reason="MEMNET_NEO4J_URL not set") -def test_neo4j_live_library_skip_or_distinct(): - """Live: skip library unless name set; never fuse cabinet hydrate.""" - lib = make_library_client_from_env() - if lib is None: - pytest.skip("MEMNET_NEO4J_LIBRARY_DATABASE not set") - cabinet = Neo4jAdapter.from_env() - assert cabinet is not None - assert lib.database_name != cabinet.config.database_name - assert lib.generate is False - locators = lib.emit_locators(limit=5) - lib.close() - for loc in locators: - assert "generate" not in loc - assert "_memnet_hid" not in loc - assert "hid" not in loc - assert "elementId" not in loc diff --git a/tests/test_rag_host_hook.py b/tests/test_rag_host_hook.py index e081061f..1916c40b 100644 --- a/tests/test_rag_host_hook.py +++ b/tests/test_rag_host_hook.py @@ -160,29 +160,14 @@ def test_http_host_timeout_skips(monkeypatch, memnet_temp): assert committed.skipped is True -def test_library_locators_consumed_as_host_source(monkeypatch): +def test_retired_neo4j_env_is_not_a_host_source(monkeypatch): monkeypatch.setenv("MEMNET_NEO4J_URL", "bolt://127.0.0.1:7687") monkeypatch.setenv("MEMNET_NEO4J_LIBRARY_DATABASE", "library") monkeypatch.delenv("MEMNET_HOST_SEARCH_URL", raising=False) - - class _Lib: - def emit_locators(self, cue, *, limit=8): - assert cue == "shape" - return [{"path": "docs/SHAPE.md", "_memnet_hid": "el1", "generate": True}] - - def close(self): - return None - - monkeypatch.setattr( - "memnet.rag_host_hook.make_library_client_from_env", - lambda: _Lib(), - ) set_rag_host_hook(None) result = EnvRagHostHook().propose(HostSearchCue(question="shape")) - assert result.skipped is False - assert result.locators == ({"path": "docs/SHAPE.md"},) - assert "generate" not in result.locators[0] - assert "_memnet_hid" not in result.locators[0] + assert result.skipped is True + assert result.locators == () def test_locators_are_not_rag_query_and_pin_map_does_not_rrf(): diff --git a/tests/test_sysml_archive_off_load.py b/tests/test_sysml_archive_off_load.py index 7a0d5549..6e810c2c 100644 --- a/tests/test_sysml_archive_off_load.py +++ b/tests/test_sysml_archive_off_load.py @@ -44,6 +44,9 @@ def test_archive_shelf_keeps_leftover_honesty(): "leftover_AssignedIdMap", "TierACodec", "LegacyPipeImport", + "Neo4jAdapter", + "Neo4jLibraryPort", + "liveNeo4jClaimed", ): assert f"part def {name}" in text @@ -53,6 +56,11 @@ def test_product_deploy_does_not_nest_leftover_fog(): assert not re.search(r"^ part def leftover_", text, re.M) assert not re.search(r"^ part def TierACodec", text, re.M) assert not re.search(r"^ part def LegacyPipeImport", text, re.M) + assert not re.search(r"^ part def Neo4jAdapter", text, re.M) + assert not re.search(r"^ part def Neo4jLibraryPort", text, re.M) + assert not re.search(r"^ part def liveNeo4jClaimed", text, re.M) + assert "part neo4j :" not in text + assert 'bindsExactlyOneOf : String = "fake|agensgraph"' in text assert "part leftover_id_first" not in text assert "part leftover_by_id" not in text assert "part leftover_MERGE_by_id" not in text