From d16181a3f76de1a53172466d609e74ec6102f4e3 Mon Sep 17 00:00:00 2001 From: cdeust Date: Wed, 29 Jul 2026 14:50:44 +0200 Subject: [PATCH 1/2] fix(core): break the synaptic-plasticity import cycle (#233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit synaptic_plasticity.py held the Tsodyks-Markram implementation AND back-imported its two siblings at the bottom of the file behind `# noqa: E402`, while synaptic_plasticity_hebbian.py and synaptic_plasticity_stochastic.py imported names back out of it. In a fresh interpreter, whichever module was imported first decided whether the import worked at all: $ python -c "import mcp_server.core.synaptic_plasticity_hebbian" ImportError: cannot import name 'apply_hebbian_update' from partially initialized module ... (most likely due to a circular import) Extract the implementation down into a new leaf module, synaptic_plasticity_stp.py — Tsodyks-Markram state and dynamics, noise injection, theta-phase gating — whose only imports are math, random and dataclasses. The two siblings now depend on the leaf; synaptic_plasticity becomes a pure re-export facade with the same 14-name __all__. Every dependency points one way, so all four modules import in any order and both `# noqa: E402` markers are gone with the cycle that required them. The moved code is byte-identical to the lines it came from (verified by diffing HEAD's lines 33-227 against the new file): no constant, equation or `# source:` comment was touched. Add tests_py/core/test_import_isolation.py, which imports each module in a SEPARATE interpreter. An in-process import cannot reproduce the class — pytest imports the facade first and every later import is a sys.modules cache hit, which is why the suite stayed green the whole time main was broken. On the pre-fix tree the new file fails 3 of its 4 cases. Mutation testing on the relocated code (mutmut 3.6.0, 128 mutants) showed the published equations were pinned only by inequality assertions: 29 survivors, including `u * (1 - U)` -> `u / (1 - U)`, `exp(-t/tau)` -> `exp(-t*tau)` and `round(x, 6)` -> `round(x, 7)`. Eight exact-value tests kill 26; the remaining 3 are equivalent mutants, documented with their rationale in the test module's docstring. Suite 6376 -> 6388; badge and doc claims regenerated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u --- CHANGELOG.md | 1 + docs/module-inventory.md | 3 +- mcp_server/core/synaptic_plasticity.py | 255 ++---------------- .../core/synaptic_plasticity_hebbian.py | 4 +- .../core/synaptic_plasticity_stochastic.py | 14 +- mcp_server/core/synaptic_plasticity_stp.py | 229 ++++++++++++++++ tests_py/core/test_import_isolation.py | 59 ++++ tests_py/core/test_stochastic_transmission.py | 161 ++++++++++- 8 files changed, 489 insertions(+), 237 deletions(-) create mode 100644 mcp_server/core/synaptic_plasticity_stp.py create mode 100644 tests_py/core/test_import_isolation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 083aa1e5..2fc7df3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ adheres to [Semantic Versioning](https://semver.org/). ### Fixed - **`json_native.to_json_native`'s own committed mutation scope had 14 surviving mutants** (#250) — a third of `[tool.mutmut]`'s demonstrated example, the module written to guarantee the 2026-06-23 PG/SQLite `structuredContent` contract, had no test pinning it. 9 of the 14 were a real gap: the `tolist()`-failure debug log (`type(obj)`/`exc` args, and the format string itself) was never asserted, so a dropped arg, a swapped arg, or a reworded message all survived — `tests_py/shared/test_json_native.py::TestTolistFailureLogging` now forces the `tolist()` exception path and asserts the exact format string plus both args via `caplog`, killing all 9. The remaining 5 are **documented equivalents, not gaps**: `bytes.decode("utf-8", …)` vs `"UTF-8"` (codec lookup is case-insensitive — verified `codecs.lookup("utf-8") is codecs.lookup("UTF-8")`) and `typing.cast("SupportsFloat", obj)`'s type-hint string, which `inspect.getsource(typing.cast)` shows is `return val` — never read at runtime, so no test can ever observe a change to it. Both rationales are written at the use site (§12.1). Boy-scout: the function was already 48 lines against this repo's own 40-line convention before this change (and my initial fix pushed it to 60); split into `_decode_bytes`/`_coerce_number`/`_tolist_fallback` plus the dispatcher, each independently under 20 lines, with the existing 14 tests passing byte-for-byte unchanged as the behavior-preservation proof. Scoped mutation run: 43 → 51 mutants (the split creates more mutation sites), **0 surviving non-equivalent mutants**, same 5 equivalents renumbered under the new helpers. +- **The synaptic-plasticity modules could not be imported directly** (#233). `synaptic_plasticity.py` held the Tsodyks-Markram implementation AND back-imported its two siblings at the bottom of the file (behind `# noqa: E402`), while `synaptic_plasticity_hebbian.py` and `synaptic_plasticity_stochastic.py` imported names back out of it — a cycle. Whichever of the three was imported FIRST in a fresh interpreter decided whether the import worked: `python -c "import mcp_server.core.synaptic_plasticity_hebbian"` raised `ImportError: cannot import name 'apply_hebbian_update' from partially initialized module`, and `_stochastic` raised the same for `apply_stochastic_hebbian_update`. Fixed by extracting the implementation down into a new leaf, **`synaptic_plasticity_stp.py`** (Tsodyks-Markram state and dynamics, noise injection, theta-phase gating), whose only imports are `math`, `random` and `dataclasses`; the two siblings now depend on the leaf, and `synaptic_plasticity.py` becomes a pure re-export facade with the same 14-name `__all__`. The moved code is **byte-identical** to the lines it came from — no constant, equation or `# source:` comment was touched — so behaviour is unchanged (160 pre-existing tests across the plasticity, ablation-hook and consolidation-handler suites pass untouched). Both `# noqa: E402` markers are gone with the cycle that required them. **The suite was green throughout the whole time this was broken**, because pytest imports the facade first and every later import is a `sys.modules` cache hit — so the regression test (`tests_py/core/test_import_isolation.py`) runs each module in a **separate interpreter**; an in-process import cannot reproduce the class. On the pre-fix tree it fails 3 of 4. Mutation testing on the relocated code found the equations were pinned only by inequality assertions (29 of 128 mutants survived, e.g. `u * (1 - U)` → `u / (1 - U)`, `exp(-t/tau)` → `exp(-t*tau)`, `round(·, 6)` → `round(·, 7)`); 8 exact-value tests now kill 26 of them, and the 3 that remain are equivalent mutants documented with their rationale at the top of `test_stochastic_transmission.py`. Suite grows 6376 → 6388. - **`update_concept` interpolated arbitrary dict keys into its SQL `SET` clause** — the one string-built-SQL site whose identifiers did NOT flow through an in-code allowlist (`docs/ASSURANCE-CASE.md` §5). Its single caller (`wiki_emerge`) passes literal keys, so no injection was reachable today, but the boundary itself enforced nothing: pre-fix, an injection-shaped key like `"label = 'x', status"` reached the SQL verbatim. Unknown keys are now REFUSED (`ValueError` before any SQL is built) against the `_UPDATABLE_COLUMNS` allowlist — the same refuse-not-escape mechanism as `wiki_view_executor._TABLE_WHITELIST` — and a DDL-drift guard test pins the allowlist to the `wiki.concepts` schema. Surfaced by the #197 family-4 S608 sweep. Tests: `tests_py/infrastructure/test_pg_store_wiki_concepts_allowlist.py`. - **`narrative.extract_events` appended a spurious `"..."` when header-stripping shrank a memory below the snippet cap.** The ellipsis gate compared the RAW content length while the truncation applied to the CLEANED text, so an auto-captured memory whose stripped `# Tool:` header pushed the raw length over 150 chars was labelled truncated with nothing cut (`extract_decisions` was already correct). Latent bug surfaced by the #197 family-3 constant extraction, which made the two sites' asymmetry visible. Regression test: `tests_py/core/test_narrative.py::TestExtractEvents::test_no_spurious_ellipsis_when_cleaning_shrinks_below_cap`. - **The doc-claim gate's own vacuity guard could be held open by a number that was never a claim.** `TEST_CLAIM` matches any `N tests` phrase in a scanned file, so an incidental count — "12 tests skipped locally that CI runs", a true and dated measurement — was read as an advertisement of the suite size. That line is not yet on `main`: it arrives with #231, whose `Test (Python 3.12)` leg fails today on exactly `CONTRIBUTING.md:36: advertises 12 tests` while its `Lint` leg passes, because the static gate skips the test-count family when no `--test-count` is given. This change is therefore the build-first half of that pair — it lands the mechanism, and #231 declares the marker on its own line. Two defects followed, and the second is the dangerous one: the false positive *counted as a match*, so `check_counts`' vacuity guard (the thing that turns a reworded or deleted claim into a build failure rather than an unnoticed loss of coverage) stayed silent. Probed on the pre-fix code: a tree whose only `N tests` text was that incidental line returned a mismatch, not the vacuity message — every real `6268 tests` claim could have been deleted and the guard would not have fired. A line whose number counts something else now declares `[not-a-count-claim: