A packaged language is invisible to every host table keyed on a BUILTIN language id — three shipped sites, found by #84 axis C #112
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#112
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 by #84 phase 4 axis C (
crates/indexer/tests/ruby_package_parity.rs), comparingcrates/plugins/src/ruby.rsagainsttests/packages/rubyover 156ruby-sinatrafiles. Full write-up and the causal experiments:_prdoc/records/84-P4-parity-deltas.md.One mechanism, three sites
A package's wire language id is
<package id>/<local id>—de.h-dv.ruby/ruby.code_index_package::langidmakes that non-colliding with a builtin id by construction:LanguageId::builtinrequires^[a-z]+$and a wire id always carries a/. That is exactly what makes producer identity provable, and it is also what makes every host table spelled with a builtin id silently exclude a package.Three shipped sites, each measured by running the comparison with the site patched and watching the delta go to zero:
crates/indexer/src/writer.rs::qualified_name_separator—match lang { "rust" | "ruby" => "::", … _ => None }qualified_name = NULLcrates/indexer/src/index.rstier-1b,st.lang IN ('php', 'ruby', 'csharp')(I023 importless same-directory arm)resolved_by::TIER1B_SAME_DIRECTORYnever fires for a packagecrates/indexer/src/index.rs::position_type_pairs_sql/code_index_core::kinds::POSITION_TYPE_KINDS_BY_LANGmember_accessre-kind exemption never applies, so type refs are demoted1 is the loudest
qualified_nameis not on the fact wire at all —code_index_abi::record::Symbolhas no such field, and the writer derives it from the parent chain. So a package cannot supply one and the host will not build one. 1067 of the 1260 symbols carry a real FQN on the builtin leg (Sinatra::CustomLogger::logger); the package leg has none.qualified_name_separator's own comment already records the identical defect one language over:Verified: with the lookup made to strip a leading
<package id>/, the symbol projection became identical on all ten columns, all 1260 rows.Why it is not a one-line fix
Stripping the package prefix and reusing the builtin entry is a guess: a package whose local id happens to be
rubyis not necessarily Ruby. Each site needs a product decision about what a manifest may declare:All three are ABI/manifest surface, all three are inside
extraction_identityif added, and none is expressible today.Status
Pinned, not fixed.
ruby_package_parity.rspins each population by a predicate on the row — for thequalified_namedelta the pin is constructive (the builtin's value must equal the::-join of the parent chain rebuilt from the package leg's own rows), so the only thing the delta may cost is the host-side join.Site 3's keying is DELIBERATE and correct — which is exactly why this is a product decision and not a bug fix
code_index_core::kinds::POSITION_TYPE_KINDS_BY_LANG's own doc says so, at length:So the fix is not "make the predicate lang-blind" and not "strip the package prefix and reuse the builtin row" — the second is a guess that a package whose local id is
rubymeans Ruby'smodule. The doc already names the shape of the right answer: "adding a row is an explicit, reviewable claim". Today only a compiled-in language can make that claim; a package cannot, because there is no manifest field for it and therefore nothing for a capability review to review.The same argument runs for site 2 (
lang IN ('php','ruby','csharp')is a claim that the language is importless) and site 1 (the separator is a claim about how names compose). Three claims a builtin makes in Rust and a package cannot make at all.EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119EMBEDDED_DISPATCH_SEMANTICS_VERSION = 0, so no file can carry two producers and #77's criterion 2 is unexercisable #119private_class_method :namemarks the INSTANCE method private, not the class method #102FIXED. One gate, three sites, and the parity suite that measured the defect is what graded the fix.
code_index_core::lang_profile— two functions, one rule.sql_adopts("s.lang", "ruby")renders(s.lang = 'ruby' OR s.lang GLOB '*/ruby'). That is the whole mechanism.The measurement, which is the point
Running
ruby_package_paritywith the gate in place, against the pin as it stood:QualifiedNameLangGate—writer::qualified_name_separatorSameDirLangGate— tier-1bst.lang IN ('php','ruby','csharp')TypePositionLangGate—POSITION_TYPE_KINDS_BY_LANGSelfQualifier— #86, the fact ABI has no qualifier TEXTEcosystemMarker— the harness's own shadow extensionAll three to zero in one pass, and the two this change does not touch stood still. That second half is what makes it evidence rather than a coincidence: a change that had merely stopped classifying would have taken all five to zero.
Column parity for a packaged language is now exact. What remains is one product gap (#86's qualifier text) and one artifact of this harness.
Why it is one change and not three
The three sites ask three different QUESTIONS about a language — how names compose, whether proximity is admissible evidence, which kinds mean "type". They are not three keying problems. There is one keying problem: a host table keyed on
rustcannot seecom.example.x/rust.So the fix changes the KEYING, once, and leaves all three tables exactly where their reviewed rationale already lives. This matters for the objection in this issue's own comment — that
POSITION_TYPE_KINDS_BY_LANG's language keying is deliberate and correct. It is, and it is untouched: the exemption still comes row by row out of that table,modulestill means "type" in ruby and namespace in csharp/php/rust, anda_namespace_is_never_exempted_even_in_type_positionstill holds. What changed is thats.lang = 'ruby'becamesql_adopts("s.lang", "ruby").The two tables that were inline literals moved into
lang_profilewith their comments carried across verbatim — the separator table (including the javascript paragraph about m0004→m0042) and the importless list (including I037 round 2's revertedrecv_unprovengate). Nothing was re-reasoned; it was relocated so one gate can read all three.The objection this issue raises, answered
Right — if the host never says what the local id means. The fix is that it now does, and the doc says it in the module that owns the rule:
A language whose LOCAL id names a host profile adopts that profile.
LocalLangId::parsealready, deliberately, permits a local id to look exactly like a builtin one;langid's own doc says the namespace and not the local name is what keeps the wire ids apart. Choosingid = "ruby"is therefore an act, not an accident, and it is now read as one: my language composes names the way Ruby does, has no import statement that makes a definition reachable, and means a TYPE bymodule.Four properties hold it up:
mylang,xaml— yields no profile, no separator, no importless arm, no exemption. Byte-for-byte what this host did before. Graded bya_language_that_names_no_profile_adopts_nothing.qualified_name, its symbols as candidates for its own refs, its kinds' exemption from its own re-kind. A package adopting the wrong profile degrades its own resolution and cannot reach another producer's edges.confirm::Disclosuregains alanguage_profilesfield rendered in the block an operator answers, one line per declared language:de.h-dv.ruby/ruby adoptsruby— qualified_name joined with::; same-directory proximity is admissible evidence for a method call;modulemay sit in type positionand for a package that adopts nothing, a line that says so and says what it means. It is the three-state
Measured/UnmeasuredshapeDisplacementalready uses, soplugin gcandplugin rollback— which name no package — say "not measured" rather than printing nothing.extraction_identitymovement, no migration. A shipped package's identity does not move.Gates
ruby_package_parity(876, 4)precision_gatephantoms=0in every language, recall 1.000corpus_ratchet(release,COSI_CORPUS_REQUIRE=1)tests/corpus/baseline.jsonmd5534084b856c22566c48e386bc41ed67eunmovedruby_package_costclippy --workspace --all-targets -D warningsrustfmt --checkOn the baseline: it did not move, and the first run said it had. A run in the shared six-lane checkout reported
rust-ripgrep refs +70,ts-zod refs +172,js-express +2,cs-dapper +2. Those are REF-ROW counts, and nothing here can add a ref row — this change touches a symbol column and two resolver predicates. Re-run in an isolated worktree pinned at HEAD: green, md5 unchanged. The delta belonged to a sibling lane's extractor work. Recorded because "measured in a contended tree" is not a measurement.Inspect binds, not deltas — and here the usual sampling is not the strongest thing available. The 53 newly-bound same-directory refs and the 19 un-demoted type refs are not merely plausible; the parity suite asserts every ref row of the package leg is now identical to the builtin leg's on every column, including
target_idand the decidingresolved_byrule. The new binds are, row for row, binds the builtin already makes on the same bytes — already insidebaseline.json's 2853 resolved and already underprecision_gate. There is no bind here that was not already graded.Mutations RUN, with real RED
The gate itself (
crates/core/src/lang_profile.rs,cp-snapshotted, restored, md5-verified43a5e459678163651c57b2953596dee0after each):The first mutation was GREEN on the first attempt, and that is a finding about the test, not about the fix.
AGREEMENT_TABLEheldcom.example.x/rubyxandcom.example.x/ruby_ish—rubyas a PREFIX — and nothing withrubyas a SUFFIX.GLOB '*ruby'matches strings that END inruby, so the widened pattern passed. Addingcom.example.x/myruby(andcom.example.ruby/xaml, for a pattern anchored on the wrong side of the separator) is what makes it red, and the row carries that history in its comment.The three sites are graded jointly and causally by the pre-fix parity run quoted at the top: reverting the keying at all three restores
(1260, 876, 53, 19, 4).What is new
crates/core/src/lang_profile.rs— the gate, the three tables, five unit tests.crates/indexer/tests/lang_profile_parity.rs— the two spellings are executed against a real SQLite over a fixed table of ids, because GLOB's greedy*is a claim SQLite should settle rather than a comment; plus the two restatements (SEP,PROFILE_NAMES) graded againstcode_index_package::langid, sincecode-index-coredepends on no workspace crate and cannot import them.sql_identifier_registries_cannot_carry_an_injectionextended to the two new registries — and to GLOB metacharacters, which are a second escape the originalsafe()predicate happened to exclude without saying it was on purpose. A profile name carrying*would over-match rows of OTHER languages without ever breaking a quote.langid.rs's module doc, which asserted the opposite stance ("language-keyed tables … simply miss for a package language, which is the CORRECT outcome") — corrected in place, with what that "miss" actually cost.Two things deliberately NOT done
No manifest field, no capability, no migration. The declaration is the local id, and adding a
[[languages]] profile = "…"key on top would moveextraction_identityfor every shipped package to express something the id already expresses. If a future language wants a profile whose name differs from its local id, that is the change to make then, and it will have a reason.javascriptis inPROFILE_NAMESeven though no plugin owns it, for the reasonBUILTIN_LANGUAGESgives — andthe_restatements_match_the_originalsfails if the two lists ever diverge, so a builtin added without a profile is a language whose semantics no package could ever inherit, and that is now a red test rather than a silent narrowing. #112, one level up.🤖 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 #167ruby_package_parityis RED at integration HEAD: #134's tier-3 origin gate reads a manifest relation the packaged leg structurally cannot have #167Triage 2026-09-06: CLOSING. One keying gate, three sites, and the three delta populations measured to zero with the corpus actually mounted.
Verified against master; landed in
efccc89.The mechanism — keying, not lang-blindness
crates/core/src/lang_profile.rs:PROFILE_NAMES(:117),profile_name(lang) -> Option<&'static str>(:235),sql_adopts(column, profile) -> String(:280),sql_adopts_any(...)(:289); exported atcrates/core/src/lib.rs:10.The change at each of the three sites is
s.lang = 'ruby'→sql_adopts("s.lang", "ruby"). All three tables and their rationale stay in place. That answers this issue's own first comment, which argued site 3's language keying is deliberate and must not be made lang-blind — it was not overridden, it was left intact and only the keying moved. It also fails closed for an unrecognised local id, which is the direction that cannot invent bindings.The measurement — and the corpus really ran
executed=1, notexecuted=0 unavailable=1. That distinction matters more than usual on this repo — a corpus-bearing suite run withoutCOSI_CORPUS_DIRreports zero executed and passes, and has produced false greens before. 153 real files were staged and both producers ran.The three populations this issue is about are gone:
QualifiedNameLangGate1260 → 0,SameDirLangGate53 → 0,TypePositionLangGate19 → 0. The one pinned delta left isSelfQualifier: 876, which is #86's fact-ABI gap (the wire carries a qualifier span, never a string) and out of scope here.lang_profile_parity(5 tests) executes both spellings against real SQLite rather than comparing strings, so the gate grades behaviour and not a transcription.Residual
None for this issue.
SelfQualifierbelongs to #86 and stays there.🤖 Triage lane, 2026-09-06, master
45cf6e4check_renamerefuses a Ruby predicate/bang name it indexes happily:new_name must be a plain identifierrejectsfoo?#295