#75's searchable-but-inert default is unreachable through the generation build when one package claims all of a project's code — gate 6 cannot tell "granted nothing, deliberately" from "a grant that failed" #206

Closed
opened 2026-09-07 11:32:06 +02:00 by buildagent · 1 comment
Member

Found while building #45's package-lifecycle ladder, which had to drive the real activation path rather than a fixture. Measured, not inferred.

The fact

build::build's gate 6 refuses an activation whose "pending generation's pools admit nothing". admitted counts contributions in any resolver pool — and a package granted no capabilities is in no pool, by construction.

So on a project all of whose code a single package claims, the searchable-but-inert state cannot be reached through a generation build. The build refuses before there is anything to serve.

The XAML reference package is the sharper case: it requests bridge_source only and no pool capability at all, so a project consisting of nothing but .xaml could never activate it.

Why this is a defect and not a policy

#75's design has searchable-but-inert as the default and the safe first step — install, enable with no capabilities, prove existing bindings unchanged, and only then grant. #80's C3 conformance layer is built on it: "index the probe corpus with (1) no package; (2) package active but searchable-only; …" and asserts step 1 equals step 2 for every pre-existing target.

Gate 6 is a good gate — an activation that admits nothing is usually a grant that silently failed to reach the pools, and refusing it is right. The problem is that it cannot tell that case from the deliberate one. Two different states, one rendering, which is the shape this project closes everywhere else.

Why the existing tests do not see it

Every package fixture in the tree grants at least one pool capability, or runs on a project the builtins also claim — so there is always something in a pool and gate 6 never fires on the inert rung. #45's ladder is the first thing to drive absent → inert → active → rollback → removed on a project where the package is the only producer, which is why it surfaced here and not earlier.

Shape of a fix

The gate needs to distinguish the two, not to be relaxed. The information exists at the call site: whether the activation requested any pool capability at all. An activation that requested none and admitted none is the documented inert state; an activation that requested some and admitted none is the failure gate 6 exists for, and must keep refusing.

What must NOT be done

  • Do not delete or weaken gate 6. A grant that fails to reach the pools is a real failure and this is the only thing that catches it.
  • Do not special-case the XAML package. It is the sharpest instance, not the class — any package that claims an extension no builtin claims hits this.
  • Do not fix it by granting the inert rung a token capability. That makes the test pass by not testing the state, and inert-means-inert is the safety property.
  • Do not close it by asserting the CLI path works. plugin enable writes the activation record; the generation build is the path that refuses. Whatever fix lands needs the ladder in package_baseline.rs to exercise it.

#75 (searchable-by-default is its design), #80 C3 (which is built on the inert step), #45 (where this surfaced), and the sibling finding filed alongside about an activation build deferring a newly-claimed extension with no files row.

Reported by the #45 lane, 2026-09-07, measured on the real activation path.

