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
|
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