Repository navigation
fix(core): remove pathlib from abstention_gate and codebase_graph (#560, 1/3) - #564
Merged
Merged
Conversation
This was referenced Sep 15, 2026
Merged
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
force-pushed
the
fix/560-core-no-os-ablation
branch
from
September 15, 2026 03:33
9c29a2a to
514d106
Compare
Owner
Author
|
ZETETIC-REVIEW: APPROVE Reviewed at 514d106, rebased on 1f3c7ff.
|
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]>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part of #560: 17 modules under
mcp_server/core/importosorpathlib, 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.pyusedPurePosixPathpurely for string manipulation (path normalization, dirname), with no filesystem access. Replaced with local pure-string helpers (_posix_normalize/_posix_parent) whose equivalence toPurePosixPathis pinned by a new test. Zero blast radius: no caller signature changed.mcp_server/core/abstention_gate.pyusedPath.home()/".cache"/...to locate and load an optional ML model file, genuine filesystem I/O. Moved that read into a newmcp_server/infrastructure/abstention_classifier.py(load_abstention_classifier), and core now accepts an injectableclassifier_factorycallable (Noneby 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 passesclassifier_factory=load_abstention_classifierfrom the handler.Pruned exactly the 3 now-stale
layer-violationbaseline 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/mainreturns OKgrep -nE "^\s*(import os|from os|import pathlib|from pathlib)" mcp_server/core/abstention_gate.py mcp_server/core/codebase_graph.pyreturns emptyruff check .andruff format --check .are cleanpyright mcp_server/(per CONTRIBUTING.md's typecheck recipe) reports 0 errorspytest -qwith--extra dev --extra sqlite --extra postgresql: all tests pass includingtest_pg_driver_passes_in_dev_env(that test needs the postgresql extra installed; it is unrelated to this change)TestPosixPathHelpers(equivalence toPurePosixPath),test_no_classifier_factory_disables_filteringandtest_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