A packaged language is invisible to every host table keyed on a BUILTIN language id — three shipped sites, found by #84 axis C #112

Closed
opened 2026-09-04 15:53:07 +02:00 by buildagent · 3 comments
Member

Found by #84 phase 4 axis C (crates/indexer/tests/ruby_package_parity.rs), comparing crates/plugins/src/ruby.rs against tests/packages/ruby over 156 ruby-sinatra files. 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::langid makes that non-colliding with a builtin id by construction: LanguageId::builtin requires ^[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:

site what is lost rows
crates/indexer/src/writer.rs::qualified_name_separator — match lang { "rust" | "ruby" => "::", … _ => None } every symbol of every packaged language gets qualified_name = NULL 1260 of 1260
crates/indexer/src/index.rs tier-1b, st.lang IN ('php', 'ruby', 'csharp') (I023 importless same-directory arm) resolved_by::TIER1B_SAME_DIRECTORY never fires for a package 53 refs lost
crates/indexer/src/index.rs::position_type_pairs_sql / code_index_core::kinds::POSITION_TYPE_KINDS_BY_LANG the member_access re-kind exemption never applies, so type refs are demoted 19 refs re-kinded

1 is the loudest

qualified_name is not on the fact wire at all — code_index_abi::record::Symbol has 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:

javascript is a real files.lang value … Its absence here gave EVERY javascript symbol in every index a NULL qualified_name from m0004 until m0042 — and that is not a cosmetic field, it is what tier 1Q and TYPE_FQN join on.

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 ruby is not necessarily Ruby. Each site needs a product decision about what a manifest may declare:

  • a qualified-name separator (site 1);
  • that the language is "importless", i.e. that same-directory proximity is admissible evidence for a method call (site 2);
  • which symbol kinds may legitimately appear in type position (site 3).

All three are ABI/manifest surface, all three are inside extraction_identity if added, and none is expressible today.

Status

Pinned, not fixed. ruby_package_parity.rs pins each population by a predicate on the row — for the qualified_name delta 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.

Found by #84 phase 4 axis C (`crates/indexer/tests/ruby_package_parity.rs`), comparing `crates/plugins/src/ruby.rs` against `tests/packages/ruby` over 156 `ruby-sinatra` files. 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::langid` makes that non-colliding with a builtin id **by construction**: `LanguageId::builtin` requires `^[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: | site | what is lost | rows | |---|---|---| | `crates/indexer/src/writer.rs::qualified_name_separator` — `match lang { "rust" \| "ruby" => "::", … _ => None }` | **every symbol of every packaged language gets `qualified_name = NULL`** | 1260 of 1260 | | `crates/indexer/src/index.rs` tier-1b, `st.lang IN ('php', 'ruby', 'csharp')` (I023 importless same-directory arm) | `resolved_by::TIER1B_SAME_DIRECTORY` never fires for a package | 53 refs lost | | `crates/indexer/src/index.rs::position_type_pairs_sql` / `code_index_core::kinds::POSITION_TYPE_KINDS_BY_LANG` | the `member_access` re-kind exemption never applies, so type refs are demoted | 19 refs re-kinded | ### 1 is the loudest `qualified_name` is **not on the fact wire at all** — `code_index_abi::record::Symbol` has 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: > `javascript` is a real `files.lang` value … Its absence here gave EVERY javascript symbol in every index a NULL `qualified_name` from m0004 until m0042 — and that is not a cosmetic field, it is what tier 1Q and TYPE_FQN join on. 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 `ruby` is not necessarily Ruby. Each site needs a product decision about what a manifest may declare: * a qualified-name separator (site 1); * that the language is "importless", i.e. that same-directory proximity is admissible evidence for a method call (site 2); * which symbol kinds may legitimately appear in type position (site 3). All three are ABI/manifest surface, all three are inside `extraction_identity` if added, and none is expressible today. ## Status Pinned, not fixed. `ruby_package_parity.rs` pins each population by a **predicate on the row** — for the `qualified_name` delta 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.
Author
Member

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:

KEYED BY LANGUAGE, and that is load-bearing. module is emitted by FOUR plugins and means "type" in only ONE of them:

plugin construct is it a type?
ruby module M YES — a mixin, Ruby's interface
csharp namespace A.B no — a namespace
php namespace A no
rust mod m no

A language-blind ["module"] list therefore exempts C#/PHP/Rust NAMESPACES bound from type position — re-admitting precisely the namespace segments the v0.10.2 reclassification exists to demote. That was a live, reproducible defect … caught by a_namespace_is_never_exempted_even_in_type_position.

… Adding a row is now an explicit, reviewable claim that the language means "type" by that kind.

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 ruby means Ruby's module. 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.