Found while building #45's package-lifecycle ladder, which had to drive the real activation path rather than a fixture. Measured, not inferred. ## The fact `build::build`'s gate 6 refuses an activation whose *"pending generation's pools admit nothing"*. `admitted` counts contributions in any resolver pool — and **a package granted no capabilities is in no pool, by construction.** So on a project all of whose code a single package claims, the searchable-but-inert state cannot be reached through a generation build. The build refuses before there is anything to serve. The XAML reference package is the sharper case: it requests `bridge_source` only and **no pool capability at all**, so a project consisting of nothing but `.xaml` could never activate it. ## Why this is a defect and not a policy #75's design has *searchable-but-inert* as the **default** and the safe first step — install, enable with no capabilities, prove existing bindings unchanged, and only then grant. #80's C3 conformance layer is built on it: *"index the probe corpus with (1) no package; (2) package active but searchable-only; …"* and asserts step 1 equals step 2 for every pre-existing target. Gate 6 is a good gate — an activation that admits nothing is usually a grant that silently failed to reach the pools, and refusing it is right. **The problem is that it cannot tell that case from the deliberate one.** Two different states, one rendering, which is the shape this project closes everywhere else. ## Why the existing tests do not see it Every package fixture in the tree grants at least one pool capability, or runs on a project the builtins also claim — so there is always something in a pool and gate 6 never fires on the inert rung. #45's ladder is the first thing to drive `absent → inert → active → rollback → removed` on a project where the package is the only producer, which is why it surfaced here and not earlier. ## Shape of a fix The gate needs to distinguish the two, not to be relaxed. The information exists at the call site: whether the activation **requested** any pool capability at all. An activation that requested none and admitted none is the documented inert state; an activation that requested some and admitted none is the failure gate 6 exists for, and must keep refusing. ## What must NOT be done - **Do not delete or weaken gate 6.** A grant that fails to reach the pools is a real failure and this is the only thing that catches it. - **Do not special-case the XAML package.** It is the sharpest instance, not the class — any package that claims an extension no builtin claims hits this. - **Do not fix it by granting the inert rung a token capability.** That makes the test pass by not testing the state, and inert-means-inert is the safety property. - **Do not close it by asserting the CLI path works.** `plugin enable` writes the activation record; the *generation build* is the path that refuses. Whatever fix lands needs the ladder in `package_baseline.rs` to exercise it. ## Related #75 (searchable-by-default is its design), #80 C3 (which is built on the inert step), #45 (where this surfaced), and the sibling finding filed alongside about an activation build deferring a newly-claimed extension with no `files` row. Reported by the #45 lane, 2026-09-07, measured on the real activation path.
Author
Member

Fixed on master at 4b332ff (merged e425320). Two corrections to this issue first — one half was already refuted by the tree.

The XAML paragraph is refuted, and package_baseline.rs already said so

This issue argues the xaml package "requests bridge_source only and no pool capability at all". Measured: capabilities::POOL_CAPABILITIES is RESOLVER_CAPABILITIES and includes bridge_source, and admit_select joins any capability. package_baseline.rs:1185-1198 had already retired that half as #233's cause.

The inert half stands, and that is what was fixed.

The prescribed rule is slightly too strong

This issue says: "an activation that requested some and admitted none is the failure gate 6 exists for, and must keep refusing."

That would refuse a granted package whose files were all deferred — which is #207's Ruby shape, and self-healing. The fix measures the expectation over the projection rather than over the request, which handles both.

What shipped

build::gate's sixth arm now measures the expectation beside the outcome. admissible_projection counts how much of the pending projection belongs to a producer this activation gave authority to — read from generation_packages (the activation record) rather than from component_capabilities (the projection under test). That distinction is the whole design: an expectation read from the projection could only ever agree with pool_admit.

  • expected > 0 && admitted == 0 → still refuses.
  • expected == 0 → promotes and discloses on Ready::admits_nothing_by_construction, which plugin enable prints.

first_activation_admits::an_ungranted_package_still_fails_the_gate became …activates_inert_and_the_generation_says_so, with a false-arm control beside it.

A third state fell out of the same clause that neither this issue nor #207 described: a granted package whose files are all deferred yields contributions = 0 → expected = 0 → inert, not refused. It needed no case of its own.

