Skip to content

fix(core): remove pathlib from abstention_gate and codebase_graph (#560, 1/3) - #564

Merged
cdeust merged 1 commit into
mainfrom
fix/560-core-no-os-ablation
Sep 15, 2026
Merged

cdeust merged 1 commit into
mainfrom
fix/560-core-no-os-ablation

Conversation

@cdeust

@cdeust cdeust commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Part of #560: 17 modules under mcp_server/core/ import os or pathlib, which the layer rule forbids (core is zero-I/O). This PR fixes 2 of the 17; two more PRs follow for the remaining 15 (env-flag/ablation cluster, wiki filesystem cluster).

  • mcp_server/core/codebase_graph.py used PurePosixPath purely for string manipulation (path normalization, dirname), with no filesystem access. Replaced with local pure-string helpers (_posix_normalize/_posix_parent) whose equivalence to PurePosixPath is pinned by a new test. Zero blast radius: no caller signature changed.
  • mcp_server/core/abstention_gate.py used Path.home()/".cache"/... to locate and load an optional ML model file, genuine filesystem I/O. Moved that read into a new mcp_server/infrastructure/abstention_classifier.py (load_abstention_classifier), and core now accepts an injectable classifier_factory callable (None by default, matching the historical "package not installed" fallback). This module has no production caller today (verified repo-wide grep; only its own tests exercise it, despite ADR-0097 documenting intent), so there is no composition root to wire yet; whoever wires it into retrieval later passes classifier_factory=load_abstention_classifier from the handler.

Pruned exactly the 3 now-stale layer-violation baseline entries for these two files. Left the other pre-existing, unrelated baseline entries for these files untouched (method-size, unsourced-constant, networkx layer-violation).

Test plan

  • python scripts/check_craftsmanship.py --base origin/main returns OK
  • grep -nE "^\s*(import os|from os|import pathlib|from pathlib)" mcp_server/core/abstention_gate.py mcp_server/core/codebase_graph.py returns empty
  • ruff check . and ruff format --check . are clean
  • pyright mcp_server/ (per CONTRIBUTING.md's typecheck recipe) reports 0 errors
  • Full pytest -q with --extra dev --extra sqlite --extra postgresql: all tests pass including test_pg_driver_passes_in_dev_env (that test needs the postgresql extra installed; it is unrelated to this change)
  • New tests: TestPosixPathHelpers (equivalence to PurePosixPath), test_no_classifier_factory_disables_filtering and test_classifier_factory_is_invoked_and_memoized (proves the injection seam and memoization), tests_py/infrastructure/test_abstention_classifier.py (real absence of the optional dependency, and an unexpected-load-error path)

🤖 Generated with Claude Code

core/ must be zero-I/O and may not import os/pathlib
(docs/module-inventory.md, Dependency Rules table), but both modules did:

- codebase_graph.py used PurePosixPath purely for string manipulation
  (path normalization and dirname) with no filesystem access. Replaced
  with local pure-string helpers (_posix_normalize/_posix_parent) whose
  equivalence to PurePosixPath is pinned by a new test
  (TestPosixPathHelpers). Zero blast radius: no caller signature changed.

- abstention_gate.py used Path.home()/".cache"/... to locate and load an
  optional ML model file, which is genuine filesystem I/O. Moved that
  read into a new mcp_server/infrastructure/abstention_classifier.py
  (load_abstention_classifier), and core now accepts an injectable
  classifier_factory callable (None by default, matching the historical
  "package not installed" fallback). This module has no production caller
  today (verified repo-wide grep; only its own tests exercise it, despite
  ADR-0097 documenting intent) so there is no composition root to wire yet;
  whoever wires it into retrieval later passes
  classifier_factory=load_abstention_classifier from the handler.

Pruned exactly the 3 now-stale layer-violation baseline entries for
these two files (pathlib x2, cortex_beam_abstain x1); left the other
pre-existing, unrelated baseline entries for these files untouched
(method-size, unsourced-constant, networkx layer-violation; none of
these reproduce differently after this change).

Part of #560 (17 core/ modules import os/pathlib); 15 remain, tracked
for follow-up PRs (ablation.py's is_mechanism_disabled alone fans out to
about 30 call sites and needs its own dedicated pass).

Verified: scripts/check_craftsmanship.py --base origin/main returns OK;
grep for os/pathlib in the two touched files returns empty; ruff
check/format are clean; pyright mcp_server/ reports 0 errors; full
pytest reports 8118 passed, 290 skipped, 1 pre-existing failure
(test_pg_driver_passes_in_dev_env, reproduces identically on unmodified
origin/main under this same --extra dev --extra sqlite sync since no
postgresql extra is installed, per the contract's prescribed env;
unrelated to this change).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@cdeust
cdeust force-pushed the fix/560-core-no-os-ablation branch from 9c29a2a to 514d106 Compare September 15, 2026 03:33
@cdeust

cdeust commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

Reviewed at 514d106, rebased on 1f3c7ff.

  • codebase_graph.py drops pathlib. Its two pure-string path helpers are pinned equal to PurePosixPath on 13 cases, including ., / and .. (TestPosixPathHelpers).
  • abstention_gate.py drops pathlib. The model lookup, the optional import and the download move to infrastructure/abstention_classifier.py, injected as a factory. Without a factory, filtering is disabled. That matches the module's existing ADR-0097 contract for an absent optional package, so it is not a new silent default. git grep on 1f3c7ff finds no caller of abstention_gate in mcp_server/ or benchmarks/, so no production path changes behaviour.
  • Three os/pathlib baseline entries are pruned. CI is green on all 24 required checks at this head.

@cdeust
cdeust merged commit 427b923 into main Sep 15, 2026
28 checks passed
@cdeust
cdeust deleted the fix/560-core-no-os-ablation branch September 15, 2026 03:57
cdeust added a commit that referenced this pull request Sep 15, 2026
… modules

Third and final PR of the multi-PR #560 fix: wiki_coverage.py (1330
lines, the largest of the 6 wiki-filesystem-cluster modules) had 5
functions doing real filesystem I/O (os.stat, os.listdir, os.walk,
open+read). New mcp_server/infrastructure/wiki_coverage_fs.py holds all
of it; core now goes through one configure_wiki_coverage_filesystem(
stat_page=, markdown_file_sizes=, domain_kind_membership=,
walk_source_files=, walk_markdown_contents=) injection point, wired by
mcp_server/__main__.py and tests_py/_composition_root_wiring.py (the
same idiom every other module in this multi-PR fix uses).

- _has_substantive_anchor: os.stat -> injected stat_page(wiki_root,
  rel_path) -> (size, mtime) | None.
- _count_substantive_pages: isdir+listdir+getsize -> injected
  markdown_file_sizes(dom_path) -> {filename: size}.
- list_domains: isdir+listdir membership counting across known-kind
  directories -> injected domain_kind_membership(wiki_root, known_kinds)
  -> {candidate_domain: [kind, ...]}; the plausibility filter and
  >=2-kinds threshold stay in core as pure logic over the returned dict.
- list_source_files: os.walk+splitext+relpath -> injected
  walk_source_files(root, skip_directories, source_extensions), core's
  own frozensets passed in as plain data (no I/O in the constants
  themselves).
- _index_wiki_file_references: os.walk+open+read every .md file ->
  injected walk_markdown_contents(wiki_root) returning the raw text of
  every page; the regex token-extraction stays in core (pure).
- Two bare os.path.basename() calls (pure string ops, not part of the
  walk) replaced with token.rsplit("/", 1)[-1].
- _SKIP_DIRECTORIES stopped duplicating the frozenset already extracted
  to mcp_server/shared/wiki_skip_directories.py for
  infrastructure/wiki_drift_fs.py's identical need (DRY boy-scout fix;
  that shared module exists specifically because infrastructure/ may not
  import core/, so the pure data had to move to shared/ for both infra
  modules to reuse).