## 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: > KEYED BY LANGUAGE, and that is load-bearing. `module` is emitted by FOUR plugins and means "type" in only ONE of them: > > | plugin | construct | is it a type? | > |---|---|---| > | ruby | `module M` | YES — a mixin, Ruby's interface | > | csharp | `namespace A.B` | no — a namespace | > | php | `namespace A` | no | > | rust | `mod m` | no | > > A language-blind `["module"]` list therefore exempts C#/PHP/Rust NAMESPACES bound from type position — re-admitting precisely the namespace segments the v0.10.2 reclassification exists to demote. That was a live, reproducible defect … caught by `a_namespace_is_never_exempted_even_in_type_position`. > > … Adding a row is now an explicit, reviewable claim that the language means "type" by that kind. 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 `ruby` means Ruby's `module`. 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.
Author
Member

FIXED. 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.

pub fn profile_name(lang: &str) -> Option<&'static str>   // Rust side
pub fn sql_adopts(column: &str, profile: &str) -> String  // SQL side

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_parity with the gate in place, against the pin as it stood:

thread 'the_package_and_the_builtin_project_the_same_index' panicked at
crates/indexer/tests/ruby_package_parity.rs:840:5:
the pinned delta populations moved.
  left: (0, 876, 0, 0, 4)
 right: (1260, 876, 53, 19, 4)
mechanism before after
QualifiedNameLangGate — writer::qualified_name_separator 1260 0
SameDirLangGate — tier-1b st.lang IN ('php','ruby','csharp') 53 0
TypePositionLangGate — POSITION_TYPE_KINDS_BY_LANG 19 0
SelfQualifier — #86, the fact ABI has no qualifier TEXT 876 876
EcosystemMarker — the harness's own shadow extension 4 4

All 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 rust cannot see com.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, module still means "type" in ruby and namespace in csharp/php/rust, and a_namespace_is_never_exempted_even_in_type_position still holds. What changed is that s.lang = 'ruby' became sql_adopts("s.lang", "ruby").

