plugin enable fails on a project holding ONLY files the package claims — the package's own grant is minted while the build runs, so the gate admits nothing #233

Closed
opened 2026-09-08 22:55:03 +02:00 by buildagent · 1 comment
Member

Measured on the current binary while building the de.h-dv.timeline release asset job, and deliberately not fixed there — it is a product defect, not a workflow one.

The symptom

A project containing only files a package claims fails activation:

gate_failed — the pending generation's pools admit nothing

generation_failures confirms the detail. Reproduced on the current binary, not inferred.

The cause

A package's own extraction_components row is minted while the build runs. So:

  1. capabilities::project_generation_grants, called from build::request, writes no grant for that package — the row it would key on does not exist yet;
  2. build::gate then counts admitted over temp.pool_admit and gets 0;
  3. the gate refuses, correctly, because nothing was admitted.

The grant lands at promotion. So the condition self-heals after the first successful activation — but the first activation is the one that fails, and on a project of only claimed files there is no first successful activation to heal from.

Why nobody hit it before

The XAML packing job's scratch project happens to contain a .cs file. That one builtin-claimed file is enough to admit something, so the gate passes and the package's grant lands at promotion as usual. The bug is invisible whenever the project contains at least one file some other extractor claims.

That is luck, not design — and it means the failure is worst exactly where a package is most likely to be tried: an operator pointing a new package at a directory of only that package's file type, which is the obvious way to evaluate one.

The TimeLine job carries a two-line Loader.cs for precisely this reason, with the measurement written into the comment at .forgejo/workflows/release.yml:3300-3318. That workaround should be deleted when this is fixed, and its removal is the regression test.

What a fix has to do

Mint the package's grant so it is visible to build::request, or make build::gate admit on the pending generation's own components rather than only on grants written before the build began. Either way the invariant to restore is: a package that claims a file in the project must be able to admit it on its first activation.

What a fix must prove

  • A scratch project containing only files the package claims activates on the first attempt. This must go RED against today's code — if it passes, the fixture is not a claimed-files-only project and the test is vacuous.
  • A project with a mix still activates (no regression).
  • Anti-vacuity: a package that claims nothing in the project must still fail the gate. Without this arm, "always admit" passes.
  • Deleting the Loader.cs workaround from the TimeLine packing job leaves that job green.

Same family as several findings this week: a state reachable only through an ordering nobody designed against, hidden because every existing exercise of the path happened to contain the one extra input that masks it. See #227 for the inverse (a state believed unreachable that every first-party package traverses).

Measured on the current binary while building the `de.h-dv.timeline` release asset job, and deliberately **not** fixed there — it is a product defect, not a workflow one. ## The symptom A project containing only files a package claims fails activation: ``` gate_failed — the pending generation's pools admit nothing ``` `generation_failures` confirms the detail. Reproduced on the current binary, not inferred. ## The cause A package's own `extraction_components` row is minted **while the build runs**. So: 1. `capabilities::project_generation_grants`, called from `build::request`, writes **no grant** for that package — the row it would key on does not exist yet; 2. `build::gate` then counts `admitted` over `temp.pool_admit` and gets **0**; 3. the gate refuses, correctly, because nothing was admitted. The grant lands **at promotion**. So the condition self-heals after the first *successful* activation — but the first activation is the one that fails, and on a project of only claimed files there is no first successful activation to heal from. ## Why nobody hit it before **The XAML packing job's scratch project happens to contain a `.cs` file.** That one builtin-claimed file is enough to admit something, so the gate passes and the package's grant lands at promotion as usual. The bug is invisible whenever the project contains at least one file some *other* extractor claims. That is luck, not design — and it means the failure is worst exactly where a package is most likely to be tried: an operator pointing a new package at a directory of only that package's file type, which is the obvious way to evaluate one. The TimeLine job carries a two-line `Loader.cs` for precisely this reason, with the measurement written into the comment at `.forgejo/workflows/release.yml:3300-3318`. **That workaround should be deleted when this is fixed**, and its removal is the regression test. ## What a fix has to do Mint the package's grant so it is visible to `build::request`, or make `build::gate` admit on the pending generation's own components rather than only on grants written before the build began. Either way the invariant to restore is: *a package that claims a file in the project must be able to admit it on its first activation.* ## What a fix must prove * A scratch project containing **only** files the package claims activates on the first attempt. This must go RED against today's code — if it passes, the fixture is not a claimed-files-only project and the test is vacuous. * A project with a mix still activates (no regression). * **Anti-vacuity**: a package that claims *nothing* in the project must still fail the gate. Without this arm, "always admit" passes. * Deleting the `Loader.cs` workaround from the TimeLine packing job leaves that job green. ## Related Same family as several findings this week: a state reachable only through an ordering nobody designed against, hidden because every existing exercise of the path happened to contain the one extra input that masks it. See #227 for the inverse (a state believed unreachable that every first-party package traverses).
Author
Member