Also fixes a pyright regression introduced by wiki_axis_registry.py's
fix in this same branch: configure_default_wiki_root's provider type
changed from Path | str | None to str | None (core cannot reference
Path even in a type hint without importing pathlib), so __main__.py's
own configure_default_wiki_root(lambda: WIKI_ROOT) needed to become
lambda: str(WIKI_ROOT) (WIKI_ROOT is a Path from
infrastructure/config.py) -- caught by pyright, not by any test.

Seam-fragility audit (per the #568/PR2 CI failure where a bare CI
preload script bypassed every composition root and hit an unconfigured
seam): grepped scripts/, .github/, benchmarks/, and mcp_server/hooks/
for every wiki_drift/wiki_axis_registry/wiki_coverage_dashboard/
wiki_coverage function and configure_* name introduced in this PR and
the prior wiki-cluster commit -- zero hits outside mcp_server/__main__.py,
tests_py/_composition_root_wiring.py, and handlers/. Unlike the reranker
seam, these four wiki seams have no bare-script exposure today, so their
"raise RuntimeError if unconfigured" design (correct here: a wiki audit
silently returning "no drift found" when actually misconfigured is a
worse failure mode than a loud crash, unlike an env-var read where
"unconfigured = false" is a safe default) is not a live landmine. Also
re-audited PR #564's abstention_gate.py (classifier_factory already
degrades gracefully to no-filtering when unconfigured, confirmed safe)
and codebase_graph.py (no I/O seam at all, pure string ops only).

