v1.108.300 — Wider than reported
Wider than reported
Four defects from three issues by @rknighton, and every one turned out wider than the report that found it. That is the release; no single fix is.
from . import <sibling> built an edge to __init__.py (#550)
from . import receipts in evidence/producers.py is a dependency on evidence/receipts.py. The specifier reaching the resolver is a bare ., which names the package, and resolve_specifier never sees the imported names — so the edge to the sibling was never built. This repo uses the form 49 times across 16 files, and it alone took find_dead_code(granularity="file") from 42 dead files to 22: 20 live files were being reported dead.
Fixed at extraction rather than resolution, which is what keeps it small. A per-name specifier (.receipts) is emitted alongside the bare one, so the 26 resolve_specifier call sites keep their single-target contract. from . import x is either the submodule x or an attribute of __init__.py, and the importing file cannot say which, so both edges are offered — .x resolves to None when no such module exists, which every consumer already skips. Measured on src/: 87 sibling edges that did not exist, 30 modules made reachable, across 62 importing files.
The report named one half. seen keys on the specifier, and every bare-dot import in a file shares the same one, so a second from . import b was dropped whole — names and all.
One wrapped sentence erased a file's entire import graph
Unreported, and found because the scan measuring the fix above crashed. _PY_IMPORT matches any line starting import , docstrings included. watcher.py's docstring wraps to import keeps the core watcher free of a hard dependency on the CLI package, — the trailing comma leaves an empty final part, [0] raised IndexError, and extract_imports catches it and returns [].
That file had zero import edges in every index ever built, indistinguishable downstream from a file that imports nothing. The bogus specifier lifted out of prose was never the problem; it resolves to None. The crash was. That path now logs at WARNING naming the file, because a caller cannot tell an empty list from a genuine absence.
Failed calls recorded ok=1, so a broken tool reported a 0% error rate (#551)
_call_tool_impl tracked its outcome in a local flag initialised to True, and three of its four error exits never cleared it. Schema-validation rejections, the search_text argument guard and a front-door relay of a child's refusal all returned isError=True to the client and wrote ok=1 to tool_calls.
Every layer was truthful about itself, which is why it survived: the flag meant "did this frame hit trouble", it is read downstream as "did the request succeed", nothing in the name marks the difference, and 1,700 lines separate the write from the read.
Patching the four exits would have left the mechanism, and two more were unreachable from inside that frame anyway — a fifth exit (project-level tool disabling) has the identical shape, and _enforce_response_cap refuses after the frame's finally has already written its row. The row is now derived in call_tool from isError on the value it returns: set in one place, covering the cap and every exit added later, and unable to drift from what the client saw because it is what the client saw.
Counter order gate refusals carried no isError (#552)
Both gate refusals returned a body whose only key was error with the flag unset, so a client branching on isError — what v1.108.74 added it for — read a refusal as a success. Two reported, four found: the same shape sat in order's args-type guard, route's missing-task guard and the front door's unknown-tool fallback.
PARSER_GENERATION 3 → 4 — your next index call re-parses once
The sibling-import fix changes which import edges exist for a file whose content never changes, so an existing index will never re-read those files and the missing edges stay missing. .254 (Python package-relative import edges) made the identical argument inside gen 2.
Two bumps in two releases is the cost of the counter being hand-maintained, not a reason to skip one. Expect one full re-parse per repo on the next index_folder / index_repo, reported as rebuild_reason="parser_generation_upgrade"; jcodemunch-mcp refresh does it in bounded, resumable slices.
Guards
tests/test_call_outcome_contract.py holds #551 and #552 as properties rather than as their reported sites — _error_call_result unreachable as a direct return from _call_tool_impl, exactly one latency writer, no front-door handler returning an error body without the flag. Every predicate was run against the pre-fix tree first and every one fires.
One of our own tests went red for the reason Practice 9 exists: test_call_tool_is_the_wrapper_not_the_dispatcher asserted src.count("return") == 1, a restatement of the mechanism, and a comment containing the word "returned" broke it while the property it names still held. It counts Return nodes now.