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: