plugin ABI: the qualifier-TEXT gap (gaps 1 and 2 CLOSED; gap 3 is a #77 capability decision, not an ABI gap) #86
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#86
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?
Status, as of 2026-09-06
Ref::derived_name,crates/abi/src/record.rs:151. Grant path graded byruby_package_e2e::a_rails_association_is_refused_for_want_of_the_derived_name_grantand…an_operator_can_grant_derived_names_and_the_rails_file_then_indexes— both green, 5/5, this laneattr_start_linehas no wire representationTAG_SYMBOL_ATTR_SPAN = 0x8001atcrates/abi/src/record.rs:17, and it is mapped:crates/indexer/src/packages.rs:4475readsattr_start_line: s.attr_span.map(|b| pos.line_col(b.start as usize).line), withwithhold_foreign_attr_linesat:4484as its enforcement halfis_extensionhas no wire bitTAG_SYMBOL_IS_EXTENSION = 0x8002exists atcrates/abi/src/record.rs:18and decodes. The host writes literalis_extension: falseatpackages.rs:4481, deliberately, with the reason at:4462-4473packages.rs:4869-4871and graded byruby_package_parity'sSelfQualifiermechanismGap 2: strike the sentence
The original body says "
packages::facts_to_extracthardcodesNonebecause there is nowhere on the wire to carry it." That has been false since51a72ee(2026-09-01). The mapping is live atpackages.rs:4475. The sentence is struck rather than deleted because it was cited onward: it is why the migration table below listed rust/python/typescript/csharp as blocked, and they are not blocked by this.Gap 3: a decision, not plumbing
Wiring the bit through would falsify
pool_capability_registry'sNeverDynamic("s.is_extension = 1")stance — that registry's registered structural justification IS the hard-codedfalse— and would admit a package to the C# extension-method candidate pool by setting one wire bit, with no grant.ext_candsis populated by one statement that consults nothing but the bit, so believing the bit is the authorisation.Closing it is three coordinated edits mirroring
derived_namesexactly: a fourth[capabilities]key through manifest →HostPolicy→Grants→ activation digest;facts_to_extractpassing the bit; and the registry row moving fromNeverDynamictoGated. Step 2 without step 3 makes the registry lie.It should be taken deliberately or recorded as declined, not left reading as unfinished ABI plumbing. Splitting it out is the right move; C# is blocked on it either way.
Gap 4: the qualifier TEXT field — where the work is
RawRef.qualifier's own doc says the qualifier is "exactly as written in source". Measured across seven languages that is false in three, by three independent mechanisms:@ivararm attachesqualifier = "self";ruby/sample.rbcontainsselfzero times.emit_implicit_member_accessattachesqualifier = "this";csharp/Sample.cscontainsthiszero times.join_useassembles nested brace-import prefixes:use std::sync::{atomic::{AtomicBool, Ordering}, Arc};yieldsstd::sync::atomic, which appears nowhere contiguously in the file.qualifier_span/receiver_spancannot represent any of these, because there is no span to point at — the text does not exist in the source.Measured, not predicted: 876 ref rows on the #84 Ruby parity leg (
834with no resolution consequence +31+11;ruby_package_parity.rs:995). Adjudicated not a port defect and proved causally: patchingcrates/plugins/src/ruby.rsto emitNonetook the delta 876 → 0 and the resolution delta 99 → 57.It is the only pinned delta left in
ruby_package_parity— #112's three host gates went to zero on 2026-09-06 and #167'sEcosystemMarkerwas deleted rather than pinned. Verified green in this lane at master4f866e5:executed=1, controls=4, SelfQualifier: 834.Shape of the fix, and it is a capability change and not only a wire one: a qualifier the source does not contain is authority of the same kind
derived_namesis, so it needs a fourth[capabilities]key through manifest →HostPolicy→Grants→ activation digest, exactly as #103 shipped. That movesextraction_identityand re-extracts every claimed file on every project with the package enabled — a coupled landing with the guest rebuild, the wasm bytes,WASM_ARTIFACTS, the.digest, theplugin.tomlversion and the recorded expectations.Revised migration table
join_use's assembled brace-import prefixesVisibility::Unknownhas no wire slot and a packaged JavaScript loses 19.4% of all resolutions (js-express, measured causally)emit_implicit_member_access's fabricated"this"Related
#84 (the Ruby migration, done), #103 (the
derived_namesprecedent to copy), #112 (closed — the three host gates), #166 (the TypeScript/JavaScript visibility residue, split out), #167 (closed), #80, #75.ORIGINAL BODY, 2026-09-01 — preserved; gap 2's premise is false and gap 3 is misfiled as an ABI gap
Measured during #80's pre-release review and #84's candidate analysis. All three are additive to the wire format — no schema migration — and each blocks a specific set of languages from ever becoming a package.
Filed together because the long-term direction is that every language ships as a plugin, and these are what stand between "one reference package" and that.
1. Derived refs have no marker — blocks Ruby's Rails support
The ABI's strongest check is
name == src[name_span], and it is right to be: it is what stops a package inventing a binding at a span it does not own.But a plugin may legitimately emit a ref whose NAME is derived rather than copied.
crates/plugins/src/ruby.rs's Rails association DSL readshas_many :postsand emits atyperef namedPost— singularised and camelised — at the span of the literal:posts.validaterefuses it withfact.span_name_mismatch, and validation is all-or-nothing per file, so three offending rows cost eleven refs across two whole files.The ABI already carves out exactly one derived kind (
binding) by name. What it needs is a general marker: a ref that declares itself derived, so the span means "where this fact came from" rather than "the bytes of this name". The verifier's guarantee is preserved for every ref that does not claim it.Blocks: Ruby (the #84 candidate). Any framework-aware extraction in any language — which is the whole "frameworks are plugins too" half of the direction.
2.
attr_start_linehas no wire representation at all — blocks four languagesRawSymbol::attr_start_lineexists in the row and is populated by four builtins — rust (8 sites), typescript (7), csharp (6), python (3) — andpackages::facts_to_extracthardcodesNonebecause there is nowhere on the wire to carry it.So any package implementing one of those four either loses the field or ships a pinned delta on a columnsymbol_atand the diff-overlap query both read.Fix: an optional tag (
>= 0x8000, the existing skippable convention) plus one line infacts_to_extract.symbols.attr_start_linealready exists — no migration. (Both halves shipped.)3.
is_extensionhas no wire bit — blocks C#Same shape, narrower: only csharp emits it (8 sites), and the resolver tier behind it is keyed on C# grammar knowledge, so closing it is a resolver change rather than only an ABI change.Blocks: csharp, outright.
Why these are worth doing before a second migration, not during
#84's candidate table exists because two languages emit neither field:
attr_start_line?is_extension?So Ruby and PHP can migrate today (modulo gap 1), and nothing else can migrate at all until gaps 2 and 3 close. A migration that opens with "these two columns are pinned deltas" is a weaker proof than one where they are identical by construction.
Related
#84 (Ruby migration, the first one), #85 (extraction serialization), #80. Record with the measurements:
_prdoc/records/80-S43-migration-candidate.md, corrected in place after gap 1 was found by a test rather than by the analysis.The ABI half of all three gaps SHIPPED on 2026-09-01/02 — this issue was stale
Audited before writing anything. All three landed in
51a72ee("three ABI facts a real language plugin needs") and1d90c3a("a project grants derived names; a package no longer holds them"). This issue's own premise — "packages::facts_to_extracthardcodesNone" — has been false for two days. Same hygiene problem as #81.What exists, verified by reading it:
Ref::derived_name, bit 2 ofTAG_REF's presence mask, a required record on purpose: a skipped flag would change a verdict, not reduce detail. The old by-namebindingcarve-out is gone; the check is nowif !derived_name { name == src[name_span] }. Gated by a split grant word (Grants { roles, derived_names }) whoseFrom<u32>grants roles only, so all 39 legacy call sites deny. The role bit (19) sits outsideGRANTABLE_ROLES, so it is unforgeable in both directions, and it is folded into the activation digest.TAG_SYMBOL_ATTR_SPAN(0x8001), carrying a span, not a line (positions are derived parent-side), refused whensp.start > decl.start.TAG_SYMBOL_IS_EXTENSION(0x8002), presence-as-value sofalsehas exactly one encoding.What was NOT done, and it was the half that matters
The consumer half was completely ungraded.
a_validated_frame_becomes_the_same_shape_a_builtin_plugin_returnsassertedattr_start_line == Noneand!is_extensionon a frame that emitted neither annotation record — vacuous with respect to both mappings — and its comment still claimed "the wire has no field for" them.Now closed by
an_attribute_span_becomes_a_line_and_the_extension_bit_is_dropped. Its fixture puts the attribute on line 3 at byte 18 and the declaration on line 4, soNone, the byte offset, and the declaration's line are three distinguishable wrong answers.The measured survivors are why this mattered. Applying both mutations together — restore gap 2's
attr_start_line: None, and flip gap 3's deliberate drop tois_extension: s.is_extension— left the entire five-crate suite green. A package could have entered the C# extension-method candidate pool with no capability gate, and nothing would have said so.attr_start_line: None(the pre-#86 hard-code)left: None right: Some(3)left: Some(18) right: Some(3)left: Some(4) right: Some(3)is_extension: s.is_extensionGap 3 end-to-end needs a CAPABILITY DECISION, not a resolver change
ext_candsis populated by one statement that consults nothing about extension-ness beyond the bit:Reachability, uniqueness and visibility are all language-agnostic, so the SQL does not need to change to admit a package's rows. What blocks it is that the bit is a conclusion, not evidence: nothing on the wire says "this method's first parameter is a
thisparameter", so believing it means authorising a package to place itself in a binding pool.Closing it is three coordinated edits mirroring
derived_namesexactly — a fourth[capabilities]key through manifest →HostPolicy→Grants→ digest bit;facts_to_extractpassing the bit; andpool_capability_registrymovingext_candsfromNeverDynamic("s.is_extension = 1")toGated(...), because that registry's registered justification IS the hard-codedfalse— step 2 without step 3 makes the registry lie.influence.rsand_prdoc/guides/80-capability-review.mdrepeat the same justification.Recommendation: not now. It is a #77 language-profile capability decision, and the ABI half is what this issue asked for. The new test pins the current stance so the flip cannot happen by accident.
Remaining scope
Gaps 1 and 2 are complete and now graded. Gap 3's ABI half is complete; its end-to-end enablement is a capability decision that should be split out rather than left implying this issue is unfinished.
Status after v0.26.0 — two of three closed, the third deliberately left open
Gap 1 (derived refs have no marker) — CLOSED. A ref can now declare itself derived, so its span means "where this fact came from" rather than "the bytes of this name". The verifier's
name == src[name_span]guarantee is preserved for every ref that does not claim it. This unblocks Ruby's Rails association DSL (has_many :posts→ atyperef namedPostat the span of:posts) and framework-aware extraction generally.Gap 2 (
attr_start_linehas no wire representation) — CLOSED. Optional tag on the existing skippable convention, plus the line infacts_to_extract. No migration;symbols.attr_start_linealready existed.The consumer half had been entirely ungraded, which is worth recording:
attr_start_line: Noneand the deliberateis_extensiondrop could BOTH be reverted and the whole five-crate suite stayed green. A package could have entered the C# extension-method candidate pool with no capability gate and nothing would have failed.Gap 3 (
is_extensionhas no wire bit) — OPEN, and staying open on purpose.Admitting the bit means authorising a package into the C# extension-method binding pool — the pool SQL consults nothing but the bit, so believing it is the authorisation. That is a capability decision, not a mapping one, and it is left as one rather than smuggled in as an ABI addition. C# remains blocked on it, as this issue says.
Also shipped alongside, from #87
A guest can now map a kind NAME to a kind id without a new host call. Guests run with an empty import list — that emptiness is what makes
fork,openandconnectinexpressible — so a guest cannot ask "what number isfunction_item?". It now asserts and the host checks: the guest exports one immutablekind_table_digest, and the worker enumerates the grammar it just loaded, in the same process whosetree::serializewrites those ids, refusing on mismatch with exit 24 and printing the number the guest should have carried.Every id is proven rather than sampled, on two axes — because a parity test cannot grade code that both its sides run. Reading
node_kind_is_visiblewherenode_kind_is_namedbelongs survived the wasm-vs-native comparison; the Node-API oracle on a real parse is what kills it. The zero-import property is measured by countingModule::imports(), not asserted in prose.What this means for the migration table
Ruby and PHP could already migrate modulo gap 1; that caveat is now gone. Python, Rust and TypeScript are unblocked by gap 2. C# alone still cannot migrate, and the reason is a capability decision that has not been made rather than a missing wire tag.
derived_names, so a package using #86 gap 1 can never be enabled #103A fourth ABI gap, now measured against a real port rather than predicted: the fact ABI has no qualifier TEXT field.
The #84 Ruby migration measured this at scale.
RawRef.qualifier's own doc says the qualifier is "exactly as written in source". Across the seven languages that is false in three, by three independent mechanisms:@ivararm attachesqualifier = "self";ruby/sample.rbcontainsselfzero times.emit_implicit_member_accessattachesqualifier = "this";csharp/Sample.cscontainsthiszero times.join_useassembles nested brace-import prefixes:use std::sync::{atomic::{AtomicBool, Ordering}, Arc};yieldsstd::sync::atomic, which appears nowhere contiguously in the file.qualifier_span/receiver_spancannot represent any of these, because there is no span to point at — the text does not exist in the source.What the port measured
876 rows. That is the single largest delta between the builtin and packaged Ruby legs, and it was adjudicated as not a port defect: the builtin fabricates a word absent from the source, and the packaged guest declined to, because the wire has no way to say "this qualifier is derived rather than quoted".
Proved causally rather than argued: patching
crates/plugins/src/ruby.rsto emitNonetook the delta 876 → 0, and the resolution delta 99 → 57.So this is not a cosmetic difference. It costs a packaged language real resolutions, and no manifest can close it — the same shape as #112's three language gates.
Why it belongs here
This issue already carries
attr_start_line(no wire slot, blocks rust/python/typescript/csharp) andis_extension(blocks csharp). This is the third of the same kind, and it is the one with a measured row count behind it.The shape of the fix is the one #86 already prefers: a derived-fact marker generalising the carve-out that exists for
binding, applied to qualifiers — one mechanism closing the class, rather than a per-language exception. #103 shipped exactly that pattern for derived names (derived_namesas a requested, granted, fail-closed authority with a measured witness), so there is now a working precedent to copy rather than a design to invent.Noting also that #86's framing — "three gaps that block migrating any language other than Ruby or PHP" — is now partly outdated in a useful direction: Ruby has been migrated, and this gap was the largest thing the migration hit. It is a prerequisite for migrating C# and Rust, and a smaller one for Ruby.
GUEST_ABI_MAJORis bracketed by no manifest field, andplugin pack --check-reproducibledoes not exist #153Re-measured 2026-09-05 by an independent pluggability review — one of the three gaps is refuted, and the real blocker is not in this issue
Gap status
attr_start_lineTAG_SYMBOL_ATTR_SPAN(0x8001) exists andfacts_to_extractmaps it (crates/indexer/src/packages.rs:4349-4517), graded with three run mutations. rust/python/typescript/csharp are not blocked by this.is_extensionfalse, deliberately: admitting the bit is the authorisation into the C# extension-method binding pool. That makes it an unmade capability decision (#77), not a missing slot.The qualifier-text gap, causally proved
The wire carries
qualifier_span/receiver_span— byte ranges only. A qualifier that is not a contiguous substring of the file is inexpressible. Three languages fabricate one: ruby"self", csharp"this", and rust'sjoin_useassemblingstd::sync::atomicfrom nested braces.The 876 rows are real and the causation is established, not inferred:
crates/indexer/tests/ruby_package_parity.rs:848pins it as an executable predicate (mutation-tested against being widened into a column exclusion), and_prdoc/records/84-P4-parity-deltas.mdrecords that patchingruby.rstoNonetook 876 → 0 and the resolution delta 99 → 57.Scoped to the fact ABI alone: php, python, typescript/javascript are clean; ruby ships with the 876 pin; rust ships with a recall delta on brace-imports; C# alone has a field the host refuses to carry.
Two things NOT in this issue that matter more
1. Column parity is zero for all six, and the cause is host-side, not ABI. Three allowlists keyed on builtin language ids miss any packaged language regardless of its plugin — that is #112, and it is the highest-leverage fix available:
writer.rs:883'squalified_name_separatorends_ => None, and a package'sfiles.langalways contains/, so every packaged symbol getsqualified_name = NULL(1260/1260 rows) — which is exactly what tier 1Q and TYPE_FQN join on. One generic gate change unblocks every future package, not one language.2.
BUILTIN_CLAIMSrefuses a builtin's extension at pack time, upstream of the ABI entirely — see #87, where the owner has now decided on an operator-consented override.New, belongs here
TypeScript emits
Visibility::Unknown(crates/plugins/src/typescript.rs:955), which has no wire slot, andabi_projection.rs:613swallows it withunwrap_or(2)— silently rewritingunknown→file. In a file whose own doc records that its previous defect was "three silentcontinues". The recall cost was not measured (the reviewer ran nothing).🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119Re-measured against a real port, 2026-09-05 — the qualifier-text gap is now the ONLY remaining product delta between a packaged language and a builtin one
The previous comment said column parity was zero for all six and that the cause was host-side (#112), not the ABI. #112 is now fixed, so this issue's gap list can be stated against a clean background for the first time.
ruby_package_parity, after the fix, over 156ruby-sinatrafiles, 1260 symbols and 18,450 ref sites:QualifiedNameLangGateSameDirLangGateTypePositionLangGateSelfQualifier— the fact ABI has no qualifier TEXTEcosystemMarkerEvery symbol column is now identical row for row — the parity suite's symbol loop asserts
diff.is_empty()and carries no pinned mechanism at all. 876 ref rows and one harness artifact are what is left.So the framing changes: this is no longer "one of several ABI gaps beside several host gaps". The host gaps are closed, and the qualifier-text gap is the single thing standing between a packaged Ruby and a builtin one. It is also, per the earlier causal experiment (
ruby.rspatched toNonetook 876 → 0 and the resolution delta 99 → 57), the one with a measured recall cost that no manifest and no host change can close — unlike #112's three, which turned out to be one gate.Gap status, restated
attr_start_lineTAG_SYMBOL_ATTR_SPAN(0x8001) exists andfacts_to_extractmaps it, graded with three run mutations.is_extensionfalsebecause admitting the bit is the authorisation into the C# extension-method binding pool.On the TypeScript
Visibility::Unknownfinding in the previous commentRe-checked, and it needs correcting in one direction and sharpening in the other.
The
unwrap_or(2)is not in production and is not undocumented. It iscrates/plugins/tests/abi_projection.rs— a test harness — and its comment says the mapping is deliberate:unknownis absent fromcode_index_abi::VISIBILITIESon purpose,DEFAULT_VISIBILITYisVisibilityId(2)=file, andregistry.rsstates the reason at length: "'not extracted, so treat as widely visible' is precisely the authority a third party may not assert."a_packaged_extractor_cannot_claim_unknown_visibilitygrades it. Calling it a silent swallow was wrong.The residue underneath it is real, and it is a fourth ABI gap in this issue's own sense.
core::raw::Visibility's own note names the asymmetry:Unknownpools asExportedfor a compiled plugin and defaults toFilefor a packaged one. Those are opposite ends of the visibility lattice.typescript.rs:955returnsUnknownfor a CommonJS file whose exports the plugin could not detect — and its comment says exactly why that must not becomeFile: "mis-marking those File would silently drop real cross-file resolutions."So a packaged TypeScript would take precisely the loss that arm exists to avoid. Not a bug in the mapping — the mapping is the correct least-authority default — but a capability shape the ABI cannot express: "I looked and could not tell" is a third state, and the wire has only "exported" and the four it enumerates.
The recall cost is still unmeasured, and I did not measure it either. Stated rather than implied. The measurement that settles it is one query on an indexed
js-expressorts-zod:SELECT COUNT(*) FROM symbols WHERE lang IN ('javascript','typescript') AND visibility = 'unknown', and then how many refs currently resolve to those rows. If it is near zero the gap is theoretical and TypeScript can port; if it is not, it is a blocker of the same kind asis_extensionis for C#, and it should be split out rather than left as a paragraph here.Revised migration table
join_use's assembled brace-import prefixes (use std::sync::{atomic::{…}}→std::sync::atomic, which appears nowhere contiguously).Visibility::Unknownthird state above, cost unmeasured.is_extension— an unmade capability decision (#77) — plus the qualifier gap foremit_implicit_member_access's fabricated"this".🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Visibility::Unknownhas no wire slot, and it costs a packaged JavaScript 19% of ALL its resolutions — MEASURED #166The
Visibility::Unknownresidue is MEASURED and it is not theoretical — split out as #166. The qualifier-TEXT gap is NOT attempted here, and the reason is a blocked gateTwo things, one closed and one deliberately refused.
1. The open measurement in the previous comment is now done
That comment said, of the
Visibility::Unknownthird state: "The recall cost is still unmeasured, and I did not measure it either. Stated rather than implied. … If it is near zero the gap is theoretical and TypeScript can port; if it is not, it is a blocker of the same kind asis_extensionis for C#, and it should be split out."It is not near zero. A packaged JavaScript loses 19.4% of ALL its resolutions.
Measured causally, not bounded:
typescript.rs's CommonJS arm was changed to returnVisibility::File— the exact mappingfacts_to_extractapplies to a packaged extractor — and both corpora were re-indexed from scratch with the same binary otherwise.js-expressts-zodThe population underneath it:
js-expressis 1,860 of 1,917 symbolsunknown(97.0%), with 833 refs resolving cross-file into them — which is what the 805 tracks.ts-zodis 4,387 of 9,667 (45.4%) with 130 cross-file.Split out as #166, with the method so it can be re-run. The plugin edit was reverted and md5-verified; nothing is checked in.
That makes the revised migration table's
typescript / javascriptrow concrete: javascript is blocked hard, and by the largest measured number in this issue's family.2. The qualifier-TEXT gap — NOT attempted, and the precondition is named
This lane held the packaged-Ruby surface and #86's live gap was in scope. It was not built, and the reason is not effort.
The 876 rows are verified by exactly one gate, and that gate is RED for an unrelated reason.
ruby_package_parityis failing atintegrationHEAD — bisected to2f16e22's #134 tier-3 origin gate, filed as #167 with the mechanism (manifest_package_dirsis basename-keyed and the parity harness shadowsGemfiletoGemfile.rbx, sopkg_diris''on the whole package leg and the gate is vacuously true there). 17 unexplained ref deltas, in both directions.An ABI change whose whole proof is "this gate's 876-row
SelfQualifiermechanism goes to 0" cannot be landed while that gate cannot be read green. It would be a green that means nothing, which is the failure mode this issue's own history keeps finding.Two further reasons, stated so the refusal is inspectable rather than just cautious:
derived_namesis, so it needs a fourth[capabilities]key through manifest →HostPolicy→Grants→ activation digest, exactly as #103 shipped for derived names. That movesextraction_identityand re-extracts every claimed file on every project with the package enabled;crates/indexer/src/index.rs's origin gate is being edited by another lane right now (#165). A resolution-moving change landing beside it makes both deltas uninspectable.The sequencing that works: #167 first (it is a host gate, and it is in the same code #165 is already in), then the qualifier-text capability with the 876 → 0 delta read off a green parity suite, in one change with the guest rebuild and the package version bump — the same coupled-landing shape #102 needed.
Related landing
#102's packaged half DID land in this lane (
f3fceedonwip/rubypkg), with the full coupled-artifact set moved together and package version 0.4.0. That one was verifiable without the parity suite, becauseplugin checkgrades the guest against the builtin-generated expectations directly.🤖 Generated with Claude Code
https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
ruby_package_parity: theSelfQualifierarm absorbs resolution differences, and closing #167's mechanism made 11 of them visible #170Triage 2026-09-06: LEFT OPEN — PARTIAL. Gaps 1 and 2 are closed (gap 2's premise is outright refuted), gap 3 is a deliberate unmade decision, gap 4 is the live one.
Comments supersede the body heavily here; graded as four gaps, per those.
Ref::derived_name—crates/abi/src/record.rs:151(52 sites); grant path proven byruby_package_e2e::a_rails_association_is_refused_for_want_of_the_derived_name_grantand…an_operator_can_grant_derived_names_and_the_rails_file_then_indexesattr_start_lineTAG_SYMBOL_ATTR_SPAN = 0x8001atcrates/abi/src/record.rs:17, and it is mapped:crates/indexer/src/packages.rs:4475attr_start_line: s.attr_span.map(|b| pos.line_col(b.start as usize).line), pluswithhold_foreign_attr_linesat:4484is_extension0x8002atrecord.rs:18; host writes literalis_extension: falseatpackages.rs:4481, with the reason at:4462-4473packages.rs:4869-4871Gap 2 is worth calling out
The body says
facts_to_extracthardcodesNone. It does not — the mapping is atpackages.rs:4475. That sentence should be struck; leaving it invites someone to "fix" a thing that works.Gap 3 is a decision, not an omission
Wiring
is_extensionthrough would falsifypool_capability_registry'sNeverDynamic("s.is_extension = 1")stance and admit a package to the C# extension pool with no grant. That is a #77 capability decision and the comment atpackages.rs:4462-4473says so. It should be taken deliberately or recorded as declined — not left reading as unfinished plumbing.Gap 4 is where the work is
The 876-row
SelfQualifierdelta stands, and it is the only pinned delta left inruby_package_parity— the three #112 populations went to zero today (see #112, closed). It was explicitly not attempted because it is blocked behind #167/#170:ruby_package_parityis that gap's only grader, and it has been red for an unrelated reason.So gap 4 is blocked on someone else's lane, which is a fine reason to leave this open and a bad reason to leave it unstated.
One doc-drift defect found while verifying — worth fixing while here
crates/indexer/src/packages.rs:4478-4480still says:That contradicts
TAG_SYMBOL_IS_EXTENSIONatcrates/abi/src/record.rs:18, and contradicts the longer comment fifteen lines above it in the same expression, which correctly says "Its ABI half is closed too." One file, one expression, two opposite claims — and it sits exactly where a future reader would look to decide whether gap 3 is possible.🤖 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 #184packages.rs:4478-4480says the wire has no bit foris_extension, contradictingrecord.rs:18and a comment fifteen lines above it in the same expression #185ruby_package_parity: theSelfQualifierarm absorbs resolution differences, and closing #167's mechanism made 11 of them visible #170packages.rs:4478-4480says the wire has no bit foris_extension, contradictingrecord.rs:18and a comment fifteen lines above it in the same expression #185plugin ABI: three gaps that block migrating any language other than Ruby or PHPto plugin ABI: the qualifier-TEXT gap (gaps 1 and 2 CLOSED; gap 3 is a #77 capability decision, not an ABI gap)GUEST_ABI_MAJORis bracketed by no manifest field, andplugin pack --check-reproducibledoes not exist #153GUEST_ABI_MAJORis bracketed by no manifest field, andplugin pack --check-reproducibledoes not exist #153