Stop a working tool reporting "no issues" from a validator that never ran (#410) - #487
Conversation
… ran (#410) #461 already documented the five scripts that import `communitymech.literature_enhanced` - a module that has never existed in any commit. What it did not reach is that four WORKING scripts still pointed at one of them, and one of those actually invoked it. `batch_snippet_fixer.validate_file` shelled out to poetry run python scripts/curate_evidence_with_pdfs.py --file X --quick Three things were wrong at once, and together they were silent: * that script cannot start - the import fails; * `poetry` is not this repo's runner, it uses uv; * cwd was `yaml_path.parent.parent`, i.e. `kb/`, not the repo root. `returncode` was never checked. Reproduced against a real record before changing anything: the subprocess exits **2**, stdout and stderr carry no `ERROR: N` for the regex to match, and the function returns `{"total": 0, "errors": 0, "warnings": 0}` - which its caller reads as *validated, clean*. A validation step that reports success because it never ran is worse than one that is missing, and `0` could not be distinguished from "could not tell". It now calls `linkml-reference-validator`, the tool behind `just validate-references`, which #466 established does genuinely check snippets against references_cache/. Returncodes other than 0/1, and a failure with no parseable issue count, both return -1 rather than rounding down to clean. Canaried on three inputs: a clean record reports 0, a record with a planted snippet that is in no publication reports 1, and a file that cannot be read reports -1. The three other scripts printed it as the next step - `apply_suggested_fixes`, `apply_suggested_snippets`, `intelligent_snippet_fixer` - all now name `just validate-references`. A dead pointer inside a working tool is how a curator discovers the breakage, which is the worst place to discover it. The new test that pins this caught its own module docstring first: the prose explaining the historical `poetry run ...` command matched a raw-text scan for live pointers. Same trap as grepping a curation note for the id it retired (#471). It walks the AST now and looks only at strings passed to print or subprocess. Not decided here: whether the five dead scripts are deleted or ported. #410 asks that and it remains open - the porting question is real (their `fetch_paper(ref, download_pdf=...)` differs from the `fetch_paper(reference, email=...)` that exists, which returns a tuple rather than a subscriptable dict, and `LiteratureFetcher` has no PDF *download*, only `fetch_unpaywall` returning a URL). What is fixed is that nothing working depends on them any more, which is what made the breakage reachable. 2319 passed, 16 skipped. ruff, black, mypy src/ clean.
…s reach (#410) Two findings, both correct, both undercutting the first version. **The -1 sentinel was corrupted by its only caller.** validate_file returns -1 for "could not validate", and `process_files_batch` did `initial["total"] - final["total"]` on it. So a file whose post-processing validation failed to run printed ✅ AMD_Acidophile_Heterotroph_Network.yaml Issues: 0 → -1 (fixed 1) 🎉 Total issues fixed across all files: 1 - a green tick, a fabricated fix, and that fabricated 1 summed into the batch total. I had moved the never-ran-but-looks-clean defect one frame up rather than removing it. There is now a `validation_failed` status with its own⚠️ icon, no arithmetic on the sentinel, unverified files excluded from the total and named explicitly rather than left to be inferred from a smaller number. Verified with the reviewer's exact reproduction: no "fixed 1", no green tick. **validate_file was unreachable from the CLI.** `--no-validate` existed with no `--validate` beside it, and `parser.set_defaults(validate=False)` pinned it False whatever was passed - since 7c658e6, the same commit that added the broken subprocess. So my PR body's "the part that was actually reachable" was wrong: no invocation could reach it. Worse, the genuinely reachable bug was the flag itself, advertising a choice the parser did not offer. `--validate` now exists as a mutually exclusive pair with `--no-validate`, default still off, with the cost stated in the help text (~1.3s per call, twice per file, ~13 minutes over 312 files). Verified: [] and --no-validate give False, --validate gives True. Also from the review: * the AST guard named 3 of the 5 dead scripts and caught 2 call shapes. It now reads `_KNOWN_BROKEN` from tests/test_scripts_import.py rather than keeping a second copy that drifts, and covers os.system, sys.stdout.write and logger calls. All five shapes the review found missing are now caught. * `@pytest.mark.slow` is not a registered marker here, so it warned and deselected nothing. The discriminating test is `e2e`, which addopts already deselects - it runs linkml-reference-validator, which #417 keeps out of qc, and its "no network" property holds only because this record's references happen to be cached. * the docstring explained the `Total checks` line while the regex parses `Issues found`; and `warnings: 0` claimed a split the tool does not report. Not fixed, recorded in the code: a well-formed YAML of the wrong shape still validates clean, which is the same defect class one layer down inside the validator. 2318 passed, 16 skipped, 8 deselected. ruff, black, mypy src/ clean.
Review round 1 — addressedBoth HIGH findings landed, and both undercut the first version rather than refining it. 1 (HIGH) — the
|
…) (#522) * Remove the five scripts that never ran, and guard the cause (#410) They imported `communitymech.literature_enhanced`, a module absent from all 498 commits — they fail before `--help`. #487 already fixed the one working tool that invoked one; what was left was the decision the tests recorded as open: port them, or drop them. Dropped. Porting was never an import swap. Their CLI flags advertise a 6-tier PDF cascade with "fallback mirrors" and `LiteratureFetcher` has no PDF surface, so a port meant *building* that — retrieving publisher PDFs through mirrors is not something to add speculatively. The need underneath it is open-access full text, and `scripts/cache_fulltext.py` serves it: the #183 sweep used it to cache full text for 64 of 125 references. docs/pdf_fetching_capability.md now maps each removed script to what to use instead. The more useful change is to the guard. `_KNOWN_BROKEN` was five names and three tests keeping the list honest — that records breakage, it does not prevent it, and a sixth script importing a sixth phantom module would just have been added to it. Every `from communitymech.X import ...` in scripts/ is now resolved against the installed package, so the next one fails at the moment it is written. The list stays, empty, because the removed names are still dead pointers for any working tool that prints them. Two things caught while doing it, both by tests already here: emptying the constant made it `_KNOWN_BROKEN: set[str] = set()`, an AnnAssign, which the sibling test's Assign-only AST walk stopped finding — it went red rather than passing on an empty set. And the >= 5 bound now rests on the removed names, or it would have started passing on nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Point the docs at what replaced the removed scripts (#523) Three docs and .gitignore said "NOT FUNCTIONAL ... tracked in #410". True until this PR; now stale in a new way, because the files are gone — a reader looks for a script that is not there and cannot tell whether it was deleted or they are on the wrong branch. They now say REMOVED and name the replacement. The gap that let this sit: nothing walked docs/ for script references. #410's guard and the one replacing it both check scripts/ — print and subprocess calls in Python files. A curator following a runbook is reading prose, which is exactly where neither looks. The new test checks existence, not tone, and that is a correction of my own first attempt. I filed #523 claiming AUTOMATION_TOOLS.md gave a bare unwarned instruction; it did not — the warning sat two lines above the command, and I had grepped for the script name and read only the line it matched. Third time this session a line-scoped scan has missed the prose that negates the hit. Whether a reference is adequately caveated is a judgement; whether the file exists is a fact, and only the fact belongs in a test. pdf_fetching_capability.md is exempt by name: it carries a document-level banner saying everything below it describes software that was never here, which a per-line check cannot see, and rewriting it would destroy the record it exists to keep. Mutation-checked: appending a reference to a nonexistent script reddens it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ments guard, ambiguous issue refs Addresses the remaining findings from this PR's review (the gate failure it also reported — test_a_script_imports[deep_research_provider.py] under "Python 3.14" — does not reproduce: verified directly with the real Python 3.14.6 interpreter installed on this machine, both in isolation and via `uv run --python 3.14 pytest`, both pass. CI itself pins Python 3.10 (.github/workflows/validate-strict.yaml). The fix from this PR's first commit — registering the probe module in sys.modules before exec_module — already covers this; the review's static-gate step likely ran against a stale checkout, a known hazard documented for this fleet's review tooling): - Medium: stage capabilities keys weren't validated against the known capability set, unlike provider_adjustments. A typo'd capability key silently contributed 0 to every provider's score instead of erroring. - Medium: bare "#487"/"#412" review-attribution comments collided with this repo's own unrelated PR numbers (CommunityMech#487 and #412 are both real, different PRs) — qualified as "proteintraitsmech#487" and "mediaingredientmech#412". - Low: `high = max(raw.values()) or 1.0` only guarded an exact-zero max; a large negative provider_adjustments value pushes every score negative, leaving `high` negative too, clamping every fit to 0 and collapsing the ranking to alphabetical order. Guards the sign now, not just falsiness. Reproduced live with all-negative adjustments — fixed ranking keeps a real relative order instead of degenerating. High finding (missing KNOWN_BLOCKED provider check + not registered for vendored-sync) is real but out of scope for this PR — it requires porting CultureMech's evolved implementation and/or deciding the fleet's sharing model, which is what CultureMech#287/#290/#298 already track. Filed CommunityMech#658 to carry it forward rather than expanding this PR indefinitely. Verified: full `uv run pytest tests/` — 2540 passed, 75 skipped, 8 deselected, 0 failed. `black --check`/`ruff check`/`mypy src/` clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…search (#657) * Add deep-research provider triage for ecological mechanism/dataset research Ports the fleet's generic deep-research provider-scoring implementation to CommunityMech, completing work the justfile already assumed: `deep-research-providers`/`deep-research-provider` recipes referenced scripts/deep_research_provider.py and conf/deep_research_provider.yaml, but neither file was ever committed, so both recipes were broken on main. research_community.py (the entity runner) was already in place and untouched. Two domain-specific focuses in conf/deep_research_provider.yaml: - ecological_mechanism — exact composition, directional interaction evidence between community members. - datasets_environment — repository-native accessions, ENVO context, cultivation and perturbation metadata. Bumps deep-research-client to 0.2.10, matching the version pinned across CultureMech/TraitMech/MediaIngredientMech/ProteinTraitsMech. Carries two fixes from review rounds on the sibling ports (proteintraitsmech#487, MediaIngredientMech#412): - provider_adjustments keys in the YAML profile are canonicalized and validated against known providers at config-load time, so a typo'd or aliased key no longer silently no-ops instead of applying its intended score bonus. - `--json --provider X` recomputes recommended_available/ fallback_available from the filtered ranking instead of leaving stale values computed from the unfiltered one. Also fixes a latent bug in tests/test_scripts_import.py (#410's import sweep, unrelated to deep-research otherwise): its subprocess probe built a module via importlib.util.module_from_spec() but never registered it in sys.modules before exec_module(). Any script defining a `from __future__ import annotations` frozen dataclass crashes there — dataclasses._is_type does `sys.modules.get(cls.__module__).__dict__` while checking for ClassVar/InitVar, and cls.__module__ ('_probe') was never actually stored in sys.modules, so .get() returns None. The real invocation path (`python scripts/foo.py`) doesn't hit this: running as __main__ registers sys.modules['__main__'] for free. Reproduced in isolation and via mutation test (reverting the one-line fix turns the probe red again on deep_research_provider.py specifically). Verified: full `uv run pytest tests/` — 2523 passed, 89 skipped, 8 deselected (0 failed). `black --check src/ tests/ scripts/`, `ruff check src/ tests/ scripts/`, `mypy src/` — all clean, matching .github/workflows/lint.yaml exactly. `uv sync --frozen --all-extras` — consistent. `scripts/check_vendored_sync.sh` — OK, unaffected. Mutation-tested provider_adjustments and --json filtering fixes independently; both turn their new tests red when reverted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix findings ported from MediaIngredientMech#412's round-2 review Same generic file, same two gaps found there, fixed identically here before CommunityMech's own review round even landed: - Medium: provider_adjustments canonicalization didn't check for two raw keys resolving to the same provider (e.g. edison: 3 and falcon: 5 in the same focus) — the second silently overwrote the first. Now raises a clear ValueError. - Medium: main()'s CLI-level rejection of an unknown --provider/--focus argument had no test coverage. Verified: mutation test on the duplicate-key guard turns the new test red. Full `uv run pytest tests/` — 2540 passed (+3 from the new tests minus the earlier no-longer-relevant skip count changes), 75 skipped, 8 deselected, 0 failed. `black --check` / `ruff check` / `mypy src/` all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix round-1 review findings: capabilities validation, negative-adjustments guard, ambiguous issue refs Addresses the remaining findings from this PR's review (the gate failure it also reported — test_a_script_imports[deep_research_provider.py] under "Python 3.14" — does not reproduce: verified directly with the real Python 3.14.6 interpreter installed on this machine, both in isolation and via `uv run --python 3.14 pytest`, both pass. CI itself pins Python 3.10 (.github/workflows/validate-strict.yaml). The fix from this PR's first commit — registering the probe module in sys.modules before exec_module — already covers this; the review's static-gate step likely ran against a stale checkout, a known hazard documented for this fleet's review tooling): - Medium: stage capabilities keys weren't validated against the known capability set, unlike provider_adjustments. A typo'd capability key silently contributed 0 to every provider's score instead of erroring. - Medium: bare "#487"/"#412" review-attribution comments collided with this repo's own unrelated PR numbers (CommunityMech#487 and #412 are both real, different PRs) — qualified as "proteintraitsmech#487" and "mediaingredientmech#412". - Low: `high = max(raw.values()) or 1.0` only guarded an exact-zero max; a large negative provider_adjustments value pushes every score negative, leaving `high` negative too, clamping every fit to 0 and collapsing the ranking to alphabetical order. Guards the sign now, not just falsiness. Reproduced live with all-negative adjustments — fixed ranking keeps a real relative order instead of degenerating. High finding (missing KNOWN_BLOCKED provider check + not registered for vendored-sync) is real but out of scope for this PR — it requires porting CultureMech's evolved implementation and/or deciding the fleet's sharing model, which is what CultureMech#287/#290/#298 already track. Filed CommunityMech#658 to carry it forward rather than expanding this PR indefinitely. Verified: full `uv run pytest tests/` — 2540 passed, 75 skipped, 8 deselected, 0 failed. `black --check`/`ruff check`/`mypy src/` clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Partially addresses #410. The delete-or-port decision stays open; what this fixes is the part that was actually reachable — and silent.
The live defect
#461 already documented the five scripts importing
communitymech.literature_enhanced, a module that has never existed in any commit. What it did not reach: four working scripts still pointed at one of them, and one actually invoked it.batch_snippet_fixer.validate_fileran:Three things wrong at once, and together they were silent:
poetryis not this repo's runner (it usesuv);cwdwasyaml_path.parent.parent, i.e.kb/, not the repo root.returncodewas never checked. Reproduced against a real record before changing anything:The subprocess failed, no
ERROR: Nappeared for the regex to match, and the function returned zeros.0could not be distinguished from "could not tell". A validation step that reports success because it never ran is worse than one that is simply missing — this is the defect class this repo keeps surfacing, and here it was sitting inside a tool that looks like it works.The fix
validate_filenow callslinkml-reference-validator— the tool behindjust validate-references, which #466 established does genuinely check snippets againstreferences_cache/. Returncodes other than 0/1, and a non-zero exit with no parseable issue count, both return-1rather than rounding down to clean.Canaried on three inputs:
{'total': 0}{'total': 1}— it actually detects now{'total': -1}— "could not validate", not cleanThe three scripts that merely printed it as the next step —
apply_suggested_fixes,apply_suggested_snippets,intelligent_snippet_fixer— now namejust validate-references. A dead pointer inside a working tool is how a curator discovers the breakage, which is the worst place to discover it.The test caught its own docstring first
The guard against dead pointers scanned raw text, so it flagged the line in
batch_snippet_fixer.pythat explains the historicalpoetry run ...command. Exactly the trap from #471, where a grep for a retired CHEBI id matched the curation note documenting its retirement. It walks the AST now and inspects only strings passed toprintorsubprocess.Mutation-checked: restoring the round-down-to-clean branch fails
test_a_file_that_cannot_be_validated_is_not_reported_as_clean.What is deliberately not decided
Whether the five dead scripts are deleted or ported. The porting question is real, not bookkeeping:
fetch_paper(ref, download_pdf=...)and subscript the result (paper["abstract"]);fetch_paper(reference, email=...)returning a tuple(abstract, pdf_url);LiteratureFetcherhas no PDF download at all — onlyfetch_unpaywall(doi), which returns a URL. Two of the scripts advertise full-text extraction (snippet_in_fulltext), and one advertises a sci-hub fallback in its docstring.Porting means writing capability that no working code in this repo has, guided only by drafts that never ran.
scripts/cache_fulltext.pyalready covers "get OA full text into the cache" in a working form. My reading is that deletion is right, but it is a judgement about intent that belongs with whoever wrote them, so #410 stays open for it.What is fixed is that nothing working depends on them any more, which is what made the breakage reachable.
2319 passed, 16 skipped.ruff,black,mypy src/clean.🤖 Generated with Claude Code