Mutations

  • M6 (delete the whole if admitted == 0 arm) → RED on an_ungranted_package_activates_inert_… and nothing else: "a generation reached ready with admitted: 0 and did not say why".
  • M7 (admissible_projection returns 0) → SURVIVED across all six integration tests — the refusal branch is unreachable with the projection intact. Covered afterwards by a new row-level unit test where M7b is RED (left: 0 / right: 1). Reported as a survivor first.
  • M8 (disclose unconditionally) → RED on the false arm.
  • M9 (revert #233 alone) → RED, 4 tests, the refusal firing with its new sentence naming the count.
  • M10 (revert #233 and delete the inner refusal) → the defect passes that arm; RED lands on other tests' assertions. Reported as two-factor, not a clean kill.
  • M11/M12/M13 → RED on the reserved, inert and outgoing-grant rows respectively.

#206 and #207 did not share a root — measured

Both render "this package produced nothing" opaquely, but the mechanisms are disjoint: #207's deferral fires at gate 3 (generation_build_files.file_id IS NULL) and never reaches the admission arm; #206's inert state fires at gate 6 with deferred = 0. The new .wpfx e2e produces deferred=1 with admitted > 0, so gate 6 is not involved; first_activation_admits's fixture reaches gate 6 with nothing deferred at all.

Three tree gates caught real consequences on the way — doc_citation_gate (a renamed test still cited), ref_kind_stance_registry (moving kind = 'type' into a helper made the registry's claim unverifiable; the literal is back at both call sites, deliberately), and activation_identity::nothing_reads_the_neutralised_clock.

baseline.json unmoved; binds unmoved by construction (nothing here is a resolver input), confirmed by corpus_ratchet executed=7 unavailable=0. Closing.

Fixed on `master` at `4b332ff` (merged `e425320`). **Two corrections to this issue first — one half was already refuted by the tree.** ## The XAML paragraph is refuted, and `package_baseline.rs` already said so This issue argues the xaml package *"requests `bridge_source` only and no pool capability at all"*. Measured: `capabilities::POOL_CAPABILITIES` **is** `RESOLVER_CAPABILITIES` and **includes `bridge_source`**, and `admit_select` joins any capability. `package_baseline.rs:1185-1198` had already retired that half as #233's cause. The **inert** half stands, and that is what was fixed. ## The prescribed rule is slightly too strong This issue says: *"an activation that requested some and admitted none is the failure gate 6 exists for, and must keep refusing."* That would refuse a granted package whose files were **all deferred** — which is #207's Ruby shape, and self-healing. The fix measures the expectation over the **projection** rather than over the **request**, which handles both. ## What shipped `build::gate`'s sixth arm now measures the *expectation* beside the outcome. `admissible_projection` counts how much of the pending projection belongs to a producer this activation gave authority to — read from `generation_packages` (the **activation record**) rather than from `component_capabilities` (the projection under test). That distinction is the whole design: an expectation read from the projection could only ever agree with `pool_admit`. - `expected > 0 && admitted == 0` → still refuses. - `expected == 0` → **promotes and discloses** on `Ready::admits_nothing_by_construction`, which `plugin enable` prints. `first_activation_admits::an_ungranted_package_still_fails_the_gate` became `…activates_inert_and_the_generation_says_so`, with a false-arm control beside it. **A third state fell out of the same clause** that neither this issue nor #207 described: a granted package whose files are *all* deferred yields `contributions = 0` → `expected = 0` → inert, not refused. It needed no case of its own. ## Mutations - **M6** (delete the whole `if admitted == 0` arm) → **RED on `an_ungranted_package_activates_inert_…` and nothing else**: *"a generation reached `ready` with `admitted: 0` and did not say why"*. - **M7** (`admissible_projection` returns 0) → **SURVIVED across all six integration tests** — the refusal branch is unreachable with the projection intact. Covered afterwards by a new row-level unit test where **M7b is RED** (`left: 0 / right: 1`). Reported as a survivor first. - **M8** (disclose unconditionally) → RED on the false arm. - **M9** (revert #233 alone) → RED, 4 tests, the refusal firing with its new sentence naming the count. - **M10** (revert #233 **and** delete the inner refusal) → the defect passes that arm; RED lands on *other* tests' assertions. Reported as **two-factor, not a clean kill**. - **M11/M12/M13** → RED on the reserved, inert and outgoing-grant rows respectively. ## #206 and #207 did not share a root — measured Both render "this package produced nothing" opaquely, but the mechanisms are disjoint: #207's deferral fires at **gate 3** (`generation_build_files.file_id IS NULL`) and never reaches the admission arm; #206's inert state fires at **gate 6** with `deferred = 0`. The new `.wpfx` e2e produces `deferred=1` with `admitted > 0`, so gate 6 is not involved; `first_activation_admits`'s fixture reaches gate 6 with nothing deferred at all. Three tree gates caught real consequences on the way — `doc_citation_gate` (a renamed test still cited), `ref_kind_stance_registry` (moving `kind = 'type'` into a helper made the registry's claim unverifiable; the literal is back at both call sites, deliberately), and `activation_identity::nothing_reads_the_neutralised_clock`. `baseline.json` unmoved; binds unmoved by construction (nothing here is a resolver input), confirmed by `corpus_ratchet` `executed=7 unavailable=0`. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#206
No description provided.