The two tables that were inline literals moved into lang_profile with their comments carried across verbatim — the separator table (including the javascript paragraph about m0004→m0042) and the importless list (including I037 round 2's reverted recv_unproven gate). Nothing was re-reasoned; it was relocated so one gate can read all three.

The objection this issue raises, answered

Stripping the package prefix and reusing the builtin entry is a guess: a package whose local id happens to be ruby is not necessarily Ruby.

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::parse already, 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. Choosing id = "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 by module.

Four properties hold it up:

  • It fails closed. An unrecognised local id — mylang, xaml — yields no profile, no separator, no importless arm, no exemption. Byte-for-byte what this host did before. Graded by a_language_that_names_no_profile_adopts_nothing.
  • It is bounded. All three claims act on the adopting language's OWN rows: its symbols' 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.
  • It is disclosed. confirm::Disclosure gains a language_profiles field rendered in the block an operator answers, one line per declared language:
    de.h-dv.ruby/ruby adopts ruby— qualified_name joined with::; same-directory proximity is admissible evidence for a method call; module may sit in type position
    and for a package that adopts nothing, a line that says so and says what it means. It is the three-state Measured/Unmeasured shape Displacement already uses, so plugin gc and plugin rollback — which name no package — say "not measured" rather than printing nothing.
  • It costs nothing to an existing package. No manifest field, no extraction_identity movement, no migration. A shipped package's identity does not move.

Gates

gate result
ruby_package_parity ok, pin now (876, 4)
precision_gate 7/7, phantoms=0 in every language, recall 1.000
corpus_ratchet (release, COSI_CORPUS_REQUIRE=1) ok, tests/corpus/baseline.json md5 534084b856c22566c48e386bc41ed67e unmoved
ruby_package_cost ok, band unmoved
clippy --workspace --all-targets -D warnings 0
per-file rustfmt --check 0

On 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_id and the deciding resolved_by rule. The new binds are, row for row, binds the builtin already makes on the same bytes — already inside baseline.json's 2853 resolved and already under precision_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-verified 43a5e459678163651c57b2953596dee0 after each):

── sql_adopts GLOB '*<SEP>{profile}' -> GLOB '*{profile}'
   `sql_adopts` and `profile_name` disagree for profile `ruby`.
     left: ["com.example.x/myruby", "de.h-dv.ruby/ruby", "ruby"]
    right: ["de.h-dv.ruby/ruby", "ruby"]

── profile_name adopts ANY local id (drop the PROFILE_NAMES find)
   `de.h-dv.xaml/xaml` names no host profile and must adopt nothing
     left: Some("xaml")   right: None

── sql_adopts_any joins with AND instead of OR
   the importless disjunction is not the union
     left: []   right: ["de.h-dv.ruby/ruby", "ruby"]

── drop "javascript" from PROFILE_NAMES
   every builtin language must have a profile a package can adopt …
     left:  ["csharp", "php", "python", "ruby", "rust", "typescript"]
    right:  ["csharp", "javascript", "php", "python", "ruby", "rust", "typescript"]
   AND the parity compared 4 matching rows; AGREEMENT_TABLE has five …

The first mutation was GREEN on the first attempt, and that is a finding about the test, not about the fix. AGREEMENT_TABLE held com.example.x/rubyx and com.example.x/ruby_ish — ruby as a PREFIX — and nothing with ruby as a SUFFIX. GLOB '*ruby' matches strings that END in ruby, so the widened pattern passed. Adding com.example.x/myruby (and com.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 against code_index_package::langid, since code-index-core depends on no workspace crate and cannot import them.
  • sql_identifier_registries_cannot_carry_an_injection extended to the two new registries — and to GLOB metacharacters, which are a second escape the original safe() 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 move extraction_identity for 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.

javascript is in PROFILE_NAMES even though no plugin owns it, for the reason BUILTIN_LANGUAGES gives — and the_restatements_match_the_originals fails 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

## FIXED. 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. ```rust pub fn profile_name(lang: &str) -> Option<&'static str> // Rust side pub fn sql_adopts(column: &str, profile: &str) -> String // SQL side ``` `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_parity` with the gate in place, against the pin as it stood: ``` thread 'the_package_and_the_builtin_project_the_same_index' panicked at crates/indexer/tests/ruby_package_parity.rs:840:5: the pinned delta populations moved. left: (0, 876, 0, 0, 4) right: (1260, 876, 53, 19, 4) ``` | mechanism | before | after | |---|---:|---:| | `QualifiedNameLangGate` — `writer::qualified_name_separator` | 1260 | **0** | | `SameDirLangGate` — tier-1b `st.lang IN ('php','ruby','csharp')` | 53 | **0** | | `TypePositionLangGate` — `POSITION_TYPE_KINDS_BY_LANG` | 19 | **0** | | `SelfQualifier` — #86, the fact ABI has no qualifier TEXT | 876 | 876 | | `EcosystemMarker` — the harness's own shadow extension | 4 | 4 | **All 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 `rust` cannot see `com.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, `module` still means "type" in ruby and namespace in csharp/php/rust, and `a_namespace_is_never_exempted_even_in_type_position` still holds. What changed is that `s.lang = 'ruby'` became `sql_adopts("s.lang", "ruby")`. The two tables that were inline literals moved into `lang_profile` **with their comments carried across verbatim** — the separator table (including the javascript paragraph about m0004→m0042) and the importless list (including I037 round 2's reverted `recv_unproven` gate). Nothing was re-reasoned; it was relocated so one gate can read all three. ### The objection this issue raises, answered > Stripping the package prefix and reusing the builtin entry is a **guess**: a package whose local id happens to be `ruby` is not necessarily Ruby. 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::parse` already, 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. Choosing `id = "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 by `module`.* Four properties hold it up: - **It fails closed.** An unrecognised local id — `mylang`, `xaml` — yields no profile, no separator, no importless arm, no exemption. Byte-for-byte what this host did before. Graded by `a_language_that_names_no_profile_adopts_nothing`. - **It is bounded.** All three claims act on the adopting language's OWN rows: its symbols' `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. - **It is disclosed.** `confirm::Disclosure` gains a `language_profiles` field rendered in the block an operator answers, one line per declared language: `de.h-dv.ruby/ruby adopts `ruby` — qualified_name joined with `::`; same-directory proximity is admissible evidence for a method call; `module` may sit in type position` and for a package that adopts nothing, a line that says so and says what it means. It is the three-state `Measured`/`Unmeasured` shape `Displacement` already uses, so `plugin gc` and `plugin rollback` — which name no package — say "not measured" rather than printing nothing. - **It costs nothing to an existing package.** No manifest field, no `extraction_identity` movement, no migration. A shipped package's identity does not move. ### Gates | gate | result | |---|---| | `ruby_package_parity` | **ok**, pin now `(876, 4)` | | `precision_gate` | **7/7, `phantoms=0` in every language**, recall 1.000 | | `corpus_ratchet` (release, `COSI_CORPUS_REQUIRE=1`) | **ok**, `tests/corpus/baseline.json` md5 `534084b856c22566c48e386bc41ed67e` **unmoved** | | `ruby_package_cost` | ok, band unmoved | | `clippy --workspace --all-targets -D warnings` | 0 | | per-file `rustfmt --check` | 0 | **On 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_id` and the deciding `resolved_by` rule**. The new binds are, row for row, binds the builtin already makes on the same bytes — already inside `baseline.json`'s 2853 resolved and already under `precision_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-verified `43a5e459678163651c57b2953596dee0` after each): ``` ── sql_adopts GLOB '*<SEP>{profile}' -> GLOB '*{profile}' `sql_adopts` and `profile_name` disagree for profile `ruby`. left: ["com.example.x/myruby", "de.h-dv.ruby/ruby", "ruby"] right: ["de.h-dv.ruby/ruby", "ruby"] ── profile_name adopts ANY local id (drop the PROFILE_NAMES find) `de.h-dv.xaml/xaml` names no host profile and must adopt nothing left: Some("xaml") right: None ── sql_adopts_any joins with AND instead of OR the importless disjunction is not the union left: [] right: ["de.h-dv.ruby/ruby", "ruby"] ── drop "javascript" from PROFILE_NAMES every builtin language must have a profile a package can adopt … left: ["csharp", "php", "python", "ruby", "rust", "typescript"] right: ["csharp", "javascript", "php", "python", "ruby", "rust", "typescript"] AND the parity compared 4 matching rows; AGREEMENT_TABLE has five … ``` **The first mutation was GREEN on the first attempt, and that is a finding about the test, not about the fix.** `AGREEMENT_TABLE` held `com.example.x/rubyx` and `com.example.x/ruby_ish` — `ruby` as a PREFIX — and nothing with `ruby` as a SUFFIX. `GLOB '*ruby'` matches strings that END in `ruby`, so the widened pattern passed. Adding `com.example.x/myruby` (and `com.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 against `code_index_package::langid`, since `code-index-core` depends on no workspace crate and cannot import them. - `sql_identifier_registries_cannot_carry_an_injection` extended to the two new registries — **and to GLOB metacharacters**, which are a second escape the original `safe()` 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 move `extraction_identity` for 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. **`javascript` is in `PROFILE_NAMES` even though no plugin owns it**, for the reason `BUILTIN_LANGUAGES` gives — and `the_restatements_match_the_originals` fails 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.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

Triage 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 at crates/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

$ export CARGO_INCREMENTAL=0
$ COSI_CORPUS_DIR=$HOME/.cache/cosi-corpus COSI_CORPUS_REQUIRE=1 \
    cargo test -p code-index-indexer --test ruby_package_parity --test lang_profile_parity -- --nocapture
→ 1 passed + 5 passed, 0 failed, EXIT 0

corpus[ruby_package_parity]: executed=1 unavailable=0 not_applicable=0 controls=4 (require=true)
#84 axis C — pinned deltas
  SelfQualifier: 876
controls: 153 files staged from ~/.cache/cosi-corpus/ruby-sinatra;
          producers builtin/ruby vs de.h-dv.ruby/ruby; builtin 2857 / package 2837 resolved

executed=1, not executed=0 unavailable=1. That distinction matters more than usual on this repo — a corpus-bearing suite run without COSI_CORPUS_DIR reports 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: QualifiedNameLangGate 1260 → 0, SameDirLangGate 53 → 0, TypePositionLangGate 19 → 0. The one pinned delta left is SelfQualifier: 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. SelfQualifier belongs to #86 and stays there.

🤖 Triage lane, 2026-09-06, master 45cf6e4

## Triage 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 at `crates/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 ``` $ export CARGO_INCREMENTAL=0 $ COSI_CORPUS_DIR=$HOME/.cache/cosi-corpus COSI_CORPUS_REQUIRE=1 \ cargo test -p code-index-indexer --test ruby_package_parity --test lang_profile_parity -- --nocapture → 1 passed + 5 passed, 0 failed, EXIT 0 corpus[ruby_package_parity]: executed=1 unavailable=0 not_applicable=0 controls=4 (require=true) #84 axis C — pinned deltas SelfQualifier: 876 controls: 153 files staged from ~/.cache/cosi-corpus/ruby-sinatra; producers builtin/ruby vs de.h-dv.ruby/ruby; builtin 2857 / package 2837 resolved ``` **`executed=1`, not `executed=0 unavailable=1`.** That distinction matters more than usual on this repo — a corpus-bearing suite run without `COSI_CORPUS_DIR` reports 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**: `QualifiedNameLangGate` 1260 → 0, `SameDirLangGate` 53 → 0, `TypePositionLangGate` 19 → 0. The one pinned delta left is `SelfQualifier: 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. `SelfQualifier` belongs to **#86** and stays there. 🤖 Triage lane, 2026-09-06, master `45cf6e4`
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#112
No description provided.