Fixes #560 (all 17 of the audited core/ modules that imported os or
pathlib now have zero such imports; part 1/3 was PR #564, part 2/3 was
PR #568).

Verified: scripts/check_craftsmanship.py --base origin/main returns OK;
grep for os/pathlib in this branch's 6 wiki modules returns empty; ruff
check/format are clean; pyright mcp_server/ reports 0 errors; `python
scripts/verify_mcp_hosts.py -- hypermnesia-mcp` passes on all 6
client/profile combinations; full pytest in progress at commit time (see
hand-back notes for the confirmed count).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
cdeust added a commit that referenced this pull request Sep 15, 2026
…569)

* wip(core): remove os/pathlib from wiki_groomer/file_doc_skeleton/drift/axis_registry/coverage_dashboard (#560)

Checkpoint commit, not yet pushed: 5 of the wiki-cluster's 6 modules done
(wiki_coverage.py remains). Same injection-seam idiom as the ablation/
env-flag PR (#568), but this checkpoint has NOT yet been audited for the
"unconfigured raises" ordering-contract problem the orchestrator flagged
on that PR — every provider here needs the same audit before this is
pushed for review.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix(core): remove os/pathlib from wiki_coverage.py, last of #560's 17 modules

Third and final PR of the multi-PR #560 fix: wiki_coverage.py (1330
lines, the largest of the 6 wiki-filesystem-cluster modules) had 5
functions doing real filesystem I/O (os.stat, os.listdir, os.walk,
open+read). New mcp_server/infrastructure/wiki_coverage_fs.py holds all
of it; core now goes through one configure_wiki_coverage_filesystem(
stat_page=, markdown_file_sizes=, domain_kind_membership=,
walk_source_files=, walk_markdown_contents=) injection point, wired by
mcp_server/__main__.py and tests_py/_composition_root_wiring.py (the
same idiom every other module in this multi-PR fix uses).

- _has_substantive_anchor: os.stat -> injected stat_page(wiki_root,
  rel_path) -> (size, mtime) | None.
- _count_substantive_pages: isdir+listdir+getsize -> injected
  markdown_file_sizes(dom_path) -> {filename: size}.
- list_domains: isdir+listdir membership counting across known-kind
  directories -> injected domain_kind_membership(wiki_root, known_kinds)
  -> {candidate_domain: [kind, ...]}; the plausibility filter and
  >=2-kinds threshold stay in core as pure logic over the returned dict.
- list_source_files: os.walk+splitext+relpath -> injected
  walk_source_files(root, skip_directories, source_extensions), core's
  own frozensets passed in as plain data (no I/O in the constants
  themselves).
- _index_wiki_file_references: os.walk+open+read every .md file ->
  injected walk_markdown_contents(wiki_root) returning the raw text of
  every page; the regex token-extraction stays in core (pure).
- Two bare os.path.basename() calls (pure string ops, not part of the
  walk) replaced with token.rsplit("/", 1)[-1].
- _SKIP_DIRECTORIES stopped duplicating the frozenset already extracted
  to mcp_server/shared/wiki_skip_directories.py for
  infrastructure/wiki_drift_fs.py's identical need (DRY boy-scout fix;
  that shared module exists specifically because infrastructure/ may not
  import core/, so the pure data had to move to shared/ for both infra
  modules to reuse).

Also fixes a pyright regression introduced by wiki_axis_registry.py's
fix in this same branch: configure_default_wiki_root's provider type
changed from Path | str | None to str | None (core cannot reference
Path even in a type hint without importing pathlib), so __main__.py's
own configure_default_wiki_root(lambda: WIKI_ROOT) needed to become
lambda: str(WIKI_ROOT) (WIKI_ROOT is a Path from
infrastructure/config.py) -- caught by pyright, not by any test.

