From 851a0c7a6bc5fc54cebad63d424b81aff3b767b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 03:49:25 +0000 Subject: [PATCH 1/3] Stop emitting nickname id on pin_map (0.19.4 honesty c) Shaped pin_map omit property id on nodes and edges. Ranking still excludes nickname. Cue/find/match_nickname still look up a nickname. Co-authored-by: chouswei --- .cursor/skills/SKILL-GRAPH.md | 2 +- .cursor/skills/memnet-format/SKILL.md | 4 ++-- .cursor/skills/memnet-reference/SKILL.md | 4 ++-- AGENTS.md | 2 +- CHANGELOG.md | 5 ++++- README.md | 2 +- docs/LLM-GUIDE.md | 12 ++++++------ docs/ROADMAP.md | 10 +++++----- docs/SHAPE.md | 2 +- .../system/llm-build-on-memnet.md | 2 +- docs/grammar/gql-wire-profile.md | 13 ++++++------- parts/common/memnet/memnet/__init__.py | 2 +- parts/common/memnet/memnet/gql.py | 18 +++++++++++++----- .../common/memnet/memnet/pin_map_composer.py | 17 ++++++++++------- project.toml | 2 +- sysml-models/outputs/system-design-notes.md | 2 +- tests/test_acl.py | 6 +++--- tests/test_add_update.py | 2 +- tests/test_bounded_match_find.py | 6 +++--- tests/test_cli.py | 3 +-- tests/test_durable_store.py | 2 +- tests/test_facade_leftover.py | 7 ++++--- tests/test_goldfish_caller.py | 16 ++++++++-------- tests/test_goldfish_paradox.py | 19 ++++++++++--------- tests/test_mcp.py | 12 +++++++----- tests/test_mission_warm.py | 2 +- tests/test_mutate_gate.py | 2 +- tests/test_peak_l.py | 10 +++++----- tests/test_pin_map_export.py | 2 +- tests/test_pin_map_observable_rank.py | 19 +++++++++++++++++++ tests/test_pin_map_view.py | 2 +- tests/test_schemes.py | 6 +++--- tests/test_session.py | 3 ++- tests/test_session_outline.py | 11 +++++------ tests/test_snapshot.py | 4 ++-- 35 files changed, 134 insertions(+), 99 deletions(-) diff --git a/.cursor/skills/SKILL-GRAPH.md b/.cursor/skills/SKILL-GRAPH.md index 8d28cb23..187ed812 100644 --- a/.cursor/skills/SKILL-GRAPH.md +++ b/.cursor/skills/SKILL-GRAPH.md @@ -2,7 +2,7 @@ **Audience:** model. Wire SSOT: [memnet-format](memnet-format/SKILL.md) and `docs/grammar/gql-wire-profile.md`. Do **not** treat this file as the session graph. -This folder **vendors** the MemNet stack. Hatch **0.19.3** (PyPI **`memnet-llm==0.19.3`**). Cue then `pin_map`; `find` if ego unknown. **GQL only.** Package SemVer `a.b.c` (claim / usage-method revision / same-method cut): `docs/ROADMAP.md`. Do not invent 0.20. +This folder **vendors** the MemNet stack. Hatch **0.19.4** (last PyPI **`memnet-llm==0.19.3`** until upload). Cue then `pin_map`; `find` if ego unknown. **GQL only.** Package SemVer `a.b.c` (claim / usage-method revision / same-method cut): `docs/ROADMAP.md`. Do not invent 0.20. ## Routing diff --git a/.cursor/skills/memnet-format/SKILL.md b/.cursor/skills/memnet-format/SKILL.md index 42af9a9e..00c4b701 100644 --- a/.cursor/skills/memnet-format/SKILL.md +++ b/.cursor/skills/memnet-format/SKILL.md @@ -8,7 +8,7 @@ metadata: pattern: tool-wrapper version: "5.3" domain: data-formats,memnet - product: memnet-llm==0.19.3 + product: memnet-llm==0.19.4 token_guardrails: | - Wire SSOT: docs/grammar/gql-wire-profile.md. This skill is MemNet-specific only. - pin_map from a cue; leftover anchor= is leftover. Do not dump S. @@ -17,7 +17,7 @@ token_guardrails: | # MemNet formats -Pair with [mcp-memnet](../mcp-memnet/SKILL.md). Formal SSOT: `docs/grammar/gql-wire-profile.md`. Hatch **0.19.3**. +Pair with [mcp-memnet](../mcp-memnet/SKILL.md). Formal SSOT: `docs/grammar/gql-wire-profile.md`. Hatch **0.19.4**. **GQL only.** Node / edge / property. Do not teach Layer, Tier A, pipe `@TAG`, or TOON/TRON. leftover `id:'NEW'` / leftover `anchor=` are leftover. diff --git a/.cursor/skills/memnet-reference/SKILL.md b/.cursor/skills/memnet-reference/SKILL.md index 143f1531..90f92168 100644 --- a/.cursor/skills/memnet-reference/SKILL.md +++ b/.cursor/skills/memnet-reference/SKILL.md @@ -10,7 +10,7 @@ metadata: pattern: pipeline version: "2.3" domain: memnet - product: "0.19.3" + product: "0.19.4" --- # MemNet product development reference @@ -19,7 +19,7 @@ Repo skill for **building** MemNet in **this** repository. Doctrine SSOT is `doc This checkout **vendors** agent skills under `.cursor/skills/` (core: `memnet-use` + MCP/format/nested/multitask). Load those when **using** MemNet; load **this** skill only when changing the engine, generic MCP, grammar, product SysML, or packaging. -**Product:** Hatch **0.19.3** (CLI `memnet`). PyPI **`memnet-llm==0.19.3`**. Version map: `docs/ROADMAP.md`. Shape: `docs/SHAPE.md`. **1.0** = 0.5–0.8 claimed (do not tag from this skill). +**Product:** Hatch **0.19.4** (CLI `memnet`). PyPI **`memnet-llm==0.19.3`** until 0.19.4 upload. Version map: `docs/ROADMAP.md`. Shape: `docs/SHAPE.md`. **1.0** = 0.5–0.8 claimed (do not tag from this skill). **`a.b.c` (ROADMAP law).** `a` = claim / generation (`0` = 1.0 unclaimed; `1` = claim of 0.5–0.8). `a` moves only if the contract breaks (Recall=`pin_map` / Commit=`mutate`, GraphElement identity, GQL-only wire). `b` = usage-method revision (new required loop step, new product tool, cue/outline/identity law, new goldfish verb). Historical extras **0.10–0.19** were `b`. N-server (#47) is not a `b`. `c` = same-method cut (honesty, leftover naming, docs/skills, wheel/tag, caps, façade, faster same `cue → pin_map → mutate`). Efficiency on the current loop is **`0.19.c`**, not `0.20`. Do not invent a 0.20 extra. Do not claim 1.0. diff --git a/AGENTS.md b/AGENTS.md index 905aa742..12314fb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ LLM hub for this system repo. Prefer in-repo skills and docs over ad-hoc inventi ## Mission -**MemNet** (Net of Memory) is **mission working memory** — a session graph (GQL **node**/vertex, **edge**/relationship, **property**) **between** LLM call pipelines and data search, not the corpus and not GraphRAG. Agents read a bounded **live pin map** each turn and write in the same **GQL (openCypher-shaped)** family — redefined **Write = display** via shaped subgraph emit ([`docs/grammar/gql-wire-profile.md`](docs/grammar/gql-wire-profile.md)). In-session recall is **serial**: kind/keyword cue, then `pin_map` neighbourhood. Primary read: MCP `pin_map` / CLI `query pin-map`; leftover `query_warm` / `query warm` are leftover aliases. Aims (MN-REQ-00): save wall-clock time and tokens while keeping factual accuracy. Aids **system**, **programme**, **software**, **firmware**, **hardware**, and **documentation**. Transport: **in-process first** (single-agent; TCP fallback). **Multitask** requires TCP serve or streamable-http MCP — see Multitask policy below. This repo is **engine + generic memnet-mcp** only — novel-writer dropped. Repo product **0.19.3** (Hatch SSOT); last published PyPI **`memnet-llm==0.19.0`** until 0.19.3 is uploaded. **1.0** = 0.5–0.8 claimed (unclaimed). See `README.md`, [`docs/SHAPE.md`](docs/SHAPE.md), and `docs/grammar/`. +**MemNet** (Net of Memory) is **mission working memory** — a session graph (GQL **node**/vertex, **edge**/relationship, **property**) **between** LLM call pipelines and data search, not the corpus and not GraphRAG. Agents read a bounded **live pin map** each turn and write in the same **GQL (openCypher-shaped)** family — redefined **Write = display** via shaped subgraph emit ([`docs/grammar/gql-wire-profile.md`](docs/grammar/gql-wire-profile.md)). In-session recall is **serial**: kind/keyword cue, then `pin_map` neighbourhood. Primary read: MCP `pin_map` / CLI `query pin-map`; leftover `query_warm` / `query warm` are leftover aliases. Aims (MN-REQ-00): save wall-clock time and tokens while keeping factual accuracy. Aids **system**, **programme**, **software**, **firmware**, **hardware**, and **documentation**. Transport: **in-process first** (single-agent; TCP fallback). **Multitask** requires TCP serve or streamable-http MCP — see Multitask policy below. This repo is **engine + generic memnet-mcp** only — novel-writer dropped. Repo product **0.19.4** (Hatch SSOT); last published PyPI **`memnet-llm==0.19.3`** until 0.19.4 is uploaded. **1.0** = 0.5–0.8 claimed (unclaimed). See `README.md`, [`docs/SHAPE.md`](docs/SHAPE.md), and `docs/grammar/`. ## Where to look diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7ca2dd..9bb0491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,11 @@ This project uses Semantic Versioning as **interpreted for MemNet**: package `a. ## [Unreleased] +## [0.19.4] - 2026-09-04 + ### Changed -- **`a.b.c` law** — ROADMAP locks how `memnet-llm` reads SemVer (`a` = claim/generation, `b` = usage-method revision, `c` = same-method cut). Package **0.19.3** unchanged; extras **0.10–0.19** unchanged; no 0.20 extra; efficiency stays a `c` unless usage changes. **1.0** stays unclaimed. +- **Honesty `c` — nickname `id` off `pin_map` emit** — Shaped `pin_map` (outline, CueConflict, find hits, export) **MUST NOT** print nickname property `id` on nodes or edges. Same effect as `DROP_KEYS={id,hid}` on the wire text. Hid already stays off. Ranking still excludes nickname from the rank key ([#147](https://github.com/chouswei/MemNet/pull/147)). Cue / find / `match_nickname` MAY still look up a nickname the agent cues by — that is not identity and not emit. STM Prediction 3 generation half: RAW pin_map with nickname on wire failed under hid/CREATE-order/nickname permutation; canonical strip of `id`/`hid` passed. Package **0.19.4** patch on **0.19**. Numbered extras **0.10–0.19** unchanged. No 0.20 extra. `operatorCount` stays 2. Caps stay hard. Do not dump \(S\). Do not invent `rag_query`. **1.0** stays unclaimed. Git tag `v0.19.4` after merge. +- **Package identity 0.19.4** — Hatch / `project.toml` / `memnet.__version__` honesty cut on **0.19**. Last published PyPI wheel remains **`memnet-llm==0.19.3`** until upload. ## [0.19.3] - 2026-08-20 diff --git a/README.md b/README.md index 0fecd277..67198303 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ MemNet sits **between** LLM call pipelines and data search (MN-REQ-00). Corpus l 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. -Package **`memnet-llm`** (CLI **`memnet`**). Python ≥ 3.11. Repo product **0.19.3**. PyPI **`memnet-llm==0.19.3`** is published. **1.0** stays unclaimed. SemVer meaning (`a.b.c`): [`docs/ROADMAP.md`](docs/ROADMAP.md). +Package **`memnet-llm`** (CLI **`memnet`**). Python ≥ 3.11. Repo product **0.19.4**. Last published PyPI **`memnet-llm==0.19.3`** until 0.19.4 upload. **1.0** stays unclaimed. SemVer meaning (`a.b.c`): [`docs/ROADMAP.md`](docs/ROADMAP.md). ## Install + quick CLI diff --git a/docs/LLM-GUIDE.md b/docs/LLM-GUIDE.md index 13b842ab..8a582e64 100644 --- a/docs/LLM-GUIDE.md +++ b/docs/LLM-GUIDE.md @@ -1,6 +1,6 @@ # MemNet — Agent Playbook (for LLMs) -**Class:** developers — MemNet engine / MCP / GQL wire / agent operating doctrine. Index: [`docs/README.md`](README.md). Product shape: [`SHAPE.md`](SHAPE.md). **Product 0.19.3.** **1.0** = 0.5–0.8 claimed (unclaimed). PyPI **`memnet-llm==0.19.3`**. SemVer `a.b.c`: [`ROADMAP.md`](ROADMAP.md). +**Class:** developers — MemNet engine / MCP / GQL wire / agent operating doctrine. Index: [`docs/README.md`](README.md). Product shape: [`SHAPE.md`](SHAPE.md). **Product 0.19.4.** **1.0** = 0.5–0.8 claimed (unclaimed). Last PyPI **`memnet-llm==0.19.3`**. SemVer `a.b.c`: [`ROADMAP.md`](ROADMAP.md). **Dialect teach = GQL only** — [`grammar/gql-wire-profile.md`](grammar/gql-wire-profile.md). ADR: [`adr/ADR-001-gql-agent-wire.md`](adr/ADR-001-gql-agent-wire.md). **M2 shipped:** engine/MCP accept openCypher-shaped GQL and emit shaped `pin_map`. Do **not** teach Layer / Tier A / `@TAG` pipe as agent wire. @@ -41,7 +41,7 @@ MATCH (t:TSK {goal:'Clear warehouse'}) SET t.status = 'settled', t.recycle = 'de After **CueConflict** (\(|Q|>1\) on find/`pin_map`), SameThingAbsorb is a **Commit rule** (not a third operator): `MATCH (a:TSK {goal:'alpha'}), (b:TSK {goal:'beta'}) SET a += b`. Two same-name nodes stay two until that Commit. ImportAbsorb does not entity-resolve. -**Shaped `pin_map` out** (properties the node actually has; nickname `id` only if set): +**Shaped `pin_map` out** (observable properties; nickname `id` stays off this emit): ```cypher (:TSK {goal:'Clear warehouse', status:'in_progress'}) @@ -52,8 +52,8 @@ After **CueConflict** (\(|Q|>1\) on find/`pin_map`), SameThingAbsorb is a **Comm Circuit / law-leaf sketch: ```cypher -(:CST {id:'CST_R', R:50, ports:{a:{direc:'inout', V:'@va', I:'@ia'}, b:{direc:'inout', V:'@vb', I:'@ib'}}, law:'$@va-@vb=@ia*R$,$@ia=-@ib$'}) -(:CST {id:'CST_Src'})-[:bind {id:'E_1', fromPort:'p', toPort:'a', carries:'I'}]->(:CST {id:'CST_R'}) +(:CST {R:50, ports:{a:{direc:'inout', V:'@va', I:'@ia'}, b:{direc:'inout', V:'@vb', I:'@ib'}}, law:'$@va-@vb=@ia*R$,$@ia=-@ib$'}) +(:CST {name:'Src'})-[:bind {fromPort:'p', toPort:'a', carries:'I'}]->(:CST {R:50}) ``` - **Create:** `CREATE (:Kind {props})` — GraphElement identity; no required `id`. leftover `id:'NEW'` mint is leftover, not product. @@ -66,7 +66,7 @@ Formal wire: [`grammar/gql-wire-profile.md`](grammar/gql-wire-profile.md). | Mode | When | Setup | |------|------|-------| -| **MCP in-process** | Cursor / local agents (**primary**) | Register `memnet-mcp` in `.cursor/mcp.json`; extra `[mcp]`. `pip install 'memnet-llm[mcp]'` (Hatch **0.19.3**; PyPI **0.19.3**). **No** `memnet serve` | +| **MCP in-process** | Cursor / local agents (**primary**) | Register `memnet-mcp` in `.cursor/mcp.json`; extra `[mcp]`. `pip install 'memnet-llm[mcp]'` (Hatch **0.19.4**; last PyPI **0.19.3**). **No** `memnet serve` | | **CLI + serve** | Scripts, TCP shared process | Terminal 1: `memnet serve`; Terminal 2: CLI with `MEMNET_SESSION` | | **MCP streamable-http** | Remote shared graph | `memnet-mcp --transport streamable-http` on `:18766/mcp` | @@ -125,7 +125,7 @@ Always pass the same `session` id across tools in one job. | **New element** | `CREATE` via `mutate` | leftover `add` | `id_exists` on leftover add | | **Change** | `MATCH…SET` via `mutate` | leftover `update` | `not_found` on leftover update | -Copy ids from pin map output — never retype from memory. There is no upsert. +Copy locators from `pin_map` (labels + observable properties) — never treat nickname `id` as identity. There is no upsert. ### IDs diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ac94f810..df42d746 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,9 +4,9 @@ **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.3**. Numbered extras **0.10–0.19** are in this package (unchanged). PyPI **`memnet-llm==0.19.3`** is published. 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.4**. Numbered extras **0.10–0.19** are in this package (unchanged). PyPI **`memnet-llm==0.19.3`** is the last published wheel until 0.19.4 upload. GraphGlot parse front is on master (#109 @ 73a63c9b). Extra **0.14** claims `liveNeo4jClaimed=true`. **1.0** is still unclaimed (0.5–0.8). -**Last updated:** 2026-08-23 (`a.b.c` law locked; package **0.19.3** unchanged; extras **0.10–0.19** unchanged; PyPI **0.19.3**; default `max_sessions` **1024**; MCP `session_close` + list `sessions|n/max`; extras first packaged as **0.19.0**; GraphGlot parse front #109 @ 73a63c9b; extra **0.19** pin-map export #123 @ 2c460e7d; extra **0.18** Peak_L #128 @ dc464cd4; extra **0.17** HostSearch #129 @ 00e74dfb; extra **0.16** two namespaces #127 @ c32d4c52; extra **0.15** catalog Snap #124 @ 7767ed84; extra **0.14** live Neo4j claimed; do not claim **1.0**; do not invent a **0.20** extra). +**Last updated:** 2026-09-04 (`a.b.c` law locked; package **0.19.4** honesty `c` — nickname `id` off `pin_map` emit; extras **0.10–0.19** unchanged; last PyPI **0.19.3** until upload; ranking honesty [#147](https://github.com/chouswei/MemNet/pull/147); do not claim **1.0**; do not invent a **0.20** extra). Patch notes: [`../CHANGELOG.md`](../CHANGELOG.md). CHANGELOG still follows Keep a Changelog. This file is how MemNet **interprets** SemVer — not a silent switch to npm-strict major=breaking for 0.x extras. @@ -39,13 +39,13 @@ One concern per `b`. Do **not** fuse two usage-method changes into one `b`. Skip Same usage method. Honesty, leftover naming, docs / skills, wheel / tag, caps, façade, **faster same goldfish loop** (`cue → pin_map → mutate` with no new agent step). -**0.19.1–0.19.3** were this (`session_close` + `sessions|n/max` + cap 1024 shipped as `c` because the loop stayed the same). +**0.19.1–0.19.4** were this (`session_close` + `sessions|n/max` + cap 1024 + ranking + nickname-off-`pin_map` shipped as `c` because the loop stayed the same). Pure efficiency / speed on the current loop is **`0.19.c`**, not `0.20`. ### Consequences -- Package stays **0.19.3** until a later cut. Extras **0.10–0.19** stay the owns table below. **1.0** stays unclaimed. +- Package stays **0.19.4** until a later cut. Extras **0.10–0.19** stay the owns table below. **1.0** stays unclaimed. - **1.0 does not wait** on 0.19.c, efficiency, HostSearch, Peak_L, catalog Snap, N-server, or GraphGlot. - A new cabinet adapter does **not** move `a`. Hosted Agens as a product service, first-class `PORT`, and full ACL modes / `session_token` stay Later / unnumbered until a cut exists. @@ -135,7 +135,7 @@ Do **not** treat leftover 0.9 identity as a live-Neo4j claim. Do **not** claim * ## Numbered extras (0.10–0.19) -One concern per **`b`** (usage-method revision). Dependency order. **In package 0.19.0** (git tag by coordinator). Skip a `b` only if the coordinator writes the skip in CHANGELOG; do not fuse two usage-method changes into one `b`. Cuts on the same method are **`c`** (0.19.1–0.19.3). There is **no** 0.20 extra row. +One concern per **`b`** (usage-method revision). Dependency order. **In package 0.19.0** (git tag by coordinator). Skip a `b` only if the coordinator writes the skip in CHANGELOG; do not fuse two usage-method changes into one `b`. Cuts on the same method are **`c`** (0.19.1–0.19.4). There is **no** 0.20 extra row. | Version | Owns | Depends on | MUST NOT | |---------|------|------------|----------| diff --git a/docs/SHAPE.md b/docs/SHAPE.md index febbafe1..aa0b8990 100644 --- a/docs/SHAPE.md +++ b/docs/SHAPE.md @@ -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:** current package is **0.19.3** (extras 0.10–0.19; Neo4j client from 0.9; live claimed as extra **0.14**). PyPI **`memnet-llm==0.19.3`** is published. 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:** current package is **0.19.4** (extras 0.10–0.19; Neo4j client from 0.9; live claimed as extra **0.14**). Last published PyPI **`memnet-llm==0.19.3`** until 0.19.4 upload. Optional extras `[mcp]`, `[agensgraph]`, `[neo4j]` (drivers only). **1.0** stays unclaimed. --- diff --git a/docs/application-notes/system/llm-build-on-memnet.md b/docs/application-notes/system/llm-build-on-memnet.md index 90741027..e1eb3f0e 100644 --- a/docs/application-notes/system/llm-build-on-memnet.md +++ b/docs/application-notes/system/llm-build-on-memnet.md @@ -126,7 +126,7 @@ Every tool returns the same shape (`MemNetResponse.to_json()`): ```json { "exit_code": 0, - "stdout": "(:TSK {id: 'T42', goal: '…', status: 'in_progress'})\n(:NPC {id: 'N03'})-[:helps {id: 'E77'}]->(:TSK {id: 'T42'})\n", + "stdout": "(:TSK {goal: '…', status: 'in_progress'})\n(:NPC {role: 'helper'})-[:helps {note: 'labour'}]->(:TSK {goal: '…'})\n", "stderr": "", "session_id": "mn_abcd", "errors": [] diff --git a/docs/grammar/gql-wire-profile.md b/docs/grammar/gql-wire-profile.md index 2f9ed415..52eaaab1 100644 --- a/docs/grammar/gql-wire-profile.md +++ b/docs/grammar/gql-wire-profile.md @@ -19,7 +19,7 @@ | **One dialect** | Agent teach and wire = **GQL (openCypher-shaped)** only. | | **Three GQL elements** | ISO/IEC 39075 names: **node** (synonym **vertex**), **edge** (synonym **relationship**), **property**. Labels name kinds; they are not a fourth element. Ports, law, `id`, locators are **property** values — not a fourth graph-element kind. | | **No store key** | GraphElement is the identity. `CREATE ()` is legal; properties MAY be empty. MATCH/MERGE = labels + properties; edge = type + ends. **MUST NOT** invent an application store key. Optional `id` = nickname only. Hidden store handle stays in the store, off the wire — not a property, not a business key. Official BNF has no store-key production. | -| **pin_map order** | Emitted `pin_map` sequence is a function of Shape observables (kind + remaining payload; edges: type + endpoint observables). Hid / `_elN` and nickname `id` are **not** ranking keys. CREATE order is not a ranking key. Hid stays off the emit. | +| **pin_map order** | Emitted `pin_map` sequence is a function of Shape observables (kind + remaining payload; edges: type + endpoint observables). Hid / `_elN` and nickname `id` are **not** ranking keys. CREATE order is not a ranking key. Hid stays off the emit. Nickname property `id` **MUST NOT** appear on `pin_map` emit (nodes or edges). Cue / find / `match_nickname` MAY still look up a nickname. | | **No Layer** | Do **not** teach, accept, or dual-path MemNet Layer / Tier A as agent wire. Those sources are **dropped** from `docs/` — not product doctrine. | | **Write = display (redefined)** | Primary agent read = **bounded shaped subgraph** in the same openCypher-family graph shapes used for mutate — not raw tabular `RETURN`. | | **Shaped-read option** | **B with A’s emit shape:** keep a `pin_map`-class tool (optional nickname, depth, view budget) that wraps GQL internally and emits a shaped subgraph. Goldfish seeds from cue/pattern (`find`) first. | @@ -85,7 +85,7 @@ Runtime **MAY** compile the envelope to internal `MATCH` / path patterns. Agents **Caller (0.13).** Each generate packs **at most one** live `pin_map` (or empty-q outline). Drop prior map rows from the chat list. Sparse Commit Δ. Env blobs (test logs, screenshots) stay in the outer harness. `view=shell` is grain on a seed — **not** session outline (0.11). Empty cue is outline of \(S\) (kinds + LIMIT exemplars). leftover `--anchor` is not law. Pytest fail code: `stuffed_maps`. Sibling user-pack may absorb caller text; this repo owns the fail. -**Emit:** shaped subgraph (§5) — openCypher-family **node / edge / property** patterns (or equivalent structured graph), neighbourhood-bounded and view-filtered. Emit **MAY** omit `{id:'…'}`. +**Emit:** shaped subgraph (§5) — openCypher-family **node / edge / property** patterns (or equivalent structured graph), neighbourhood-bounded and view-filtered. Emit **MUST NOT** include nickname property `{id:'…'}` (hid already stays off). Cue by nickname remains a leftover/product cue, not identity. **Honesty:** SysML nests `BoundedMatchFind` beside `PinMapShapedRead` under `Recall` / `AgentShapedRead` (parent `RecallCommit`). Same Recall operator; seed rule is cue/pattern first, optional leftover nickname for a later walk. Find emits **hit nodes only** (`query find` / MCP `find`); then `pin_map` walks. Do **not** teach MATCH…RETURN as goldfish. Product math: [`math-skeleton.md`](math-skeleton.md). leftover `PinMapComposer.require_anchor` / `by_id` — leftoverIssue, not TARGET. @@ -176,7 +176,7 @@ GQL has one relationship primitive. MemNet preserves **two endpoint grains** via | Grain | Rel type | Endpoints | Encoding (**locked for M1 → M2**) | |-------|----------|-----------|-----------------------------------| | **Bind** (ideal pipe / copper / continuity) | `:bind` | Port↔port | Rel properties **`fromPort`** / **`toPort`** (port names on the incident nodes). Optional `carries`. Nodes hold `ports` bags. | -| **Relation** (chart / semantic) | Other types (`:about`, `:helps`, `:derives_result`, …) | Bare node↔node | **No** `fromPort`/`toPort`. Ends are nodes (optional nickname `id` if present). | +| **Relation** (chart / semantic) | Other types (`:about`, `:helps`, `:derives_result`, …) | Bare node↔node | **No** `fromPort`/`toPort`. Ends are nodes (labels + observable properties). Nickname `id` stays off `pin_map` emit. | **MUST:** @@ -199,7 +199,7 @@ GQL has one relationship primitive. MemNet preserves **two endpoint grains** via - Teach **GQL only** as the agent wire: **node** (vertex), **edge** (relationship), **property**. GraphElement is the identity; grammar has no store key. `CREATE ()` is legal. - Seed goldfish from cue/pattern (`find`); use **`pin_map`-class** walks (depth / view / max_rows). Nickname `anchor` is optional. -- Emit **shaped subgraph** for primary agent read (same family as mutate). Emit **MAY** omit `{id:'…'}`. +- Emit **shaped subgraph** for primary agent read (same family as mutate). `pin_map` **MUST NOT** emit nickname `{id:'…'}`. - Treat MATCH/MERGE as labels + properties (edge = type + ends), not a PK. Optional `id` = nickname. leftover_NEW_mint is leftover sugar. - Encode bind as `:bind` + `fromPort`/`toPort`; law on node. @@ -239,16 +239,15 @@ Shaped subgraph = ordered openCypher-family lines (or isomorphic structured grap ```gql (:Label {goal:'…'}) (:Label {goal:'…'})-[:TYPE {note:'…'}]->(:Label {status:'…'}) -(:Label {id:'TSK_optional_nickname', goal:'…'}) ``` **Rules:** - Include the seed neighbourhood and in-budget neighbours only (depth / view / max_rows). -- **MAY** omit `{id:'…'}`. Prefer copyable property maps (law, ports, params, optional nickname) agents need for the next mutate. +- **MUST NOT** emit nickname `{id:'…'}` on nodes or edges. Match the next mutate by labels + observable properties. Cue / find MAY still accept a nickname the agent already holds. - Hide recyclable / out-of-budget neighbours (MN-REQ-04). - Engine-law / control preamble rows **MAY** prepend when authorised — still not a binding table. -- **0.9 leftover emit** still prints `id` because the store keys `by_id` — leftoverIssue, not TARGET. +- Ranking already excludes nickname `id` from the rank key. This cut also drops it from the composed text. ### 5.3 Write = display (redefined) diff --git a/parts/common/memnet/memnet/__init__.py b/parts/common/memnet/memnet/__init__.py index 4d57cf40..c6349ec2 100644 --- a/parts/common/memnet/memnet/__init__.py +++ b/parts/common/memnet/memnet/__init__.py @@ -1,3 +1,3 @@ """MemNet — mission working memory for LLM agents (session graph + pin_map).""" -__version__ = "0.19.3" +__version__ = "0.19.4" diff --git a/parts/common/memnet/memnet/gql.py b/parts/common/memnet/memnet/gql.py index a5883538..29e8a9ab 100644 --- a/parts/common/memnet/memnet/gql.py +++ b/parts/common/memnet/memnet/gql.py @@ -905,9 +905,16 @@ def _emit_props(props: dict[str, str], *, omit_empty: bool = True) -> str: return "{" + ", ".join(parts) + "}" -def emit_node_shaped(kind: str, rid: str, fields: dict[str, str]) -> str: +def emit_node_shaped( + kind: str, + rid: str, + fields: dict[str, str], + *, + include_nickname: bool = True, +) -> str: + """Shaped present-form node. ``include_nickname=False`` for pin_map emit.""" props: dict[str, str] = {} - if rid: + if include_nickname and rid: props["id"] = rid for k, v in fields.items(): if k == "id": @@ -935,9 +942,10 @@ def emit_edge_shaped( dst_id: str, edge_id: str, fields: dict[str, str], + include_nickname: bool = True, ) -> str: rel_props: dict[str, str] = {} - if edge_id: + if include_nickname and edge_id: rel_props["id"] = edge_id # Prefer GQL fromPort/toPort names on the wire for store_key, wire_key in ( @@ -975,8 +983,8 @@ def emit_edge_shaped( rel_props.setdefault("note", v) continue rel_props[k] = v - src_bit = emit_node_shaped(src_kind, src_id, {}) - dst_bit = emit_node_shaped(dst_kind, dst_id, {}) + src_bit = emit_node_shaped(src_kind, src_id, {}, include_nickname=include_nickname) + dst_bit = emit_node_shaped(dst_kind, dst_id, {}, include_nickname=include_nickname) rel_s = f":{rel} {_emit_props(rel_props)}" if rel_props else f":{rel}" return f"{src_bit}-[{rel_s}]->{dst_bit}" diff --git a/parts/common/memnet/memnet/pin_map_composer.py b/parts/common/memnet/memnet/pin_map_composer.py index beb9a949..325e51bc 100644 --- a/parts/common/memnet/memnet/pin_map_composer.py +++ b/parts/common/memnet/memnet/pin_map_composer.py @@ -1,6 +1,8 @@ """PinMapComposer / PinMapShapedRead — live pin map as shaped GQL subgraph. Emits openCypher-family node and relationship lines (gql-wire-profile §5). +Nickname property ``id`` stays off this emit (same effect as DROP_KEYS={id,hid} +on the wire text). Cue / find / match_nickname may still look up a nickname. Optional ``view=`` grain: ``shell`` / ``interior`` taught; ``flowchart`` / ``parts`` / ``statechart`` accepted with soft shell caps. @@ -137,9 +139,6 @@ def record_to_gql_line(rec: Record, *, store=None) -> str: from memnet.gql import _emit_props rel_props: dict[str, str] = {} - nick = rec.id - if nick: - rel_props["id"] = nick for store_key, wire_key in ( ("fromPort", "fromPort"), ("toPort", "toPort"), @@ -152,7 +151,12 @@ def record_to_gql_line(rec: Record, *, store=None) -> str: rel_s = f":{rel} {_emit_props(rel_props)}" if rel_props else f":{rel}" return f"{src_line}-[{rel_s}]->{dst_line}" fields = {k: v for k, v in rec.fields.items() if k != "id"} - return emit_node_shaped(rec.tag if rec.tag != "NODE" else rec.tag, rec.id, fields) + return emit_node_shaped( + rec.tag if rec.tag != "NODE" else rec.tag, + rec.id, + fields, + include_nickname=False, + ) def _endpoint_shaped(store, token: str) -> str: @@ -160,11 +164,10 @@ def _endpoint_shaped(store, token: str) -> str: if store is not None and hasattr(store, "resolve_one"): rec = store.resolve_one(token) if rec is None: - nick = "" if str(token).startswith("_el") else token - return emit_node_shaped("NODE", nick or "", {}) + return emit_node_shaped("NODE", "", {}, include_nickname=False) fields = {k: v for k, v in rec.fields.items() if k != "id"} kind = rec.tag if rec.tag and rec.tag != "EDG" else "NODE" - return emit_node_shaped(kind, rec.id, fields) + return emit_node_shaped(kind, rec.id, fields, include_nickname=False) class PinMapComposer: diff --git a/project.toml b/project.toml index 2ce7df5b..747ee0b5 100644 --- a/project.toml +++ b/project.toml @@ -1,7 +1,7 @@ [project] name = "memnet" repo = "MemNet" -version = "0.19.3" +version = "0.19.4" description = "Mission working memory for LLM agents: session graph + pin_map, not a RAG corpus" # No pcba-libs pins — software-only system (see LAYOUT.md). diff --git a/sysml-models/outputs/system-design-notes.md b/sysml-models/outputs/system-design-notes.md index c3f2178f..72220e07 100644 --- a/sysml-models/outputs/system-design-notes.md +++ b/sysml-models/outputs/system-design-notes.md @@ -198,4 +198,4 @@ reserve and Path-B ingest are **shipped**. - TierA / LegacyPipe* — parked in connections RETIRED archive; MUST NOT nest on product path - EvidenceCentre / MissionDock / CompanyMemory / **HostSearchBridge** / **CousinPointingContrast** — application / contrast nests only; MUST NOT nest under MemNetSystem ([host-search-nest-case-study.md](host-search-nest-case-study.md); `models/cousins.sysml`) - BoundedMatchFind — **shipped** (`implemented=true`; MN-REQ-04.6 / #73 seed-only); pin_map remains default goldfish **from a cue** (empty q = outline, not “when leftover-anchored”) -- pin_map ranking — **MN-REQ-04.11**: emit order is kind + observable payload; hid / nickname `id` / CREATE order are not ranking keys (0.19.c honesty; hid still off the wire) +- pin_map ranking — **MN-REQ-04.11**: emit order is kind + observable payload; hid / nickname `id` / CREATE order are not ranking keys. Nickname `id` stays off `pin_map` emit (0.19.c honesty; hid still off the wire) diff --git a/tests/test_acl.py b/tests/test_acl.py index 130290f8..be6aefbe 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -106,7 +106,7 @@ def test_pin_map_composer_who(memnet_temp, schema_file): MutateGate(ss).apply([_PLR], mode="add", caller="writer") ss.grant_caller("reader", can_pin_map=True, can_mutate=False) text = PinMapComposer(ss).compose(anchor="PLR01", caller="reader")[1] - assert "PLR01" in text + assert "identity: 'Hero'" in text ss.grant_caller("muted", can_pin_map=False, can_mutate=False) with pytest.raises(MemNetError) as ei: @@ -257,7 +257,7 @@ def test_cli_acl_grant_and_deny(memnet_temp, schema_file): ["query", "pin-map", "--anchor", "PLR01", "--session", sid, "--caller", "owner"], ) assert pin.exit_code == 0, pin.stderr - assert "PLR01" in pin.stdout + assert "identity: 'Hero'" in pin.stdout def test_acl_off_by_default_keeps_legacy_green(memnet_temp, schema_file): @@ -266,7 +266,7 @@ def test_acl_off_by_default_keeps_legacy_green(memnet_temp, schema_file): result = MutateGate(ss).apply([_PLR], mode="add") assert result.records text = PinMapComposer(ss).compose(anchor="PLR01")[1] - assert "PLR01" in text + assert "identity: 'Hero'" in text def test_check_write_scope_unit(): diff --git a/tests/test_add_update.py b/tests/test_add_update.py index 12c3892e..9cbf871a 100644 --- a/tests/test_add_update.py +++ b/tests/test_add_update.py @@ -18,7 +18,7 @@ def test_add_then_update(memnet_temp, schema_file): assert runner.invoke(app, ["update", updated, "--session", sid]).exit_code == 0 got = runner.invoke(app, ["query", "pin-map", "--cue", "PLR01", "--session", sid]) assert got.exit_code == 0, got.stderr - assert "2" in got.stdout or "PLR01" in got.stdout + assert "wealth: '2'" in got.stdout or "wealth: 2" in got.stdout def test_add_two_same_nickname_gql(memnet_temp, schema_file): diff --git a/tests/test_bounded_match_find.py b/tests/test_bounded_match_find.py index 1d1c24af..2204891d 100644 --- a/tests/test_bounded_match_find.py +++ b/tests/test_bounded_match_find.py @@ -101,11 +101,11 @@ def test_find_locator_and_keyword(memnet_temp): ], ) assert loc.exit_code == 0, loc.stderr - assert "MOD_z" in loc.stdout + assert "path: 'z.py'" in loc.stdout kw = runner.invoke( app, ["query", "find", "--keyword", "gamma", "--limit", "3", "--session", sid], ) assert kw.exit_code == 0, kw.stderr - assert "TSK_c" in kw.stdout - assert "TSK_a" not in kw.stdout + assert "goal: 'gamma'" in kw.stdout + assert "goal: 'alpha'" not in kw.stdout diff --git a/tests/test_cli.py b/tests/test_cli.py index 799e0499..1fb07eef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -57,8 +57,8 @@ def test_query_pin_map_cli(memnet_temp, schema_file): env={"MEMNET_TEST_INLINE": "1"}, ) assert warm_result.exit_code == 0 - assert "PLR77" in warm_result.stdout assert "(:PLR" in warm_result.stdout + assert "identity:" in warm_result.stdout or "Test" in warm_result.stdout assert "CREATE" not in warm_result.stdout shell_result = runner.invoke( @@ -76,7 +76,6 @@ def test_query_pin_map_cli(memnet_temp, schema_file): env={"MEMNET_TEST_INLINE": "1"}, ) assert shell_result.exit_code == 0 - assert "PLR77" in shell_result.stdout assert "(:PLR" in shell_result.stdout diff --git a/tests/test_durable_store.py b/tests/test_durable_store.py index 7de8a3c9..1036f8d7 100644 --- a/tests/test_durable_store.py +++ b/tests/test_durable_store.py @@ -87,7 +87,7 @@ def test_hydrate_into_session_then_pin_map(memnet_temp): assert "COM_acme" in ids assert "TSK_mission_q3" in ids assert "CREATE" not in text # shaped present form, not mutate - assert "COM_acme" in text + assert "name: 'Acme'" in text assert "ABOUT" in text diff --git a/tests/test_facade_leftover.py b/tests/test_facade_leftover.py index b1af3ed1..5787a537 100644 --- a/tests/test_facade_leftover.py +++ b/tests/test_facade_leftover.py @@ -102,10 +102,10 @@ def test_pin_map_cue_is_product_nickname(memnet_temp): ) pin = runner.invoke(app, ["query", "pin-map", "--cue", "TSK_cue", "--session", sid]) assert pin.exit_code == 0, pin.stderr - assert "TSK_cue" in pin.stdout + assert "goal: 'cued'" in pin.stdout leftover = runner.invoke(app, ["query", "pin-map", "--anchor", "TSK_cue", "--session", sid]) assert leftover.exit_code == 0, leftover.stderr - assert "TSK_cue" in leftover.stdout + assert leftover.stdout == pin.stdout flags = _cli_option_flags("query", "pin-map") assert "--cue" in flags assert "--anchor" in flags @@ -169,5 +169,6 @@ def test_mcp_product_pin_map_cue(memnet_temp, monkeypatch): pin = asyncio.run(pin_map(cue="TSK_mcp", session=sid)) payload = json.loads(pin) assert payload["exit_code"] == 0, payload - assert "TSK_mcp" in payload["stdout"] + assert "goal: 'mcp-cue'" in payload["stdout"] + assert "TSK_mcp" not in payload["stdout"] assert "_el" not in payload["stdout"] diff --git a/tests/test_goldfish_caller.py b/tests/test_goldfish_caller.py index 9f419dba..f2510742 100644 --- a/tests/test_goldfish_caller.py +++ b/tests/test_goldfish_caller.py @@ -111,8 +111,8 @@ def test_empty_cue_is_outline_regardless_of_view(memnet_temp): assert with_shell.exit_code == 0, with_shell.stderr assert "## outline" in with_shell.stdout assert "no_anchor" not in with_shell.stderr - assert "TSK_live" in with_shell.stdout - assert "MOD_x" in with_shell.stdout + assert "goal: 'work'" in with_shell.stdout + assert "path: 'src/x.py'" in with_shell.stdout assert "-[:" not in with_shell.stdout assert "owns" not in with_shell.stdout # view=shell is not the outline operator; empty q is. @@ -140,9 +140,9 @@ def test_view_shell_is_grain_on_a_seed_not_session_outline(memnet_temp): anchor=None, view="shell", kind=None, locators=None, keyword=None ) assert "## outline" in census_text - assert "TSK_live" in census_text - assert "MOD_unlinked" in census_text - assert "USR_one" in census_text + assert "goal: 'work'" in census_text + assert "path: 'src/y.py'" in census_text + assert "topic: 'size'" in census_text assert "-[:" not in census_text assert all(r.tag != "EDG" for r in census) @@ -152,8 +152,8 @@ def test_view_shell_is_grain_on_a_seed_not_session_outline(memnet_temp): kind="TSK", locators=[("goal", "work")], ) - assert "TSK_live" in text - assert "MOD_unlinked" not in text - assert "USR_one" not in text + assert "goal: 'work'" in text + assert "path: 'src/y.py'" not in text + assert "topic: 'size'" not in text kinds = {r.tag for r in rows if r.tag not in {"LAW", "EDG"}} assert kinds <= {"TSK"} diff --git a/tests/test_goldfish_paradox.py b/tests/test_goldfish_paradox.py index 8bbf11ca..ce421f89 100644 --- a/tests/test_goldfish_paradox.py +++ b/tests/test_goldfish_paradox.py @@ -42,15 +42,15 @@ def test_v1_isolated_tsk_hides_unlinked_mod(memnet_temp): ) first = _pin(sid, "TSK_x") assert first.exit_code == 0, first.stderr - assert "TSK_x" in first.stdout - assert "MOD_y" not in first.stdout + assert "goal: 'solo'" in first.stdout + assert "src/y.py" not in first.stdout _add( sid, "MATCH (t {id: 'TSK_x'}), (m {id: 'MOD_y'})\nCREATE (t)-[:owns {id: 'NEW'}]->(m)\n", ) second = _pin(sid, "TSK_x") assert second.exit_code == 0, second.stderr - assert "MOD_y" in second.stdout + assert "src/y.py" in second.stdout def test_v3_empty_read_list_then_empty_cue_outlines_s(memnet_temp): @@ -84,7 +84,7 @@ def test_v4_sparse_owns_edge(memnet_temp): after = _pin(sid, "TSK_a") assert after.exit_code == 0, after.stderr assert "owns" in after.stdout - assert "MOD_a" in after.stdout + assert "src/a.py" in after.stdout def test_v6_two_same_nickname_stay_two(memnet_temp): @@ -132,10 +132,11 @@ def test_v2_union_under_one_m(memnet_temp): law_lines = [ln for ln in payload if ln.startswith("(:LAW")] body = [ln for ln in payload if not ln.startswith("(:LAW")] assert law_lines, both.stdout - assert both.stdout.count("(:LAW {id: 'LAW01'") == 1 + assert both.stdout.count("(:LAW") == 1 + assert "name: 'EDG'" in both.stdout assert payload[: len(law_lines)] == law_lines assert len(body) <= 5 - mods = {ln for ln in body if "MOD_" in ln} + mods = {ln for ln in body if "path:" in ln} assert len(mods) < 6, "both stars must not fully expand under one M" @@ -159,13 +160,13 @@ def test_v5_n_pin_maps_repeat_law(memnet_temp): for i, aid in enumerate(anchors): r = _pin(sid, aid) assert r.exit_code == 0, r.stderr - n = r.stdout.count("(:LAW {id: 'LAW01'") + n = r.stdout.count("(:LAW") assert n == 1, r.stdout law_hits += n if i == 0: first_out = r.stdout assert law_hits == 5 - assert "TSK_v5" in first_out + assert "goal: 'one-task'" in first_out def test_v9_raw_degree_contains_parent_is_not_peak(memnet_temp): @@ -199,7 +200,7 @@ def test_v9_raw_degree_contains_parent_is_not_peak(memnet_temp): ) assert miss.exit_code == 0, miss.stderr assert "## outline" not in miss.stdout - assert "TSK_live" in miss.stdout + assert "goal: 'work'" in miss.stdout assert "owns" in miss.stdout assert "contains" not in miss.stdout assert "CueConflict" not in miss.stdout diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 34849f67..fe29ccb6 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -41,7 +41,8 @@ def test_run_memnet_add_and_warm(memnet_temp, schema_file): session=sid, ) assert warm_resp.exit_code == 0 - assert "PLR77" in warm_resp.stdout + assert "(:PLR" in warm_resp.stdout + assert "identity:" in warm_resp.stdout def test_serve_required_without_inline(monkeypatch): @@ -147,7 +148,8 @@ def test_query_warm_tool_envelope(memnet_temp, schema_file, monkeypatch): warm_raw = asyncio.run(pin_map(anchor="PLR55", depth=1, session=sid)) warm_payload = json.loads(warm_raw) assert warm_payload["exit_code"] == 0 - assert "PLR55" in warm_payload["stdout"] + assert "(:PLR" in warm_payload["stdout"] + assert "identity:" in warm_payload["stdout"] assert warm_payload["errors"] == [] alias_raw = asyncio.run(query_warm(anchor="PLR55", depth=1, session=sid)) @@ -157,7 +159,7 @@ def test_query_warm_tool_envelope(memnet_temp, schema_file, monkeypatch): shell_raw = asyncio.run(pin_map(anchor="PLR55", depth=2, view="shell", session=sid)) shell_payload = json.loads(shell_raw) assert shell_payload["exit_code"] == 0 - assert "PLR55" in shell_payload["stdout"] + assert "(:PLR" in shell_payload["stdout"] bad_raw = asyncio.run(pin_map(anchor="PLR55", view="persons", session=sid)) bad_payload = json.loads(bad_raw) @@ -321,8 +323,8 @@ def test_session_open_seed_lines(memnet_temp, schema_file, monkeypatch): warm_raw = asyncio.run(pin_map(anchor="CFG01", depth=1, session=sid)) warm_payload = json.loads(warm_raw) assert warm_payload["exit_code"] == 0 - assert "CFG01" in warm_payload["stdout"] - assert "LAW01" in warm_payload["stdout"] + assert "daily_news" in warm_payload["stdout"] or "(:CFG" in warm_payload["stdout"] + assert "atomise" in warm_payload["stdout"] or "(:LAW" in warm_payload["stdout"] def test_mcp_session_list_header_and_close_decrements(memnet_temp, schema_file, monkeypatch): diff --git a/tests/test_mission_warm.py b/tests/test_mission_warm.py index 10746500..53912a24 100644 --- a/tests/test_mission_warm.py +++ b/tests/test_mission_warm.py @@ -28,7 +28,7 @@ def test_mission_warm_hides_settled(memnet_temp, schema_file, workflow_file): ) assert warm.exit_code == 0 assert "T01" not in warm.stdout or "delete_on_settle" not in warm.stdout - assert "LAW01" in warm.stdout or "(:LAW" in warm.stdout + assert "(:LAW" in warm.stdout assert "CREATE (:" not in warm.stdout still = runner.invoke( diff --git a/tests/test_mutate_gate.py b/tests/test_mutate_gate.py index 2a8ac1ad..09f34180 100644 --- a/tests/test_mutate_gate.py +++ b/tests/test_mutate_gate.py @@ -88,7 +88,7 @@ def test_pin_map_composer_unit(memnet_temp, schema_file): ) text = PinMapComposer(ss).compose(anchor="PLR01", depth=1)[1] assert "(:PLR" in text - assert "PLR01" in text + assert "identity: 'Hero'" in text assert "CREATE" not in text diff --git a/tests/test_peak_l.py b/tests/test_peak_l.py index b9d68106..3bd9eb28 100644 --- a/tests/test_peak_l.py +++ b/tests/test_peak_l.py @@ -81,7 +81,7 @@ def test_v9_contains_parent_is_not_rho_star_peak(memnet_temp): ) assert miss.exit_code == 0, miss.stderr assert "## outline" not in miss.stdout - assert "TSK_live" in miss.stdout + assert "goal: 'work'" in miss.stdout assert "owns" in miss.stdout assert "contains" not in miss.stdout assert "CueConflict" not in miss.stdout @@ -108,9 +108,9 @@ def test_peak_not_used_when_codebook_hits(memnet_temp): ], ) assert hit.exit_code == 0, hit.stderr - assert "PKG_root" in hit.stdout + assert "name: 'root'" in hit.stdout assert "contains" in hit.stdout - assert "TSK_live" not in hit.stdout + assert "goal: 'work'" not in hit.stdout assert "## CueConflict" not in hit.stdout @@ -166,8 +166,8 @@ def test_two_peaks_cue_conflict(memnet_temp): assert miss.exit_code == 0, miss.stderr assert "CueConflict" in miss.stdout assert "|Q|=2" in miss.stdout - assert "TSK_a" in miss.stdout - assert "TSK_b" in miss.stdout + assert "goal: 'star-a'" in miss.stdout + assert "goal: 'star-b'" in miss.stdout assert "-[:" not in miss.stdout assert "_el" not in miss.stdout diff --git a/tests/test_pin_map_export.py b/tests/test_pin_map_export.py index d0c46fdb..47b57e57 100644 --- a/tests/test_pin_map_export.py +++ b/tests/test_pin_map_export.py @@ -47,7 +47,7 @@ def test_export_cue_is_shaped_pin_map_not_dump(memnet_temp): out = r.stdout assert out.startswith("@EXPORT: pin-map|cue=kind:TSK") assert "(:TSK" in out - assert "TSK_live" in out + assert "goal: 'export-cut'" in out assert "CREATE" not in out assert "MERGE" not in out assert "_el" not in out diff --git a/tests/test_pin_map_observable_rank.py b/tests/test_pin_map_observable_rank.py index fd1b1b29..d53ff5d6 100644 --- a/tests/test_pin_map_observable_rank.py +++ b/tests/test_pin_map_observable_rank.py @@ -106,6 +106,25 @@ def test_isomorphic_create_shuffle_same_pin_map_sequence(memnet_temp): assert seq == first label_sets = [frozenset(seq) for seq in sequences] assert len(set(label_sets)) == 1 + assert len(set(raw_texts)) == 1 + wire = raw_texts[0] + assert not re.search(r"(?:\{|,)\s*id\s*:", wire) + assert "TSK_hub_a" not in wire + assert "TSK_H" not in wire + + +def test_pin_map_omits_nickname_id_property(memnet_temp): + ss = _build_session( + memnet_temp, + {"hub": "NICK_HUB", "a": "NICK_A", "b": "NICK_B", "note": "NICK_NOTE"}, + node_order=(0, 1, 2, 3), + edge_order=(0, 1, 2), + ) + text, _canon = _pin_map(ss) + assert "NICK_HUB" not in text + assert not re.search(r"(?:\{|,)\s*id\s*:", text) + assert "slug: 'alpha'" in text + assert ss.store.match_nickname("NICK_HUB") def test_find_seed_order_follows_observables_not_hid(memnet_temp): diff --git a/tests/test_pin_map_view.py b/tests/test_pin_map_view.py index d6f23ada..be2820b3 100644 --- a/tests/test_pin_map_view.py +++ b/tests/test_pin_map_view.py @@ -122,7 +122,7 @@ def test_compose_omit_view_unchanged(memnet_temp, schema_file): a = PinMapComposer(ss).compose(anchor="PLR01", depth=1)[1] b = PinMapComposer(ss).compose(anchor="PLR01", depth=1, view=None)[1] assert a == b - assert "PLR01" in a + assert "identity: 'Hero'" in a assert "(:PLR" in a diff --git a/tests/test_schemes.py b/tests/test_schemes.py index 64b537b2..8fcd8d76 100644 --- a/tests/test_schemes.py +++ b/tests/test_schemes.py @@ -17,9 +17,9 @@ def test_s01_goldfish_refresh(memnet_temp, schema_file, workflow_file): runner.invoke(app, ["add", "--file", str(workflow_file), "--session", sid]) warm = runner.invoke(app, ["query", "warm", "--anchor", "PLR01", "--session", sid]) assert warm.exit_code == 0 - assert "LAW" in warm.stdout or "LAW01" in warm.stdout - assert "PLR01" in warm.stdout - assert "(:PLR" in warm.stdout or "id: 'PLR01'" in warm.stdout + assert "LAW" in warm.stdout or "EDG" in warm.stdout + assert "identity:" in warm.stdout + assert "(:PLR" in warm.stdout @pytest.mark.scheme("S08") diff --git a/tests/test_session.py b/tests/test_session.py index 58e9a8a2..9b586815 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -33,7 +33,8 @@ def test_open_write_resume_cross_process(memnet_temp, schema_file, workflow_file r4 = runner.invoke(app, ["query", "pin-map", "--cue", "PLR01", "--session", sid]) assert r4.exit_code == 0, r4.stderr - assert "PLR01" in r4.stdout + assert "(:PLR" in r4.stdout + assert "identity:" in r4.stdout def test_session_expired(memnet_temp, schema_file): diff --git a/tests/test_session_outline.py b/tests/test_session_outline.py index fa508f35..bccf4b82 100644 --- a/tests/test_session_outline.py +++ b/tests/test_session_outline.py @@ -45,11 +45,9 @@ def test_empty_cue_outline_is_census_of_s(memnet_temp): assert "TSK" in out assert "MOD" in out assert "SYM" in out - assert "TSK_model_memnet" in out + assert "goal: 'model'" in out assert "status: 'open'" in out - assert "MOD_deploy" in out assert "path: 'models/deploy.sysml'" in out - assert "SYM_Recall" in out assert "name: 'Recall'" in out assert "-[:" not in out assert "owns" not in out @@ -78,8 +76,8 @@ def test_empty_cue_with_view_shell_is_still_outline_not_shell_hop(memnet_temp): ) assert r.exit_code == 0, r.stderr assert "## outline" in r.stdout - assert "TSK_live" in r.stdout - assert "MOD_x" in r.stdout + assert "goal: 'work'" in r.stdout + assert "path: 'src/x.py'" in r.stdout assert "-[:" not in r.stdout assert "owns" not in r.stdout assert "_el" not in r.stdout @@ -112,7 +110,8 @@ def test_outline_cue_conflict_if_exemplar_name_collides(memnet_temp): assert r.exit_code == 0, r.stderr assert "## outline" in r.stdout assert "CueConflict" in r.stdout - assert r.stdout.count("trump") >= 2 + assert r.stdout.count("goal: 'same-name'") >= 2 + assert "trump" not in r.stdout assert "-[:" not in r.stdout assert "_el" not in r.stdout from memnet.session import get_session diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index e55d59a0..565d0106 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -51,10 +51,10 @@ def test_cli_session_save_load(memnet_temp, schema_file, workflow_file, tmp_path assert new_sid.startswith("mn_") warm = runner.invoke(app, ["query", "warm", "--anchor", "PLR01", "--session", new_sid]) assert warm.exit_code == 0 - assert "PLR01" in warm.stdout + assert "identity:" in warm.stdout assert "@PLR:" not in warm.stdout assert "(:PLR" in warm.stdout - assert "id: 'PLR01'" in warm.stdout + assert "id: 'PLR01'" not in warm.stdout def test_snapshot_text_matches_file(memnet_temp, schema_file): From 69b0bf51911d43b739c7e33ea01a89f324eb7a6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 03:49:31 +0000 Subject: [PATCH 2/3] Note Hatch 0.19.4 in docs README version map Co-authored-by: chouswei --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 20299564..27f8c8aa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ Do **not** teach Layer / Tier A. **1.0** = claim of 0.5–0.8 (unclaimed). | Doc | Role | |-----|------| | [`SHAPE.md`](SHAPE.md) | Product shape from the problem (0.8 teach) | -| [`ROADMAP.md`](ROADMAP.md) | **SemVer SSOT** — locked `a.b.c`; extras 0.10–0.19 in Hatch **0.19.3** | +| [`ROADMAP.md`](ROADMAP.md) | **SemVer SSOT** — locked `a.b.c`; extras 0.10–0.19 in Hatch **0.19.4** | | [`adr/ADR-001-gql-agent-wire.md`](adr/ADR-001-gql-agent-wire.md) | Accepted: GQL wire; **no Layer** | ## Wire — `grammar/` From f00bd624c3c39e18bef005b2255ae3e683e7b682 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 03:51:41 +0000 Subject: [PATCH 3/3] Assert locators, not nicknames, on pin_map in leftover tests Co-authored-by: chouswei --- tests/test_local_ipc_gateway.py | 2 +- tests/test_pin_map_view.py | 2 +- tests/test_serve_stdin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_local_ipc_gateway.py b/tests/test_local_ipc_gateway.py index 9f7b5ea7..963c3695 100644 --- a/tests/test_local_ipc_gateway.py +++ b/tests/test_local_ipc_gateway.py @@ -83,7 +83,7 @@ def test_second_client_pin_map_via_socket(ipc_serve, schema_file): path=sock, ) assert pin_resp["exit_code"] == 0, pin_resp - assert "PLR50" in pin_resp["stdout"] + assert "identity: 'IpcShare'" in pin_resp["stdout"] def test_ipc_probe_false_when_down(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): diff --git a/tests/test_pin_map_view.py b/tests/test_pin_map_view.py index be2820b3..04fa708e 100644 --- a/tests/test_pin_map_view.py +++ b/tests/test_pin_map_view.py @@ -101,7 +101,7 @@ def test_compose_view_shell_caps_fanout(memnet_temp): edges_shell = [r for r in rows_shell if r.tag == "EDG"] assert len(nodes_shell) <= SHELL_MAX_NODES assert len(edges_shell) <= SHELL_MAX_EDGES - assert "CST_Hub" in text_shell + assert "name: 'hub'" in text_shell assert "(:CST" in text_shell assert nodes_shell[0].id == "CST_Hub" or any(r.id == "CST_Hub" for r in nodes_shell) diff --git a/tests/test_serve_stdin.py b/tests/test_serve_stdin.py index e95448c7..809e23da 100644 --- a/tests/test_serve_stdin.py +++ b/tests/test_serve_stdin.py @@ -84,7 +84,7 @@ def test_send_command_stdin_add(memnet_serve, schema_file): port=port, ) assert get_resp["exit_code"] == 0 - assert "PLR99" in get_resp["stdout"] + assert "identity: 'Test'" in get_resp["stdout"] def test_send_command_stdin_update(memnet_serve, schema_file):