ruby_package_parity: the SelfQualifier arm absorbs resolution differences, and closing #167's mechanism made 11 of them visible #170
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#170
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 closing #167. Filed rather than swallowed, and pinned in the suite so it cannot grow in silence.
The arm
classifytriesSelfQualifierfirst, and its predicate isC_TARGETis in there deliberately — the doc calls it "theselfqualifier, alone or with its resolution consequence" — but the arm never checks that the target moved because the qualifier did. Any row where the qualifier text differs AND the two legs resolved to different targets is classified asSelfQualifier, whatever caused the second difference.What that hid
EcosystemMarker's predicate was the shape "builtin~UNRESOLVED~, package resolved byTIER1B_FILE_KEY_IMPORT". It only ever saw rowsSelfQualifierhad already declined. Measured onruby-sinatraat its pinned sha:11 of the 876
SelfQualifierrows carryEcosystemMarker's exact shape — builtin unresolved, package resolved,brule == -1,prule == 13.They were counted as qualifier-text deltas before #167 as much as after it. This is not a regression; it is a pre-existing looseness that closing the other mechanism made legible.
Why it matters
The suite's whole claim is "every delta carries a
Mechanismwhose predicate is checked ON THE ROW" — the alternative to "a column was excluded from the comparison". An arm that admits an unexplained target difference alongside an explained qualifier difference is, for those rows, exactly the excluded column it was built to avoid. And the count is now load-bearing in a way it was not: with four of five mechanisms closed,SelfQualifier == 876is most of what stands between the suite and vacuity.What was done here
Not widened, not narrowed — pinned:
counted in
comparefromproject_rules(whichclassifyno longer reads at all) and asserted. If the number grows, the surviving mechanism is absorbing resolution differences it does not name, and someone has to look.Directions
SelfQualifierproper = qualifier text only (834 by the record's own count in_prdoc/records/84-P4-parity-deltas.md, "876 refs (834 alone, 42 with a resolution consequence)"). The 42-with-a-consequence half wants its own predicate saying what the consequence is — tier 1Q anchors on the qualifier, so a missing qualifier legitimately unresolves a ref, and that is checkable viaresolved_byon the builtin leg.Direction 2 is the smaller change and would move exactly those 11 rows to
None, i.e. to the loud panic. That is the right destination for them, but it needs the #86 qualifier work or a new named mechanism, so it is filed rather than done in the #165/#167 lane.Related
#167 (closed
EcosystemMarker, which is what exposed this), #84, #102, #86 (the missing qualifier text itself).🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
ruby_package_parityis RED at integration HEAD: #134's tier-3 origin gate reads a manifest relation the packaged leg structurally cannot have #167Direction 1 done; direction 2 REFUTED by reading the eleven rows
Measured in
/tmp/cosi-lane-precisionofforigin/master(f6a878a), ruby-sinatra at its pinned sha, both legs indexed as the suite does.The 876, decomposed
Instrumented
compareto classify every rowclassifycallsSelfQualifierby the direction of its resolution difference:834 + 42, exactly as
_prdoc/records/84-P4-parity-deltas.mdsays. What the record did not say is that the 42 run in two opposite directions.Direction 2's premise is false, and the eleven rows say so at source
The direction proposes requiring the resolution difference to run the way the mechanism predicts, on the premise that "a missing qualifier can only cost the package a bind, never gain it one; 11 of these rows are the package GAINING one, which the mechanism cannot explain."
All eleven are in one file,
sinatra-contrib/lib/sinatra/cookies.rb:…and the same for
response.lib/sinatra/base.rb:978is theattr_accessoronSinatra::Base.Sinatra::Cookiesis a helper module mixed intoSinatra::Base, soself.requestthere names exactly that accessor. The package leg's answer is the correct one, and the builtin's refusal is the error.The mechanism is the
selfqualifier's own gate, not the port: aself/Self/static/parentqualifier restricts the candidate pool to the ref's own file (I026),cookies.rbdeclares norequest, so the builtin refuses; with no qualifier text the package never enters that gate, reaches tier 1b's file-key import arm, and lands on the truth. That is the I026 residual already on record — "Self::/static:: resolve by locality not impl-hierarchy" — showing up in Ruby, where a mixin'sself.xis by construction not in the mixin's file.So sending these eleven to
Nonewould make the suite panic on correct binds. Direction 2 is refused. The rows do not need a louder destination; they need a name.Direction 1, implemented — three mechanisms, all predicates ON THE ROW
Mechanismis now:SelfQualifierdiffdoes not contain the target columnQualifierTextCostsARelativeBind~UNRESOLVED~SelfGateRefusesAMixinAccessor~UNRESOLVED~, package has a targetNothing reads a rule column to classify, so the split needs no signature change and stays a statement about the data. And the case the old predicate quietly admitted — both legs bound, to different targets — now falls to
_ => Noneand panics. It has never been observed here, and that is recorded as ungraded rather than claimed.The absorbed-count backstop (
SELF_QUALIFIER_ROWS_WITH_A_RESOLUTION_DELTA = 11) is replaced by something stronger, not deleted: each consequence arm is counted a SECOND time from the deciding RULE — the columnclassifydoes not read.SelfGateRefusesAMixinAccessor:brule == -1 && prule == 13QualifierTextCostsARelativeBind:brule == 41 && prule == -1A split that mis-assigns a row agrees with itself; it cannot agree with a column it never looked at. Plus a file-set pin: every
SelfGateRefusesAMixinAccessorrow must be insinatra-contrib/lib/sinatra/cookies.rb, so the population spreading is a finding rather than a count that drifted.The two structural facts the file already pinned — one delta per site, 59 files — now apply to the union of the three arms, so they stay the same two facts about the same 876 rows across the split.
MUTATIONS (RUN), and which gate fired
The three gates are ordered, so each run names the one that caught it. The third neutralises the first two, because a mutation that survives under a different gate proves nothing about the gate under it.
SelfQualifierfor anydiff ⊆ {qualifier, target}) → RED at the ROW/RULE cross-check:SelfGateRefusesAMixinAccessor … left: 0, right: 11.left: 0, right: 11— which is the point of the second observation.left: {}, right: {"sinatra-contrib/lib/sinatra/cookies.rb"}.Suite green after:
the_package_and_the_builtin_project_the_same_index ... ok,executed=1,controls=4, printing(Variants renamed from the drafting names
SelfQualifierCostsARelativeBind/SelfQualifierLiftsASameFileRefusal: clippy'senum_variant_namesrefuses three variants sharing one prefix, and the names it forced are better — one says what was lost, the other says what refused.)What this leaves open
SelfGateRefusesAMixinAccessoris now a named product finding, not a parity artefact: the builtin resolver loses 11 real binds in ruby-sinatra to theself-gate's same-file restriction, and we only know because a packaged leg without the qualifier text found them. Whether the gate should admit a mixin's host is #86/I026 territory and is not touched here. If that is ever fixed, this mechanism goes to zero and its variant should be deleted rather than pinned at zero — the file's own rule, for the reasonMechanism's doc gives.private_class_method :namemarks the INSTANCE method private, not the class method #102CLOSING — direction 1 is on master and graded; direction 2 stays refuted
Close-out lane. Verified on master
552e3a2in/tmp/cosi-lane-closeout.The arm no longer absorbs
Mechanism(crates/indexer/tests/ruby_package_parity.rs:409) now has three variants where the absorbing one stood:The backstop this issue asked for —
SELF_QUALIFIER_ROWS_WITH_A_RESOLUTION_DELTA = 11— is gone from the file, replaced by something stronger rather than deleted: each consequence arm is counted a second time from the deciding rule column (brule/prule), whichclassifynever reads, plus a file-set pin. The case the old predicate quietly admitted — both legs bound to different targets — now falls to_ => Noneand panics.Run here, exit code captured directly (no pipe)
executed=1,controls=4— not the vacuousexecuted=0 unavailable=1shape. 834 + 31 + 11 = 876, the same population, now split by three predicates each checked on the row.Direction 2 is refused, and that refusal is the finding
The premise — "a missing qualifier can only cost the package a bind, never gain it one" — is false, and the eleven rows say so at source. All eleven are
self.request/self.responseinsinatra-contrib/lib/sinatra/cookies.rb, binding theattr_accessoratlib/sinatra/base.rb:978.Sinatra::Cookiesis mixed intoSinatra::Base, so the package leg's answer is the correct one and the builtin's refusal is the error. Sending those rows toNonewould make the suite panic on correct binds.Residual, which is NOT this issue
SelfGateRefusesAMixinAccessoris now a named product finding rather than a parity artefact: the builtin resolver loses 11 real binds on ruby-sinatra to theself-gate's same-file restriction (the I026 residual, "Self::/static:: resolve by locality not impl-hierarchy", surfacing in Ruby where a mixin'sself.xis by construction not in the mixin's file). That belongs to #86, and if it is ever fixed the variant should be deleted, not pinned at zero — the file's own rule.Closing: the arm that absorbed unexplained resolution differences is split, every arm's predicate is checked on the row, and the count is cross-checked against a column the classifier cannot see.