Seam-fragility audit (per the #568/PR2 CI failure where a bare CI
preload script bypassed every composition root and hit an unconfigured
seam): grepped scripts/, .github/, benchmarks/, and mcp_server/hooks/
for every wiki_drift/wiki_axis_registry/wiki_coverage_dashboard/
wiki_coverage function and configure_* name introduced in this PR and
the prior wiki-cluster commit -- zero hits outside mcp_server/__main__.py,
tests_py/_composition_root_wiring.py, and handlers/. Unlike the reranker
seam, these four wiki seams have no bare-script exposure today, so their
"raise RuntimeError if unconfigured" design (correct here: a wiki audit
silently returning "no drift found" when actually misconfigured is a
worse failure mode than a loud crash, unlike an env-var read where
"unconfigured = false" is a safe default) is not a live landmine. Also
re-audited PR #564's abstention_gate.py (classifier_factory already
degrades gracefully to no-filtering when unconfigured, confirmed safe)
and codebase_graph.py (no I/O seam at all, pure string ops only).

Fixes #560 (all 17 of the audited core/ modules that imported os or
pathlib now have zero such imports; part 1/3 was PR #564, part 2/3 was
PR #568).

Verified: scripts/check_craftsmanship.py --base origin/main returns OK;
grep for os/pathlib in this branch's 6 wiki modules returns empty; ruff
check/format are clean; pyright mcp_server/ reports 0 errors; `python
scripts/verify_mcp_hosts.py -- hypermnesia-mcp` passes on all 6
client/profile combinations; full pytest in progress at commit time (see
hand-back notes for the confirmed count).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix(core): wiki_axis_registry get_registry raises on an unwired seam

Addresses the reviewer's explicit "apply the same no-silent-default rule
to #569, for example a wiki-root seam that falls back to a default
path" note on PR #568's review.

get_registry() used to collapse "configure_default_wiki_root was never
called" and "the configured provider legitimately returned None" into
the same wiki_root=None -> defaults-only behavior. The first case is a
missing composition root, not a valid answer, and silently degrading to
built-in defaults would hide any wiki-relative custom axis taxonomy a
user configured via _schema/ files (CLAUDE.md: no silent fallbacks).
Now: an explicitly-configured provider that returns None still yields
defaults-only (unchanged, legitimate); a provider that was never
configured at all raises RuntimeError.

Wired tests_py/_composition_root_wiring.py's
configure_default_wiki_root(lambda: None) explicitly, matching the
test session's pre-existing implicit behavior (conftest.py isolates
tests from any real wiki data) but now stated rather than silently
defaulted. All 78 tests across test_wiki_axis_registry.py,
test_wiki_classifier.py, test_ble001_sweep_core.py, and
test_craftsmanship_imports.py pass unchanged.

Also fixed a malformed `source: ADR-0297, issue #560` citation in
wiki_coverage_fs.py's docstring (decision-integrity gate parses the
first token after "source:" as a single decision ID; a trailing comma
made it unparseable) -- same class of typo already fixed elsewhere in
this PR, missed on this file.

Verified: scripts/check_craftsmanship.py --base origin/main returns OK;
pyright mcp_server/ reports 0 errors; full pytest reports 8447 passed,
165 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix(core): one composition root wires every #560 seam at every entry point

Review of #569 found the wiki filesystem seams wired only in __main__ and
the test conftest while they raise when unwired: the SessionStart-spawned
hooks/consolidate_background.py runs consolidate with wiki=True in its own
process, so every autonomous consolidation lost its dashboards (the
sub-task error was recorded nested in stats and the cycle still reported
status=ok). scripts/regenerate_file_docs.py had the same gap.

- mcp_server/composition_root.py: wire_composition_root(), idempotent,
  wires the wiki root, the schema reader and the coverage, drift and
  dashboard filesystems in one place.
