diff --git a/AGENTS.md b/AGENTS.md index 346a85de..ac96665b 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.18** (Hatch SSOT; last published PyPI **`memnet-llm==0.19.16`** until this cut is uploaded; 0.19.18 honesty `c`: snap_model bounded session grain (#199). Earlier `c` cuts include 0.19.17 product-gate cap contract (#196) and admin-only serve usage report (#197), 0.19.16 shared Path-B ingest defaults 2000 nodes / 2000 edges, 0.19.15 storage role labels (#186), Path-B SysML ingest kinds (#188), Neo4j retired (#189), 0.19.14 TTL expire restore by known sid, and 0.19.11 catalog cross-session satisfy locators + CousinSysMLEdge mustNotInventUploadBind). **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.19** (Hatch SSOT; last published PyPI **`memnet-llm==0.19.18`** until this cut is uploaded; 0.19.19 honesty `c`: one-session-per-document serve readiness probe (#201) and lossless snapshot round-trip, one decoded value cap, honoured-or-refused WHERE, ACL who on save/load/close, and expire-save failures kept live (#202). Earlier `c` cuts include 0.19.18 snap_model bounded session grain (#199), 0.19.17 product-gate cap contract (#196) and admin-only serve usage report (#197), 0.19.16 shared Path-B ingest defaults 2000 nodes / 2000 edges, 0.19.15 storage role labels (#186), Path-B SysML ingest kinds (#188), Neo4j retired (#189), 0.19.14 TTL expire restore by known sid, and 0.19.11 catalog cross-session satisfy locators + CousinSysMLEdge mustNotInventUploadBind). **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 94265202..c2ab5ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,21 @@ This project uses Semantic Versioning as **interpreted for MemNet**: package `a. ## [Unreleased] +### Changed +- **Invent only — ClusterRoute vs SliceHandCarry (#191 / #47 cousin)** — `MemNetTwoMoves` outside `MemNetSystem` (`MN-REQ-06.9` + `MN-REQ-06.10` / `MN-VER-06-S08`). ClusterRoute = where the session lives (`MemNetLanMcpFront`; one owner; `pin_map` / `find` SHALL NOT span backends). SliceHandCarry = explicit copy into another session (`export_pin_map` or `session_save` → LAN file copy → dest import/`session_load`; `import_slice` same-serve only). Not a live hop. `import_slice(from_url)` not shipped. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/cluster-route-vs-slice-hand-carry.md`](docs/operations/cluster-route-vs-slice-hand-carry.md). +- **Invent only — LAN MCP front over several serves (#191)** — `MemNetLanMcpFront` outside `MemNetSystem` (`MN-REQ-06.9` / `MN-VER-06-S07`). One MCP catalogue, N LAN `memnet serve` backends; `SessionOwnerRegistry` is owner (explicit pin allowed; silent hash is not sole routing). One owner per session; `pin_map` / `find` SHALL NOT span backends. Cousin of #47 (peer sid handoff), not the same invent. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/memnet-lan-mcp-front.md`](docs/operations/memnet-lan-mcp-front.md). + +## [0.19.19] - 2026-10-08 + ### Added -- **Honesty `c` — one session per document over serve (probe)** — Loopback `memnet-serve` readiness for a product gate that keeps one session per document (TTL 60, save-on-expire, `MEMNET_MAX_SESSIONS=1024`, ~1 800 parts). Extra probes E11–E14, E12 on a fulldoc-with-edges fixture (3000 nodes + 4500 edges at 5000 and 10000), E16 latency, E17 `WHERE CONTAINS`, E18 snapshot `value_bytes` (decoded vs escaped) / tab-CR / `max_fields` vs `line_bytes`, and RSS fixtures. Probe, tests, and [`docs/operations/one-session-per-document.md`](docs/operations/one-session-per-document.md) now match the engine fix (lossless snapshot, honoured WHERE, ACL who on save/load/close). No SemVer bump. +- **Honesty `c` — one session per document over serve (probe, #201)** — Loopback `memnet-serve` readiness for a product gate that keeps one session per document (TTL 60, save-on-expire, `MEMNET_MAX_SESSIONS=1024`, ~1 800 parts). Extra probes E11–E14, E12 on a fulldoc-with-edges fixture (3000 nodes + 4500 edges at 5000 and 10000), E16 latency, E17 `WHERE CONTAINS`, E18 snapshot `value_bytes` (decoded vs escaped) / tab-CR / `max_fields` vs `line_bytes`, and RSS fixtures. Probe, tests, and [`docs/operations/one-session-per-document.md`](docs/operations/one-session-per-document.md) now match the engine fix (lossless snapshot, honoured WHERE, ACL who on save/load/close). Shipped in 0.19.19 (#201). ### Fixed -- **Honesty `c` — snapshot lossless round-trip, value cap, WHERE, ACL who (#201 follow-on)** — Snapshot emit escapes every `str.splitlines()` separator (LF/CR/VT/FF/FS/GS/RS/NEL/LS/PS) plus `|`/`\`; load splits records on LF only. Undeclared RAM properties persist by widening the snapshot SCHEMA (live SCHEMA unchanged; extras on EDG/LAW or over `max_fields` fail closed). `line_bytes` is the escaped/raw line at save verify and load. Save fails closed (`snapshot_unsaveable`) rather than write an unloadable file. Expire-save that cannot write (`snapshot_unsaveable` or any other save failure) writes no file and **keeps RAM**; the session still counts against `MEMNET_MAX_SESSIONS`, retries emit `@WRN: expire_snapshot_failed|{code}`, and access after TTL is `session_expired|overdue` until an explicit successful save or close. When save-on-expire is off, TTL still drops RAM. GQL mutate, leftover pipe, and snapshot load share one decoded `MEMNET_MAX_VALUE_BYTES` cap (`limit_exceeded|value_bytes n/max`). MATCH WHERE SET/DELETE honours the predicate or refuses `unsupported_predicate` with nothing applied. Session save/load/close who-check when ACL is enabled (`--caller` / `MEMNET_CALLER`; MCP passes `caller`). 0.19.18 snapshots still load. MN-REQ-01.9 / 01.10 / 03.4 / 05.3. No version bump. +- **Honesty `c` — snapshot lossless round-trip, value cap, WHERE, ACL who (#202)** — Snapshot emit escapes every `str.splitlines()` separator (LF/CR/VT/FF/FS/GS/RS/NEL/LS/PS) plus `|`/`\`; load splits records on LF only. Undeclared RAM properties persist by widening the snapshot SCHEMA (live SCHEMA unchanged; extras on EDG/LAW or over `max_fields` fail closed). `line_bytes` is the escaped/raw line at save verify and load. Save fails closed (`snapshot_unsaveable`) rather than write an unloadable file. Expire-save that cannot write (`snapshot_unsaveable` or any other save failure) writes no file and **keeps RAM**; the session still counts against `MEMNET_MAX_SESSIONS`, retries emit `@WRN: expire_snapshot_failed|{code}`, and access after TTL is `session_expired|overdue` until an explicit successful save or close. When save-on-expire is off, TTL still drops RAM. GQL mutate, leftover pipe, and snapshot load share one decoded `MEMNET_MAX_VALUE_BYTES` cap (`limit_exceeded|value_bytes n/max`). MATCH WHERE SET/DELETE honours the predicate or refuses `unsupported_predicate` with nothing applied. Session save/load/close who-check when ACL is enabled (`--caller` / `MEMNET_CALLER`; MCP passes `caller`). 0.19.18 snapshots still load. MN-REQ-01.9 / 01.10 / 03.4 / 05.3. Shipped in 0.19.19 (#202). ### Changed -- **Invent only — ClusterRoute vs SliceHandCarry (#191 / #47 cousin)** — `MemNetTwoMoves` outside `MemNetSystem` (`MN-REQ-06.9` + `MN-REQ-06.10` / `MN-VER-06-S08`). ClusterRoute = where the session lives (`MemNetLanMcpFront`; one owner; `pin_map` / `find` SHALL NOT span backends). SliceHandCarry = explicit copy into another session (`export_pin_map` or `session_save` → LAN file copy → dest import/`session_load`; `import_slice` same-serve only). Not a live hop. `import_slice(from_url)` not shipped. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/cluster-route-vs-slice-hand-carry.md`](docs/operations/cluster-route-vs-slice-hand-carry.md). -- **Invent only — LAN MCP front over several serves (#191)** — `MemNetLanMcpFront` outside `MemNetSystem` (`MN-REQ-06.9` / `MN-VER-06-S07`). One MCP catalogue, N LAN `memnet serve` backends; `SessionOwnerRegistry` is owner (explicit pin allowed; silent hash is not sole routing). One owner per session; `pin_map` / `find` SHALL NOT span backends. Cousin of #47 (peer sid handoff), not the same invent. tip≠face. `inventOnly=true`; `implemented=false`; no engine code; no SemVer bump. Wire: [`docs/operations/memnet-lan-mcp-front.md`](docs/operations/memnet-lan-mcp-front.md). +- **Honesty `c` — release summary for product gates (#201 / #202)** — (1) Snapshot `session_save` → `session_load` is lossless for long and multi-line text (every `splitlines()` separator escaped). (2) One value cap: `MEMNET_MAX_VALUE_BYTES` (default 4096; a product MAY raise it to 16384 per the cap contract), counted on decoded UTF-8, shared by GQL mutate, pipe, and snapshot load; over-cap refuses `@ERR: limit_exceeded|value_bytes n/max`. (3) An unhonoured MATCH `WHERE` refuses `@ERR: unsupported_predicate|WHERE {name} is not honoured` with nothing applied; `WHERE true` is still honoured (edge delete `MATCH (n WHERE true)-[r {id}]->() DELETE r` keeps working). (4) With ACL on, the who-check covers `session_save`, `session_load`, and `session_close`. (5) A save that cannot round-trip refuses `@ERR: snapshot_unsaveable|…` and writes no file. (6) Expire-save failure keeps the session in RAM (still counts against `MEMNET_MAX_SESSIONS`) with `@WRN: expire_snapshot_failed|{code}`; access after TTL is `@ERR: session_expired|overdue` until a successful save or explicit close. 0.19.18 snapshots still load. +- **Package identity 0.19.19** — Hatch / `project.toml` / `memnet.__version__` honesty cut on **0.19**. Agent loop (`cue → pin_map → mutate`) unchanged; product gates that sent an unhonoured `WHERE` now get a refuse instead of a silent skip. ## [0.19.18] - 2026-10-08 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f1d698d0..0c488c1d 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.18** (`memnet.__version__`). Last published PyPI wheel is **`memnet-llm==0.19.16`** until this cut is uploaded. Numbered extras **0.10–0.19** are in this package (unchanged). **0.19.18** honesty `c` is snap_model bounded session grain (#199). **0.19.17** honesty `c` was the product-gate cap contract (#196) and admin-only serve usage report (#197). **0.19.16** honesty `c` was shared Path-B ingest defaults (2000 nodes / 2000 edges). **0.19.15** honesty `c` was storage role labels (#186), Path-B SysML ingest kinds (#188), and Neo4j retired (#189). **0.19.14** honesty `c` was 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). +**Package now:** Hatch **0.19.19** (`memnet.__version__`). Last published PyPI wheel is **`memnet-llm==0.19.18`** until this cut is uploaded. Numbered extras **0.10–0.19** are in this package (unchanged). **0.19.19** honesty `c` is the one-session-per-document readiness probe (#201) plus lossless snapshot round-trip, one decoded value cap, honoured-or-refused WHERE, ACL who on save/load/close, and expire-save failures kept live (#202). **0.19.18** honesty `c` was snap_model bounded session grain (#199). **0.19.17** honesty `c` was the product-gate cap contract (#196) and admin-only serve usage report (#197). **0.19.16** honesty `c` was shared Path-B ingest defaults (2000 nodes / 2000 edges). **0.19.15** honesty `c` was storage role labels (#186), Path-B SysML ingest kinds (#188), and Neo4j retired (#189). **0.19.14** honesty `c` was 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-10-08 (`a.b.c` law locked; package **0.19.18** honesty `c` — snap_model bounded session grain (#199); extras **0.10–0.19** unchanged; Hatch **0.19.18**; last published PyPI **`memnet-llm==0.19.16`**; do not claim **1.0**; do not invent a **0.20** extra; do not claim tip=face). +**Last updated:** 2026-10-08 (`a.b.c` law locked; package **0.19.19** honesty `c` — doc-gate readiness (#201) and snapshot/value-cap/WHERE/ACL/expire fixes (#202); extras **0.10–0.19** unchanged; Hatch **0.19.19**; last published PyPI **`memnet-llm==0.19.18`**; do not claim **1.0**; do not invent a **0.20** extra; do not claim tip=face). 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. @@ -45,7 +45,7 @@ Pure efficiency / speed on the current loop is **`0.19.c`**, not `0.20`. ### Consequences -- Package is **0.19.18**. Extras **0.10–0.19** stay the owns table below. **1.0** stays unclaimed. +- Package is **0.19.19**. 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. @@ -99,8 +99,8 @@ Handoff = **session id** (+ cue / write scope). Peers **re-`pin_map`** from labe | **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.18**; last published PyPI **`memnet-llm==0.19.16`**; extras first shipped as 0.19.0) | -| **1.0.0** | **Claim** of **0.5 + 0.6 + 0.7 + 0.8**. Shape mature for people. Not GraphRAG. Not cabinet-only. Not a new engine. This is `a=1` | **Claim when coordinator tags** — package **0.19.18** does not claim 1.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.19**; last published PyPI **`memnet-llm==0.19.18`**; extras first shipped as 0.19.0) | +| **1.0.0** | **Claim** of **0.5 + 0.6 + 0.7 + 0.8**. Shape mature for people. Not GraphRAG. Not cabinet-only. Not a new engine. This is `a=1` | **Claim when coordinator tags** — package **0.19.19** does not claim 1.0 | | **Later** | Grammar Open / hosted product / leftover ACL; N-server research (#47); LAN MCP front invent (#191). GraphGlot parse-front is **shipped**. If **1.0 tags first**, remaining extras become **1.1, 1.2, …** with the same owns (`b` after the claim) | **Out** of 1.0 | **1.0 MAY ship from 0.9** (claim only). **0.10+ MAY ship before 1.0** as extras (`b` on `a=0`). Do not wait for the other. User-pack GQL rewrite is **sibling** (`chouswei/cursor-user-skills`), not this repo. @@ -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.18). 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.19). There is **no** 0.20 extra row. | Version | Owns | Depends on | MUST NOT | |---------|------|------------|----------| diff --git a/parts/common/memnet/memnet/__init__.py b/parts/common/memnet/memnet/__init__.py index 089c6b11..1462a382 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.18" +__version__ = "0.19.19" diff --git a/project.toml b/project.toml index 097cb33a..39eee610 100644 --- a/project.toml +++ b/project.toml @@ -1,7 +1,7 @@ [project] name = "memnet" repo = "MemNet" -version = "0.19.18" +version = "0.19.19" 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/tests/test_doc_gate_readiness.py b/tests/test_doc_gate_readiness.py index 5edd12b6..8e2c7a43 100644 --- a/tests/test_doc_gate_readiness.py +++ b/tests/test_doc_gate_readiness.py @@ -51,8 +51,8 @@ from memnet.snapshot import SNAPSHOT_MAGIC -def test_version_is_0_19_18(): - assert __version__ == "0.19.18" +def test_version_is_0_19_19(): + assert __version__ == "0.19.19" def test_cap_contract_needles_unchanged(): diff --git a/tests/test_sysml_cluster_vs_slice.py b/tests/test_sysml_cluster_vs_slice.py index 9b94551d..5a049b30 100644 --- a/tests/test_sysml_cluster_vs_slice.py +++ b/tests/test_sysml_cluster_vs_slice.py @@ -111,5 +111,5 @@ def test_teach_one_screen_contrast(): def test_no_semver_bump_in_this_invent(): text = _PROJECT.read_text(encoding="utf-8") - assert 'version = "0.19.18"' in text - assert "0.19.19" not in text + assert 'version = "0.19.19"' in text + assert "0.19.20" not in text diff --git a/tests/test_sysml_lan_mcp_front.py b/tests/test_sysml_lan_mcp_front.py index d4e3655c..e5c080cb 100644 --- a/tests/test_sysml_lan_mcp_front.py +++ b/tests/test_sysml_lan_mcp_front.py @@ -120,5 +120,5 @@ def test_teach_contrasts_47_and_worth_building(): def test_no_semver_bump_in_this_invent(): text = _PROJECT.read_text(encoding="utf-8") - assert 'version = "0.19.18"' in text - assert "0.19.19" not in text + assert 'version = "0.19.19"' in text + assert "0.19.20" not in text