bug: two versions of one package id can both be approved — the normal upgrade path creates it, and the diagnosis blames the wrong package #91
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#91
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?
Split out of #90's adversarial review, which reported it as "both generations of one package id run at once". That framing is too strong for the common case, and it misses the sharper problem. Verified by reading the code paths.
1. Creating the duplicate is the ORDINARY UPGRADE PATH
ApprovalRecord::setde-duplicates by digest:So
plugin enable <v2-digest>afterplugin enable <v1-digest>leaves both approved. An operator upgrading a package is in this state immediately, and nothing says so. Not an exotic input — it is what upgrading looks like.discover_with_storethen activates every approval with no id guard:2. What happens next splits on claims, and NEITHER branch is right
Claims intersect — the normal case for two versions of one package, since they claim the same extensions. The second is refused by the real check:
So it does not silently run both. But the refusal is
package.claim_conflictat scopePackageVsPackage, which tells the operator "another package's claims collide with yours". They go looking for a third-party package. The truth is "you approved two versions of your own", and the repair isplugin disable <old digest>. Right refusal, wrong diagnosis, wrong repair.Claims do NOT intersect — v2 drops a claim and adds another (v1 claims
.xaml, v2 claims only.axaml). Then both activate, and that is where the review's stronger claim is correct.3. The sharp part: two live owners MINT THE SAME LANGUAGE ID
The wire language id is derived
<package id>/<local id>, so two digests of one package id produce the samefiles.lang/symbols.lang.That falsifies a premise stated in the claim machinery's own doc:
With duplicate package ids, two owners share one. #77 deliberately made provenance row-level and said
files.langcannot be the source of truth for mixed-language resolution — but a consumer reading the language id now cannot tell which of two live digests produced a row, and the claim algebra is reasoning under an assumption that no longer holds.4. #90 can create this WITHOUT a human
Activation-on-demand writes approvals. After #90's F2 fix an auto-enable only writes a digest the basis project approved — but if this project already has v2 approved manually and the basis approved v1, v1 is "not approved here", so it is offered and can be silently enabled. The duplicate then arrives with nobody having chosen it.
What to decide, not just what to fix
The fix is not obviously "supersede by package id". Silently dropping a grant an operator explicitly made is its own defect, and pinning an exact digest is the epic's stated model ("Projects activate an exact digest", #75). Candidate directions:
enablerefuses when another digest of the same id is approved, naming it and the one command that resolves it. Preserves "an operator's grant is never dropped by a machine".setsupersedes by package id, with the superseded digest disclosed in the reply. Matches upgrade intuition; violates 1.Whichever is chosen, two things are needed regardless:
package.claim_conflict, so the witness sends the operator to the right repair.plugin_activation/plugin statusshould say "two versions of<id>are approved here" while it is true.Acceptance
package.claim_conflictfor this case asserts the new code instead — and a mutation restoring the old code goes red.Shipped in v0.26.0
When a store holds more than one approved, resolvable digest of the same package id, only one can be live. Which one that is, and which digests it displaced, are now reported — previously the others simply did not run and nothing said so.
DuplicatePackageId { package_id, active, shadowed }: the id as the stored bytes declare it, the digest that activates, and every other approved resolvable digest of the same id. It is a view over the existingunapprovedlist, not a fifth list — these rows belong in the same place as every other "you asked, and here is what happened to it" answer.The daemon-side type is separate from the indexer's by design: the indexer type is the live set's own vocabulary, the wire type's field names are a contract a client matches on, and the mapping between them is the identity — so there is nothing to re-derive and therefore nothing that can drift.
Live on the installed build
An empty measurement, not an absent field.
Ordering
The set is applied in digest-sorted order, so which copy activates is deterministic rather than filesystem-dependent. Worth recording because the decision record for this originally rejected "lowest digest" on the grounds that it would change which version runs on machines working today — that claim was wrong,
setalready sorted by digest, the two rules are identical, and the record was corrected in place rather than left standing.EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119archive_refusedreaches nocoverage_reasonscode, so an agent's answer is qualified by nothing #124