- Called by __main__, scripts/launcher.py, every hooks/*.py __main__
  block, every script and benchmark runner that reaches core.
- tests_py/scripts/test_composition_root_wiring_coverage.py enumerates
  the entry points mechanically (plugin.json hook commands, hooks with a
  __main__ block, scripts and benchmarks importing core/handlers) and
  requires an actual wire_composition_root() call in each.
- tests_py/hooks/test_consolidate_background_wiki_composition_root.py
  runs the hook as a real subprocess on a throwaway SQLite store without
  the test conftest. Measured: unwired, the hook logs
  "dashboards=error: RuntimeError: ... not configured"; wired, it logs
  "dashboards=written=0". The hook's wiki summary line now reports the
  dashboard outcome instead of defaulting to "ok".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* fix(core): wiring lives in hooks/, the one layer every entry point may import

CI's craftsmanship gate refused hooks/* importing the top-level
mcp_server.composition_root: the layer table lets hooks/ import core,
infrastructure and shared only. hooks/ is also the only layer that may
itself import both core and infrastructure and be imported by every
entry point (the top-level __main__, scripts and benchmarks are not
layer-checked), so the single wiring function moves to
mcp_server/hooks/wiring.py.

- post_tool_capture wires in _dispatch_with_store_cleanup, after the
  capture-mode gate, instead of inside process_event: excluded events
  still import nothing from core or infrastructure (W3-1b), and
  process_event stays within the 40-line cap.
- Single-line imports keep post_commit_reindex.py under 300 lines.
- The coverage test matches a real top-level main block, not the phrase
  quoted in a docstring.
- Measured cost of the wiring import in a hook process: 24-35 ms
  cumulative (python -X importtime, 3 runs, this machine).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
cdeust added a commit that referenced this pull request Sep 15, 2026
…g modules (#560, 3/3)

Last of the three #560 PRs (after #564 and #569): ablation.py,
conflict_monitor.py, pg_recall_weights.py, recall_pipeline.py,
reconsolidation.py, reranker_model.py, retrieval_dispatch.py,
telemetry.py and thermodynamics.py no longer import os or pathlib.

- core/environment.py: read_environment_variable/read_environment_float
  read through an injected reader, evaluated on every call, so runtime
  toggles (CORTEX_ABLATE_*, CORTEX_DECAY_DISABLED, CORTEX_*_BETA) still
  take effect after import. An unconfigured reader raises: returning
  "unset" silently would make every ablation run measure the un-ablated
  system (benchmarks set CORTEX_ABLATE_* without importing __main__).
- core/reranker_model.py: the FlashRank cache lookup, existence check and
  sha256 move to infrastructure/reranker_cache.py behind a seam that
  raises when unwired.
- Both seams join the wiki seams in mcp_server.hooks.wiring
  .wire_composition_root(), the single function every entry point calls.
  It now re-applies its bindings on every call instead of short-circuiting
  after the first, so a seam reset by a test is restored. The per-hook
  copies, benchmarks/lib/_environment_wiring.py, _reranker_wiring.py and
  the import-time wiring in hooks/_telemetry.py are gone;
  benchmarks/lib/_composition_root_wiring.py calls the same function.
- doctor.ensure_reranker_ready() wires through that function; CI's
  reranker preload step calls it instead of reaching into core directly.
- tests_py/benchmarks/test_ablation_runner_wiring.py drives the
  ablation-runner path with a CORTEX_ABLATE_* flag and proves the
  mechanism is disabled.

Fixes #560

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
cdeust added a commit that referenced this pull request Sep 15, 2026
…g modules (#560, 2/3) (#568)

* fix(core): remove os/pathlib from ablation.py and 8 retrieval env-flag modules (#560, 3/3)

Last of the three #560 PRs (after #564 and #569): ablation.py,
conflict_monitor.py, pg_recall_weights.py, recall_pipeline.py,
reconsolidation.py, reranker_model.py, retrieval_dispatch.py,
telemetry.py and thermodynamics.py no longer import os or pathlib.

- core/environment.py: read_environment_variable/read_environment_float
  read through an injected reader, evaluated on every call, so runtime
  toggles (CORTEX_ABLATE_*, CORTEX_DECAY_DISABLED, CORTEX_*_BETA) still
  take effect after import. An unconfigured reader raises: returning
  "unset" silently would make every ablation run measure the un-ablated
  system (benchmarks set CORTEX_ABLATE_* without importing __main__).
- core/reranker_model.py: the FlashRank cache lookup, existence check and
  sha256 move to infrastructure/reranker_cache.py behind a seam that
  raises when unwired.
- Both seams join the wiki seams in mcp_server.hooks.wiring
  .wire_composition_root(), the single function every entry point calls.
  It now re-applies its bindings on every call instead of short-circuiting
  after the first, so a seam reset by a test is restored. The per-hook
  copies, benchmarks/lib/_environment_wiring.py, _reranker_wiring.py and
  the import-time wiring in hooks/_telemetry.py are gone;
  benchmarks/lib/_composition_root_wiring.py calls the same function.
- doctor.ensure_reranker_ready() wires through that function; CI's
  reranker preload step calls it instead of reaching into core directly.
- tests_py/benchmarks/test_ablation_runner_wiring.py drives the
  ablation-runner path with a CORTEX_ABLATE_* flag and proves the
  mechanism is disabled.

Fixes #560

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* test(ablation): the runner wiring test needs psycopg, like the runner (#560)

benchmarks/lib/ablation_runner.py imports db_snapshot, which imports
psycopg; the SQLite-only CI job has no postgresql extra and failed at
import. The PostgreSQL test jobs keep running it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
@cdeust cdeust mentioned this pull request Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant