A truncated extraction is disclosed only by index_coverage — every other tool serves the partial file silently #101
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#101
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reported from the field against
de.h-dv.xaml 0.1.0on a real 2 406-file XAML solution, and verified in the source.What the customer measured
They found it, correctly diagnosed it, and had to keep the knowledge by hand.
Verified
extraction_diagnosticsis a field of theindex_coveragereply and nothing else. Incrates/mcp-server/src/server.rsit is declared inside that tool's scope and populated fromoverlap.extraction_diagnostics; every other construction of that struct passesNone. No other tool reads it.So for a file that reports
extract.truncated_tree:index_coverageindexedfile and you must use shell for the rest of it"search_symbols,file_outline,find_callers,find_references,read_code,change_impact,context_pack,safe_delete,check_renameWhy this outranks the neighbouring disclosure gaps
This is not an unmeasured zero (#99). Rows that should exist do not exist, and the file is otherwise a perfectly ordinary
indexedrow. Consequences that follow directly:ref_counton any symbol in a truncated file is wrong, not merely a floor — references past the cut were never extracted.find_callersreturns fewer callers than exist, and itsconfidenceblock says only the standard "unresolved, external, and dynamic uses may be absent" — which is true of every file and does not describe this one.safe_deleteandcheck_renameare the dangerous pair. Their whole contract is "never say safe on incomplete evidence". A truncated file is incomplete evidence by construction, and neither tool can currently see that it is looking at one.safe_deletesaying a symbol has no remaining uses, computed over a file whose second half was never read, is the failure mode those tools exist to prevent.The project already has the vocabulary for this.
index_coverage's own description states the rule —nullis NOT REPORTED,[]is a measurement, non-empty means symbols are missing — and it is applied on one surface out of a dozen.Same shape as #99, one level worse
#99 is
find_callersreturning a structural zero with no disclosure whilesearch_symbolsexplains the same fact. This is that pattern with missing data instead of an unmeasurable count. Both are "a fact the system knows, disclosed on one surface and silent on its neighbours", and both should probably be fixed by the same mechanism rather than one at a time — the #97 fix covered all 23 tools by construction for exactly this reason.What the fix needs to be
Any tool whose answer is derived from a file with a non-empty
extraction_diagnosticsshould say so, in the reply, per result — not in a tool description, which nobody reads at the moment they need it (see the standing rule that disclosures belong in the payload).Sketch, reusing the existing vocabulary rather than a second one:
Absent when no source was truncated, present when one was — so a reply without the field keeps meaning "nothing behind this was partial".
safe_deleteandcheck_renameshould go further than disclosing. Given their never-say-safe contract, a truncated file among the evidence should downgrade the verdict rather than annotate it.Also worth checking
project_overview'sfile_healthblock lists per-file ref counts and does not appear to carry truncation either — a file that is silently half-indexed looks like a file with genuinely few refs.Reported alongside
Two other findings from the same session, both already handled:
no_use_reference_channelon 30 479 XAML controls — the disclosure working correctly, the customer read it right — andplugin_add's poll instruction naming a condition that could never become true, now corrected and guarded.Fixed, together with #99, by one mechanism rather than a fix per tool.
annotate_evidence_gaps— a single post-dispatch graderIt sits in
CodeIndexServer::call_toolabove the router, exactly where the #97 argcheck ladder sits. That placement is the whole point: tool 24 is covered without opting in, and nobody has to remember to annotate a new tool. It parses the finished reply, grades it, and rewrites it.Two graders feed one
evidence_gapsblock:IndexAccess::partial_sources(), deliberately with no path argument: the file that corrupts an answer is the one the answer cannot name, so asking "is this path partial?" would miss exactly the case that matters. Backed byread_partial_sourcesand a new partial index (m0058_partial_source_index.rs, schema 57→58 — measured 20 ms → below timer resolution on a synthetic 200 000-contribution index; the probe runs once per tool call).SymbolRow::name_fallback_unmeasuredoffget_symbol, which is the same field from the same producersearch_symbolsalready publishes, so the two tools cannot drift into disagreeing about one symbol.Matching is by exact whole-string equality against every string in the payload, so
pathfields, bareVec<String>lists liketext_occurrence_files, and any shape added later are covered without the grader knowing a single field name.Coverage is measured, not asserted: the sweep drives 18 tools against a truncated index and against a clean one.
The three states are kept apart
partial_sources_unmeasured: ["primary"]— renders "could not report", never "none"partial_sources+partial_sources_in_indexpartial_sources_in_indexisCOUNT(DISTINCT path), neversources.len(), and is absent when no project answered. The unmeasured state was verified live against this repo's running v0.26.1 daemon.safe_delete/check_renameno longer merely annotateWhen
partial_sources_in_index > 0or the probe is unmeasured,no_evidence_of_use/cleanare removed fromreasonsandevidence_incompleteleads; what was taken out is named inevidence_gaps.downgraded. Written client-side, so an older daemon's reply is downgraded too and no reason-code value crosses the wire.This was narrowed once, on evidence. The first version also downgraded on #99's fact, and the pre-existing
refactor_tools_disclose_a_saturated_fts_candidate_windowwent red onWidget— an ordinary struct — becauseno_use_reference_channelfires wherever the index demonstrates no channel for a (kind, lang) pair, which on a small repo is most kinds. A guard that fires on everything discloses nothing. So #99 gets disclosure beside the verdict; #101 gets the downgrade.Mutations — all RUN, all RED
call_toolis_silent()→ always falsesearch_symbolsinvented an evidence gap on a clean index"downgrade_absence_verdict→Noneprobe_unmeasured_population→ emptyleft: None / right: Some(2)on a body that is verbatim #99's bug reportWHEREtotal = sources.len()left: Some(500) / right: Some(501)Both directions everywhere: a clean index emits no block, and a function nothing calls still reports a bare
total: 0.Two gates I wrote were themselves wrong first, and both notes are in the test docs: the call-site gate was brittle to
rustfmtline-breaking, and the 501-file cap fixture originally INSERTed rows — the server reconciles away paths with no file on disk, so it measured 501 in SQL while the server reported 1.The open question answered:
project_overview.file_healthcarried nothingFileResolutionispath/refs/resolved/no_candidate/internal_missed/unresolved_qualified/unresolved_bare— no truncation field. Demonstrated: a truncated file reports{"path":"src/other.rs","refs":3,"resolved":3,"internal_missed":0}, a perfectly healthy-looking row. The new mechanism names that exact path inpartial_sourcesin the same reply.Scope, stated rather than implied
parse_with_pluginreturnsParse::Ok(e, Vec::new())under the comment "NO DIAGNOSTIC CHANNEL EXISTS ON THIS ARM"; the only non-empty producer ispackages::facts_diagnostics, reachable solely fromTaskSource::Package. A builtin extractor cannot produce a truncated tree — but "all parsers become plugins" (#75/#84) makes every user a package user, and this repo's own index already carries one.read_resourceserves the same data ungraded. Deliberate: both issues are about tool reports. Worth a follow-up.read_codegets the block too. Its bytes are unaffected by truncation; the uniform rule is the price of "no per-tool allowlist", and the wording was softened accordingly.Cost: ~0.06–0.11 ms per call (0.252/0.256 → 0.314/0.364 ms, isolated release, 300 calls).
Gates: fmt, clippy native and
x86_64-pc-windows-gnu, rustdoc,cargo test --workspace2849 passed / 0 failed, daemon-leg mcp-server 555 passed / 0 failed (proving the block crosses the real RPC wire), corpus ratchet unmoved at534084b856c22566c48e386bc41ed67e,precision_gate7/7.evidence_gapsgrader, so the same data is served graded through tools and ungraded through resources #107change_impactreturns an empty, confident answer wherefind_callersfinds 5 call sites — and nothing in its payload says why #122Response::Unclaimedis the same silent-loss door as #115, still open: three causes collapse into one count that reads as the designed case #123change_impactreturns an empty, confident answer wherefind_callersfinds 5 call sites — and nothing in its payload says why #122archive_refusedreaches nocoverage_reasonscode, so an agent's answer is qualified by nothing #124index_coveragereports "Indexed and current" for a file whose facts were refused by the validator #136response_format: "concise"dropsinfluenceandresolved_by, so the dynamic-influence disclosure is invisible in the cheapest format we ship #139evidence_gaps.partial_sources_in_index: 1fires on EVERY reply whilepartial_sourcesis never populated — a three-state disclosure that only ever renders its unfalsifiable state #200