A refused package is invisible on the MCP surface: archive_refused reaches no coverage_reasons code, so an agent's answer is qualified by nothing #124
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#124
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?
Found while retiring the declarative tier (#76). Pre-existing, not introduced by that change — but the retirement makes it reachable on upgrade for the first time, which is why it is worth filing now.
The gap
eligibility::requested_not_installed()filters forpackage_requested_not_installed, andsignature_refusals()for the signature family. Neither covers everything onPackageSet::refused().So an
archive_refusedfromStore::get— or any other refusal on that list — produces nocoverage_reasonscode at all.plugin doctor,plugin statusand the daemon log all show it. An agent readingproject_overvieworindex_coveragegets an answer qualified by nothing.It is true of
digest_mismatchtoday too, so the class is wider than the one trigger that surfaced it.Why the retirement makes it matter now
Every release through v0.26.1 admitted a declarative package. On upgrade, such a package is now refused at
Store::getwitharchive_refused— correctly, and with the tier's own witness in the CLI. But a project that had one enabled will, through MCP, simply stop having those files' symbols, with nothing in any tool reply explaining why.That is the exact shape this project keeps closing elsewhere: a fact the system knows, disclosed on one surface and silent on its neighbours. #101 and #99 closed it for truncated extractions and structural zeros; #114 closed a README claim that outran its analysis. This is the package-refusal instance, and it is arguably worse, because the files simply disappear from answers rather than returning a qualified zero.
Shape of the fix
Cover the whole of
PackageSet::refused(), not one more member of it. Two filters exist because two reasons were handled; a third filter would be the third instance of the same omission. The honest form is: every refusal on that list maps to a coverage code, and a refusal with no mapping fails the build — the inversionreason_code_registry.rsalready performs for the reason-code vocabulary.Three-state discipline applies as usual: absent means this daemon did not report,
[]is a measurement that nothing was refused, non-empty is the finding.Worth noting
[]only recently became reachable at all — the declarative retirement removed the constant that madecoverage_reasonsnon-empty on every project, so the "empty is a measurement" sentence describes a real state for the first time since it was written.Test
A project with an installed package that
Store::getrefuses, driven through the MCP surface, asserting the reply names the refusal. Mutation: drop the new mapping and the reply must go back to being silent — red on a payload where files really did lose their symbols, not on a hand-built struct.Related
#101, #99 (the same asymmetry on other facts), #107 (resources bypassing the grader), #115 (a package refusal silently promoted — the write side of the same blind spot). Found by the #76 declarative-tier retirement.
index_coveragereports "Indexed and current" for a file whose facts were refused by the validator #136Response::Unclaimedis the same silent-loss door as #115, still open: three causes collapse into one count that reads as the designed case #123Triage 2026-09-06: LEFT OPEN — the headline defect is fixed, but this issue names the wrong list, and the list it names is still uncovered.
Reported as fixed, and the fix is good. I am leaving it open because of a distinction the fixing commit itself makes.
What is fixed — completely
archive_refusedanddigest_mismatchridePackageSet::unapproved(), and that channel is now covered in full, with no filter:crates/indexer/src/coverage.rs:307—pub const PACKAGE_REFUSAL_COVERAGE_REASONS: &[&str] = crate::approval::APPROVAL_REASONS;— the constant itself, and the doc at:275says "not a copy of it". A 22nd approval reason becomes a coverage code the day it is declared. There is no third filter and no registry of exceptions to keep true, which is exactly what this issue demanded.SignatureRefusals(u8, 4 codes) becamePackageRefusals(u32, the family) — set type at:335,observeat:348.crates/mcp-server/src/eligibility.rs:169-172—package_refusals: Option<PackageRefusals>,None= did not report,Some(empty)= measured none, with explicit skew handling in both directions at:713-725.coverage.rs:1399— "the family outgrew PackageRefusals' u32; widen the field before appending".And it is graded through MCP against a real store, as asked:
That log line is from
Store::get— a real refusal, not a hand-built struct. The test title also carries the right second property: not called permanent.Why it stays open
73ef473states it plainly, and it is a correction to this issue rather than to the fix:So: this issue's fix shape says "cover the whole of
PackageSet::refused(), not one more member of it", andPackageSet::refused()—crates/indexer/src/packages.rs:778, returning&[RefusedPackage]— is a different list from the one that was covered. It is still silent on the MCP surface.The examples in the body are fixed. The rule in the body is not. Closing on the examples would retire the rule at the moment it is half-applied — and the rule is the part this issue was actually written to establish, since a third filter for a third list is precisely the failure it names.
What closing needs
Either:
refused()/load_one'sRefusedPackageunder a coverage mapping the same wayunapproved()now is — one relation, no third filter; orrefused()cannot reach an agent-visible answer (if that is true), and record it on theinstalled_but_abi_rejectedrow where the note already lives, atcrates/indexer/src/coverage.rs:612.Option 2 is legitimate and may well be the right answer — but it has not been argued, and "still reaches no coverage code" is not the same as "cannot".
🤖 Triage lane, 2026-09-06, master
45cf6e4code-index://docs/reason-codesis at 3,979 of its 4,000-token cap, so the next reason code this project mints cannot be documented #184GUEST_ABI_MAJORis bracketed by no manifest field, andplugin pack --check-reproducibledoes not exist #153FIXED — the second list is covered, by the same mechanism and with no third filter. Commit
378f7f6onlane/pkg-124-refused-coverage.The previous triage was right to leave this open and right about why: "this issue's fix shape says cover the whole of
PackageSet::refused(), and that is a DIFFERENT list from the one that was covered." It offered two ways to close.Option 2 was checked first and it is FALSE.
refused()really can reach an agent-visible answer:load_onerefuses a package the operator installed AND approved, whose bytes the store handed back — so the extensions it claims have no producer and its files lose their symbols, while every reply stays silent. That is not "cannot reach"; it is "did reach, unqualified". So option 1.The mechanism, and why it is one relation rather than a second channel
PackageRefusals' vocabulary is now the union of the two closed families, one per list, both DERIVED and neither enumerated:both concatenated in a
const fn. A twenty-second approval reason and a fifty-fourthReasonare each a coverage code the day they are declared.Reason::as_strbecameconstfor exactly this — the alternative was a hand-written list, which is the defect this issue was filed about.No new wire field: the refusals ride the same
package_refusalskey, because they answer the same question an agent asked.daemon::eligibility::package_refusalsnow sweeps both lists and filters neither."A refusal with no mapping fails the build" is a property of the TYPE.
observe_reasontakes acode_index_abi::Reason, not a string, so the mapping cannot miss — a caller cannot hand over a string that falls outside the family because it cannot hand over a string at all. A runtime check would have been the wrong shape: it fires on the refusals a fixture happens to produce, and every refusal never produced under a test stays unmapped and silent, which is this issue exactly.Graded through MCP, against a real store
coverage_signature_e2e::a_package_the_host_refused_at_load_is_reported_and_is_not_called_permanent. Two packages installed and approved by the operator; the second'sextractor.wasmis not a WebAssembly module,--precompilerefuses it,load_oneputs it onrefused()withReason::ExtractorMalformed. Nothing hand-built.It is stronger than the sibling above it in one deliberate way: the refused package is the only claimant of
.lockx, sodata.lockxis code the project asked to have indexed and does not have — which is what lets this test reach the #140 permanence branch the sibling cannot (the sibling's path is claimed by a package that IS running). A healthy package stays active throughout, so a build that refused everything fails the anti-vacuity arm rather than passing.The reply the mutation prints is this issue verbatim:
Mutations — five, all run, all RED
for r in host.set().refused()sweep — the pre-change state, restored exactlyan approved package this build refused reaches an agent as silence: []--release, with thedebug_assert!compiled out, fails on the test's own assertionleft: [] right: ["abi.version_unsupported"]index_coverage'sif fixable { … } elseSEMANTICSthe load half names no member at allMutation 2 in both profiles is the one worth calling out: the guard behind
observe_reasonis adebug_assert!, and this repository does not let a profile-dependent check carry a test.Two existing gates caught the widening. Both were MOVED, not weakened
the_bitset_records_only_the_family_and_keeps_its_orderusedfact.span_out_of_rangeas its negative control — a code from a different closed vocabulary. That vocabulary is now half the family. A control naming a member of the family it controls for must move, so it is nowplugin_state_unavailable, a host-minted Coverage code, withonly_a_package_refusal_contradicts_permanenceasserting the same separation from the other side.the_semantics_names_every_code_the_field_can_carrynow anchors both families to a real member instead of one.The #140 permanence clause was re-derived rather than assumed: a
Reasonon this channel names a package the operator installed, so removing, replacing or rebuilding it changes the verdict — the widened family still contradicts a permanence claim for every member, and the gate asserts it over the whole widened set.The pointer that this fix made false, fixed in the same change
EXTENSION_STATES_NOT_DERIVED'sinstalled_but_abi_rejectedrow said "That list is NOT the onecoverage_reasonsreads." True when written, false the moment this landed. It now carrieselsewhere: Some("plugin_activation.coverage_reasons")and says which half is still not derived (the per-EXTENSION row, which really would need the manifest this build just refused).#140's pointer gate is what keeps it honest.code-index://docs/reason-codesdocuments the two families.One thing that broke, and the gate that caught it is the story
Making
Reason::as_strconstbrokereason_code_registry: it locates the method by a source-text needle spellingpub fn, at three sites, so all three stopped matching at once and every parser in that file returned the empty set.the_populations_are_not_emptycaught it by its floor —— which is an anti-vacuity check doing exactly its job, on a one-word change in another crate. The needle now starts at
fnand is one constant instead of three literals.All FOUR
PackageSetlists now reach an agentpackages(),unapproved()(#124 first pass),refused()(this),duplicate_package_ids()(#91, viapackage_duplicate_ids). That is the closure argument this issue asked for: not "one more member", but every list.Gates
cargo fmt --all -- --check0 ·cargo clippy --workspace --all-targets -- -D warnings0 ·RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items0 ·coverage_signature_e2e3/3 ·index_coverage_e2e11/11 ·reason_code_registry12/12 ·disclosure_derivation_registry11/11 ·symbol_blind_coverage_e2e5/5 ·coverageunit tests 24/24.cargo test --workspace --no-fail-fast: 310 suites, 1 failure —overview_payload_budget_e2e, measured PRE-EXISTING and filed as #197. Master's exact semantics wording reads 4054 tokens against the 4050 ceiling; this change reads 4052, so it is 2 tokens better and still red. Not blessed, not papered over, and not trimmed out of somebody else's field to get a green run.No protected record moved:
baseline.json,stage-baseline.json,tier3-baseline.json,ruby-package-cost.jsonandtests/bench/oracle/*are untouched, and nothing undertests/packages/changed — this is a read-side change with no coupled package landing behind it.🤖 Packaged-language lane, 2026-09-06, master
4f866e5, worktree/tmp/cosi-lane-pkgFIXED and merged as
e5775ae(lane commit378f7f6).The fix takes the shape this issue asked for, not a third filter.
PackageRefusals' vocabulary is now the derived union of both closed families (APPROVAL_REASONS ++ Reason::ALLin aconst fn), it sweeps both lists, and "an unmapped refusal fails the build" became a property of the type:observe_reasontakes aReason, not a string. All fourPackageSetlists now reach MCP.The cheaper hypothesis was checked first and refuted:
refused()really does reach an agent, so the disclosure was genuinely absent rather than already covered by a neighbouring path.Five mutations run, all RED — including one that had to be run in both profiles, because the guard behind it is a
debug_assert!and would have been vacuous in release. Two existing gates caught the widening and were moved rather than weakened.Closing.