Fixed in 51d6029, on master. And this issue understated it: the "mixed project" case was never safe, only quiet.

The escalation, measured

I filed this as an evaluation-time annoyance — loud on a claimed-files-only project, invisible elsewhere because some other extractor claims a file. The second half is wrong.

Reverting the fix and running the mixed-project arm:

the mixed project admits 1 of 3 contribution(s)
Ready { generation: 2, reparsed: 2, carried: 1, contributions: 2,
        carried_contributions: 1, symbols: 4, refs: 0, admitted: 1, ... }

It reaches Ready and is promoted — with the package's own two contributions in no pool at all. Only the carried builtin file was admitted.

So on every release that has shipped packages, the first activation of any package produced a generation whose package rows resolved nothing until a later pass rewrote them. Not just the pure-claim case: every case. The pure-claim project fails loudly (gate_failed); the mixed project fails silently, and the silent one is both worse and far more common.

Re-run independently before merge, not taken from the lane's report: same mutation, same numbers, source restored by cp and verified by md5.

Cause — reproduced twice before any code was touched

  1. Cause level: panicked at generation_build.rs:1399: the activation record named a claim language and no component was minted for it, so \project_generation_grants` had nothing to project onto`
  2. Symptom level, on the shipped bytes: tests/packages/timeline packed, signed, installed, approved and activated over one Sample.dataset → gate_failed: the pending generation's pools admit nothing

extraction_components is minted by writer::write_pending_contribution while the build runs; project_generation_grants joins generation_packages to it, and so wrote the package no grant at build::request time.

The fix, and why not the other candidate

activation.rs:466-502 — in write_generation, beside the ensure_package it already performed, mint each claim language's component via contributions::component_of + ensure_component, carrying packages::write_grants' reserved-key refusal across.

The issue offered two shapes. Candidate 2 — teach build::gate to admit on pending components — would have been wrong, not merely larger. The pending grant is what the resolver reads during the build, not just what gate counts. A gate-only fix would have activated the package and left every row it produced in no pool until the next pass: the silent defect above, preserved, with the loud symptom removed. That is the worst possible outcome and it was the tempting one.

Candidate 1 is also the smaller change, because a component's identity derives from the claim languages, which only ActivationInputs carries — generation_packages does not hold them, so the projection could not mint them.

Nothing is fabricated: only the row the grant is projected onto. A package claiming nothing still fails the gate.

Mutations — all RUN, no survivors

mutation result
delete the ensure_component loop (revert the fix) 6 RED across 3 suites, including the mixed case reaching Ready with admitted: 1 of 3
drop only the is_reserved_key arm 1 RED, isolated
delete build::gate's if admitted == 0 ("always admit") 1 RED of 45 — the anti-vacuity arm: an ungranted package activated over a project of only its own files: Ready(… admitted: 0 …)
fixture control: claim a language that IS present 1 RED at the reason, proving the fixture exercises the intended gate

The Loader.cs workaround is gone

Deleted from the TimeLine packing job (release.yml:3264), replaced by a comment recording why. Its local stand-in — the_shipped_timeline_package_activates_over_only_its_own_files — runs on every CI leg and goes RED without the fix, so the removal is graded rather than merely performed.

A doc claim this retires

package_baseline.rs's Ruby-leg doc said the XAML package "requests bridge_source only and no pool capability at all, so a project of nothing but .xaml could never activate it." That was one cause presented as two: capabilities::admit_select does not filter on pool capabilities and gate counts COUNT(DISTINCT contribution_id), so a bridge_source grant that reaches the table is counted — it just never reached it. Corrected in place; the inert half now graded by an_ungranted_package_still_fails_the_gate rather than argued.

Gates, isolated: fmt, clippy (host and windows-gnu), cargo test --workspace (327 suites, 3524 passed, 0 failed), rustdoc -D warnings, corpus ratchet (6 passed in 22.37s — it really ran; baseline.json unmoved), precision gate 7/7 phantoms=0 with POPULATION lines read, and COSI_E2E_LEG=daemon (746 passed) — all exit 0.

