Frameworks do not generalise: claims are basename-only, and bridges have no containment constraint, no project-wide scope and no type-driven join #154

Open
opened 2026-09-05 12:55:41 +02:00 by buildagent · 1 comment
Member

Found by a third-party-pluggability review. The owner's question was "are we on the way to a pluggable system for multiple languages and frameworks?" — languages: yes. Frameworks: two structural walls, both independent of the ABI.

Wall 1 — a framework is not a language, and the host language is taken

Rails support lives inside the Ruby extractor. Django, Spring and Angular all sit on languages a builtin owns, so .py, .ts, .cs are unavailable to a package. A third party cannot ship Django or Angular-component awareness at all today.

What they can ship is the framework's own unclaimed file type — .xaml, .vue, .erb, .html templates — bridged into the host language. That is exactly the XAML shape, and it is currently the only shape that fits. (#87's operator-consented override changes the arithmetic here; this issue is about what remains after it.)

Wall 2 — claims are basename-only

crates/package/src/claim.rs supports Ext, Name, Suffix — no directory component, and it notes that a fourth key kind "is a decision, not a convenience". So app/models/, */migrations/*.py, src/main/java/** are inexpressible.

Framework conventions are overwhelmingly directory-shaped. This is the single most load-bearing gap for the framework story. See also #135 (claim::Key has no bounded path globs, and the bounded_glob_claim fixture tests no glob).

What the two existing mechanisms actually do

derived_names is genuinely fail-closed and the design is right. The guest computes the string, nothing verifies it, and an ungranted derived ref refuses the whole file with fact.span_name_mismatch — with withheld_grant_witness re-validating with the grant to distinguish "missing grant" from "buggy span". But it is one permission bit, not a capability system: camelize/singularize live in the guest, there is no host transform registry, and Tier::Declarative is retired specifically so there is no rule surface. New inflection means new wasm.

Bridges are a name-join with three fixed scopes (same_file, paired_file, same_directory), three name maps, and ambiguity with exactly one member. The candidate side is the whole project's symbols of one kind with no containment constraint at all — in the shipped XAML e2e, Click="OnSaveClick" binds any C# method of that name in the paired file, not a member of the class x:Class names. And MAX_BRIDGES = 16 against 3×6 = 18 possible pairs, so one language pair cannot be fully covered.

Mapped concretely

framework where it hits the wall
Django ForeignKey('app.Model') fits until two apps declare Model — i.e. the normal repo
Django urls.py needs a project-wide scope that does not exist
Spring DI type-driven; implement/inherit ref kinds are declared but emitted by no plugin
Angular templates needs "member of this component class" — the identical wall that makes XAML's {Binding …} emit as un-bridgeable member_access

The three missing primitives, in blast-radius order

  1. A containment constraint on the bridge — "a member of the class this file names", not "any symbol of this kind".
  2. A project-wide scope.
  3. A type-driven join.

Plus a directory component on claims (wall 2), which is arguably prerequisite to all three.

  • #135 (bounded path globs)
  • #87 (builtin claim override)
  • #119 (embedded-region dispatch — a file carrying two producers is the adjacent case)

🤖 Generated with Claude Code

https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K

Found by a third-party-pluggability review. The owner's question was "are we on the way to a pluggable system for multiple languages **and frameworks**?" — languages: yes. Frameworks: two structural walls, both independent of the ABI. ## Wall 1 — a framework is not a language, and the host language is taken Rails support lives **inside** the Ruby extractor. Django, Spring and Angular all sit on languages a builtin owns, so `.py`, `.ts`, `.cs` are unavailable to a package. **A third party cannot ship Django or Angular-component awareness at all today.** What they *can* ship is the framework's own unclaimed file type — `.xaml`, `.vue`, `.erb`, `.html` templates — bridged into the host language. That is exactly the XAML shape, and it is currently the only shape that fits. (#87's operator-consented override changes the arithmetic here; this issue is about what remains after it.) ## Wall 2 — claims are basename-only `crates/package/src/claim.rs` supports `Ext`, `Name`, `Suffix` — **no directory component**, and it notes that a fourth key kind "is a decision, not a convenience". So `app/models/`, `*/migrations/*.py`, `src/main/java/**` are inexpressible. **Framework conventions are overwhelmingly directory-shaped.** This is the single most load-bearing gap for the framework story. See also #135 (`claim::Key` has no bounded path globs, and the `bounded_glob_claim` fixture tests no glob). ## What the two existing mechanisms actually do **`derived_names` is genuinely fail-closed and the design is right.** The guest computes the string, nothing verifies it, and an ungranted derived ref refuses the whole file with `fact.span_name_mismatch` — with `withheld_grant_witness` re-validating *with* the grant to distinguish "missing grant" from "buggy span". But it is **one permission bit, not a capability system**: `camelize`/`singularize` live in the guest, there is no host transform registry, and `Tier::Declarative` is retired specifically so there is no rule surface. New inflection means new wasm. **Bridges** are a name-join with three fixed scopes (`same_file`, `paired_file`, `same_directory`), three name maps, and `ambiguity` with exactly one member. The candidate side is **the whole project's symbols of one kind with no containment constraint at all** — in the shipped XAML e2e, `Click="OnSaveClick"` binds any C# method of that name in the paired file, not a member of the class `x:Class` names. And `MAX_BRIDGES = 16` against 3×6 = 18 possible pairs, so **one language pair cannot be fully covered**. ## Mapped concretely | framework | where it hits the wall | |---|---| | Django `ForeignKey('app.Model')` | fits until two apps declare `Model` — i.e. the normal repo | | Django `urls.py` | needs a project-wide scope that does not exist | | Spring DI | type-driven; `implement`/`inherit` ref kinds are declared but **emitted by no plugin** | | Angular templates | needs "member of *this* component class" — the identical wall that makes XAML's `{Binding …}` emit as un-bridgeable `member_access` | ## The three missing primitives, in blast-radius order 1. **A containment constraint on the bridge** — "a member of the class this file names", not "any symbol of this kind". 2. **A project-wide scope.** 3. **A type-driven join.** Plus a directory component on claims (wall 2), which is arguably prerequisite to all three. ## Related - #135 (bounded path globs) - #87 (builtin claim override) - #119 (embedded-region dispatch — a file carrying two producers is the adjacent case) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K1zj5VcFJvJt3pQxe9259K
Author
Member

Both walls verified from the code, one of the three primitives is now measured rather than asserted, and a FOURTH instance of wall 2 landed this week. Not attempted in this lane, with the reason stated.

The packaged-language lane held this today alongside #86, #119 and #124. It was not built — see the last section for why, in numbers — but every claim in the body was checked against master 4f866e5 rather than taken forward, and two of them can now be cited to a line.

Wall 2 — claims are basename-only: CONFIRMED, exactly as filed

crates/package/src/claim.rs:99-106. The enum is three variants and none of them sees a directory:

pub enum Key {
    Ext(Cow<'static, str>),            // extension, no dot, case-insensitive
    Name(Cow<'static, str>, Fold),     // the whole BASENAME
    Suffix(Cow<'static, str>, Fold),   // a BASENAME suffix
}

matches takes base and nothing else. So app/models/, */migrations/*.py and src/main/java/** are inexpressible, and the body's reading is right: framework conventions are overwhelmingly directory-shaped, so this is the most load-bearing gap for the framework story. See also #135.

Wall 2 has a FOURTH instance, and it is a HOST gate, not a manifest one

Worth adding to this issue because it is the same family arriving from a different direction — and because it is the first one that cost a suite.

manifest_package_dirs (crates/indexer/src/index.rs:1801) discovers package boundaries by BASENAME:

const MANIFESTS: &[&str] = &["Cargo.toml", "package.json", "composer.json",
                             "pyproject.toml", "setup.py", "Gemfile", "go.mod", …];
let base = path.rsplit('/').next().unwrap_or("");

That is blind to any packaged language whose files a package claims under a different extension — the same shape as #112's three language-id-keyed allowlists, and as this issue's wall 2. It surfaced as #167, where it made ruby_package_parity's two legs run the origin gate against different data (17 unexplained ref deltas, in both directions).

The product fix was implemented TWICE and refused on measurement, which is the part worth carrying here:

fallback partition parity existing fixtures
package_root_of (parent dir) green 3 RED
top-level directory green 4 RED, including one that names the admitted bind "a phantom" in so many words

And neither is measurable on the pinned corpus — all nine repos carry discoverable manifests, so the fallback never fires and the bind digest is byte-identical under both. #167 was closed with a harness fix and nearest_package_dir left carrying a doc that records both refutations.

The lesson for this issue: a directory component on claims is not only a manifest-vocabulary addition. The host already has directory-shaped reasoning, it is basename-keyed in at least one place, and there is no ground truth for the manifest-less case. Whoever takes primitive 4 should read #167's thread first; that is the same decision arriving early.

Primitive 1 — the containment constraint: CONFIRMED, and it is one SQL statement

This is the one the body states in prose and #119 now depends on. It is exact. fill_bridge_cands (crates/indexer/src/index.rs:2826-2867):

SELECT s.id, s.file_id, f.path, s.name
  FROM symbols s
  JOIN files f ON f.id = s.file_id
  <bridge_destination admit join>
 WHERE s.lang = ?1 AND s.kind = ?2

Every symbol in the project of one language and one kind. The scopes narrow it afterwards through dir and pair_key columns — and all three of same_file, paired_file, same_directory are FILE-shaped. There is no column, and no join, that could express "a member of the class this file names".

So the body's XAML observation is structural rather than incidental: Click="OnSaveClick" binds any C# method of that name in the paired file, because the relation has no way to know which class x:Class named.

MAX_BRIDGES = 16 against 3×6 = 18 possible pairs also holds (crates/package/src/limits.rs:215), as does BRIDGE_AMBIGUITY = &["unique"] — one member (crates/package/src/bridge.rs:148).

derived_names — the body's assessment holds, and it now has a shipped precedent

"Genuinely fail-closed and the design is right… but one permission bit, not a capability system" is accurate. Worth noting for whoever builds the transform registry: #103 shipped the full pattern — a requested, granted, fail-closed authority with a withheld_grant_witness that re-validates with the grant to distinguish "missing grant" from "buggy span", folded into the activation digest. #86's remaining gap 4 (qualifier TEXT) is queued to copy the same shape. A transform registry would be the third instance, so the mechanism is no longer speculative — only the vocabulary is.

Why this lane did not build any of it

Stated so the refusal is inspectable rather than just cautious.

  1. Primitive 1 is a resolver change with a coupled package landing behind it. The candidate relation is shared by every bridge, and the only end-to-end grader is the XAML package — so a containment constraint means a manifest field, a BridgeDecl field, an activation-digest bit, a rebuilt tests/packages/xaml (wasm bytes, WASM_ARTIFACTS sha, .digest, plugin.toml version, recorded expectations) and a re-blessed cost band. That is a full coupled landing, not a clause.
  2. Wall 2 is a decision this lane does not own. claim.rs says a fourth key kind "is a decision, not a convenience", and #167 has just demonstrated what happens when directory-shaped reasoning is changed without ground truth: two implementations, seven red fixtures between them, and no corpus repo that can tell them apart. #135 holds that decision.
  3. This lane's landable work went to #124 and to correcting #86 and #153, where the change was bounded and the evidence was available today.
  1. Primitive 1 (containment) — it is one relation, it unblocks #119, and it is the difference between a second producer's rows being joinable and being a name-keyed pile. Highest leverage per unit of blast radius.
  2. Wall 2 (a directory component on claims) — but only after somebody decides the manifest-less partition question #167 refuted twice, and ideally after a manifest-less corpus repo exists, which #167 names as the thing that would settle it. That is a corpus change, not a resolver change, and it is cheap.
  3. Primitives 2 and 3 (project-wide scope, type-driven join) — both need 1 first; a project-wide scope with no containment constraint is the current candidate relation with the file filter removed, which is strictly worse than what ships.

One correction to the body's related list: #112 is closed (one lang_profile mechanism, not three exceptions), and #167 is closed (harness fix; the product finding stands and is recorded in-tree).

🤖 Packaged-language lane, 2026-09-06, master 4f866e5

## Both walls verified from the code, one of the three primitives is now measured rather than asserted, and a FOURTH instance of wall 2 landed this week. **Not attempted in this lane, with the reason stated.** The packaged-language lane held this today alongside #86, #119 and #124. It was not built — see the last section for why, in numbers — but every claim in the body was checked against master `4f866e5` rather than taken forward, and two of them can now be cited to a line. ### Wall 2 — claims are basename-only: **CONFIRMED, exactly as filed** `crates/package/src/claim.rs:99-106`. The enum is three variants and none of them sees a directory: ```rust pub enum Key { Ext(Cow<'static, str>), // extension, no dot, case-insensitive Name(Cow<'static, str>, Fold), // the whole BASENAME Suffix(Cow<'static, str>, Fold), // a BASENAME suffix } ``` `matches` takes `base` and nothing else. So `app/models/`, `*/migrations/*.py` and `src/main/java/**` are inexpressible, and the body's reading is right: framework conventions are overwhelmingly directory-shaped, so this is the most load-bearing gap for the framework story. See also #135. ### Wall 2 has a FOURTH instance, and it is a HOST gate, not a manifest one Worth adding to this issue because it is the same family arriving from a different direction — and because it is the first one that cost a suite. `manifest_package_dirs` (`crates/indexer/src/index.rs:1801`) discovers package boundaries by **BASENAME**: ```rust const MANIFESTS: &[&str] = &["Cargo.toml", "package.json", "composer.json", "pyproject.toml", "setup.py", "Gemfile", "go.mod", …]; let base = path.rsplit('/').next().unwrap_or(""); ``` That is blind to any packaged language whose files a package claims under a different extension — the same shape as #112's three language-id-keyed allowlists, and as this issue's wall 2. It surfaced as **#167**, where it made `ruby_package_parity`'s two legs run the origin gate against different data (17 unexplained ref deltas, in both directions). **The product fix was implemented TWICE and refused on measurement**, which is the part worth carrying here: | fallback partition | parity | existing fixtures | |---|---|---| | `package_root_of` (parent dir) | green | **3 RED** | | top-level directory | green | **4 RED**, including one that names the admitted bind "a phantom" in so many words | And **neither is measurable on the pinned corpus** — all nine repos carry discoverable manifests, so the fallback never fires and the bind digest is byte-identical under both. #167 was closed with a **harness** fix and `nearest_package_dir` left carrying a doc that records both refutations. The lesson for this issue: **a directory component on claims is not only a manifest-vocabulary addition.** The host already has directory-shaped reasoning, it is basename-keyed in at least one place, and there is no ground truth for the manifest-less case. Whoever takes primitive 4 should read #167's thread first; that is the same decision arriving early. ### Primitive 1 — the containment constraint: **CONFIRMED, and it is one SQL statement** This is the one the body states in prose and #119 now depends on. It is exact. `fill_bridge_cands` (`crates/indexer/src/index.rs:2826-2867`): ```sql SELECT s.id, s.file_id, f.path, s.name FROM symbols s JOIN files f ON f.id = s.file_id <bridge_destination admit join> WHERE s.lang = ?1 AND s.kind = ?2 ``` **Every symbol in the project of one language and one kind.** The scopes narrow it afterwards through `dir` and `pair_key` columns — and all three of `same_file`, `paired_file`, `same_directory` are **FILE-shaped**. There is no column, and no join, that could express *"a member of the class this file names"*. So the body's XAML observation is structural rather than incidental: `Click="OnSaveClick"` binds any C# method of that name in the paired file, because the relation has no way to know which class `x:Class` named. `MAX_BRIDGES = 16` against 3×6 = 18 possible pairs also holds (`crates/package/src/limits.rs:215`), as does `BRIDGE_AMBIGUITY = &["unique"]` — one member (`crates/package/src/bridge.rs:148`). ### `derived_names` — the body's assessment holds, and it now has a shipped precedent *"Genuinely fail-closed and the design is right… but one permission bit, not a capability system"* is accurate. Worth noting for whoever builds the transform registry: **#103 shipped the full pattern** — a requested, granted, fail-closed authority with a `withheld_grant_witness` that re-validates *with* the grant to distinguish "missing grant" from "buggy span", folded into the activation digest. #86's remaining gap 4 (qualifier TEXT) is queued to copy the same shape. A transform registry would be the third instance, so the mechanism is no longer speculative — only the vocabulary is. ### Why this lane did not build any of it Stated so the refusal is inspectable rather than just cautious. 1. **Primitive 1 is a resolver change with a coupled package landing behind it.** The candidate relation is shared by every bridge, and the only end-to-end grader is the XAML package — so a containment constraint means a manifest field, a `BridgeDecl` field, an activation-digest bit, a rebuilt `tests/packages/xaml` (wasm bytes, `WASM_ARTIFACTS` sha, `.digest`, `plugin.toml` version, recorded expectations) and a re-blessed cost band. That is a full coupled landing, not a clause. 2. **Wall 2 is a decision this lane does not own.** `claim.rs` says a fourth key kind *"is a decision, not a convenience"*, and #167 has just demonstrated what happens when directory-shaped reasoning is changed without ground truth: two implementations, seven red fixtures between them, and no corpus repo that can tell them apart. #135 holds that decision. 3. **This lane's landable work went to #124 and to correcting #86 and #153**, where the change was bounded and the evidence was available today. ### Recommended sequencing, from what was measured 1. **Primitive 1 (containment)** — it is one relation, it unblocks #119, and it is the difference between a second producer's rows being joinable and being a name-keyed pile. Highest leverage per unit of blast radius. 2. **Wall 2 (a directory component on claims)** — but only after somebody decides the manifest-less partition question #167 refuted twice, and ideally after a **manifest-less corpus repo** exists, which #167 names as the thing that would settle it. That is a corpus change, not a resolver change, and it is cheap. 3. **Primitives 2 and 3 (project-wide scope, type-driven join)** — both need 1 first; a project-wide scope with no containment constraint is the current candidate relation with the file filter removed, which is strictly worse than what ships. One correction to the body's related list: **#112 is closed** (one `lang_profile` mechanism, not three exceptions), and **#167 is closed** (harness fix; the product finding stands and is recorded in-tree). 🤖 Packaged-language lane, 2026-09-06, master `4f866e5`
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#154
No description provided.