Fixed in `51d6029`, on `master`. **And this issue understated it: the "mixed project" case was never safe, only quiet.** ## The escalation, measured I filed this as an evaluation-time annoyance — loud on a claimed-files-only project, invisible elsewhere because some other extractor claims a file. The second half is wrong. Reverting the fix and running the mixed-project arm: ``` the mixed project admits 1 of 3 contribution(s) Ready { generation: 2, reparsed: 2, carried: 1, contributions: 2, carried_contributions: 1, symbols: 4, refs: 0, admitted: 1, ... } ``` It reaches `Ready` and **is promoted** — with the package's own two contributions in **no pool at all**. Only the carried builtin file was admitted. So on every release that has shipped packages, **the first activation of any package produced a generation whose package rows resolved nothing** until a later pass rewrote them. Not just the pure-claim case: every case. The pure-claim project fails *loudly* (`gate_failed`); the mixed project fails *silently*, and the silent one is both worse and far more common. Re-run independently before merge, not taken from the lane's report: same mutation, same numbers, source restored by `cp` and verified by md5. ## Cause — reproduced twice before any code was touched 1. **Cause level:** `panicked at generation_build.rs:1399: the activation record named a claim language and no component was minted for it, so \`project_generation_grants\` had nothing to project onto` 2. **Symptom level, on the shipped bytes:** `tests/packages/timeline` packed, signed, installed, approved and activated over one `Sample.dataset` → `gate_failed: the pending generation's pools admit nothing` `extraction_components` is minted by `writer::write_pending_contribution` **while the build runs**; `project_generation_grants` joins `generation_packages` to it, and so wrote the package no grant at `build::request` time. ## The fix, and why not the other candidate `activation.rs:466-502` — in `write_generation`, beside the `ensure_package` it already performed, mint each claim language's component via `contributions::component_of` + `ensure_component`, carrying `packages::write_grants`' reserved-key refusal across. The issue offered two shapes. **Candidate 2 — teach `build::gate` to admit on pending components — would have been wrong, not merely larger.** The pending grant is what the **resolver** reads during the build, not just what `gate` counts. A gate-only fix would have activated the package and left every row it produced in no pool until the next pass: the silent defect above, preserved, with the loud symptom removed. That is the worst possible outcome and it was the tempting one. Candidate 1 is also the smaller change, because a component's identity derives from the *claim languages*, which only `ActivationInputs` carries — `generation_packages` does not hold them, so the projection could not mint them. Nothing is fabricated: only the row the grant is projected *onto*. A package claiming nothing still fails the gate. ## Mutations — all RUN, no survivors | mutation | result | |---|---| | delete the `ensure_component` loop (revert the fix) | **6 RED across 3 suites**, including the mixed case reaching `Ready` with `admitted: 1` of 3 | | drop only the `is_reserved_key` arm | 1 RED, isolated | | **delete `build::gate`'s `if admitted == 0`** ("always admit") | 1 RED of 45 — **the anti-vacuity arm**: `an ungranted package activated over a project of only its own files: Ready(… admitted: 0 …)` | | fixture control: claim a language that IS present | 1 RED at the *reason*, proving the fixture exercises the intended gate | ## The `Loader.cs` workaround is gone Deleted from the TimeLine packing job (`release.yml:3264`), replaced by a comment recording why. Its local stand-in — `the_shipped_timeline_package_activates_over_only_its_own_files` — runs on every CI leg and goes RED without the fix, so the removal is graded rather than merely performed. ## A doc claim this retires `package_baseline.rs`'s Ruby-leg doc said the XAML package "requests `bridge_source` only and no pool capability at all, so a project of nothing but `.xaml` could never activate it." That was one cause presented as two: `capabilities::admit_select` does not filter on *pool* capabilities and `gate` counts `COUNT(DISTINCT contribution_id)`, so a `bridge_source` grant that reaches the table **is** counted — it just never reached it. Corrected in place; the `inert` half now graded by `an_ungranted_package_still_fails_the_gate` rather than argued. Gates, isolated: fmt, clippy (host **and** windows-gnu), `cargo test --workspace` (327 suites, **3524 passed, 0 failed**), rustdoc `-D warnings`, corpus ratchet (6 passed in 22.37s — it really ran; `baseline.json` unmoved), precision gate **7/7 phantoms=0** with POPULATION lines read, and `COSI_E2E_LEG=daemon` (746 passed) — all exit 0.
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#233
No description provided.