Packages cannot see inside a multi-language file: one grammar per package, no tree-sitter injection #275

Open
opened 2026-09-15 09:30:43 +02:00 by buildagent · 4 comments
Member

STATUS: DESIGN SETTLED AND CORRECTED — not yet implemented

The original report is kept verbatim below the divider; the comments cite it by section. What changed, and where:

the substrate ALREADY EXISTS and is unexercised — file_contributions carries region_start/region_end with the region in its UNIQUE key, and its own comment says single-owner "today". MEASURED: 0 files with >1 contribution, all 888 rows at region_start = 0.
a correction to that comment refs.lang and imports.lang DO exist since m0044 — I asserted the opposite from one call site. This makes the design simpler, not harder.
the design the guest emits the range, the host owns the language. Not option 1 (a second grammar per package) and not a host-side injections.scm engine. A new optional fact tag, following TAG_SYMBOL_BASENAME (#229/#268): the guest says "bytes [a,b) are language L", and the language is named by INDEX into a host-owned table, never a string.
scope foundation + ONE real consumer: the delegated region goes to the compiled-in typescript/javascript extractor and is proven on .svelte. Delegating to another package's sandboxed extractor is OUT of v1 — and that exclusion is enforced by the TYPE, not by discipline.
what a six-lane review changed the corrections comment — read it before implementing. Summarised below.

THE FOUR SOCKETS THIS FEATURE FILLS

This is not a feature being bolted on. The tree was shaped for it, in four places, and three of them are currently pinned by tests asserting they are UNUSED:

  1. file_contributions.region_start/region_end, region in the UNIQUE key, "single-owner today".
  2. Three reason codes that exist and have never had a producer — fact.language_not_owned, fact.region_out_of_range, fact.region_depth_exceeded — with reason_producers.rs::PRODUCERLESS pinning that they have none.
  3. EMBEDDED_DISPATCH_SEMANTICS_VERSION, sitting at 0, whose doc is a specification for this exact change: "The day a <script> block inside HTML is dispatched to a second producer, the same bytes extract differently and every index built under the old semantics is stale."
  4. Per-row languages: symbols.lang since m0001, refs.lang/imports.lang since m0044.

None of that licenses "it mostly works already". Every socket is untested by construction. A change filling them must edit the tests that pin them empty, in the same commit.

WHAT THE REVIEW CHANGED — do not implement the first spec as written

Four items are BLOCK-grade:

  • Attribute delegated rows to the DELEGATING PACKAGE's key, not to builtin. Routing them under builtin/<lang> reaches the outcome packages.rs:1898 explicitly refuses ("it gains the whole authority model"), through a door that refusal does not watch. Minting under de.h-dv.svelte/typescript keeps the refusal holding, gives the rows the package's OWN operator-granted capabilities, closes the "indistinguishable from real TypeScript" hazard for free, and dissolves three of the four open questions.
  • Hazard #1 in the original spec is FALSE. Folding delegated symbols into the file-level outline hash is the wrong fix for the wrong problem: adds already survive a Scoped pass, the real hazard is REMOVE/RENAME, and folding in would push half to two-thirds of .svelte edits onto a Full resolve — measured at 400x the median Scoped pass on this repository. Use a PER-CONTRIBUTION outline hash instead.
  • The charge is on the wrong quantity. 64 one-byte regions at EOF cost 64 x file_size, ~209,715x wire amplification — the identical mistake already recorded as a run mutation in span_extent_amplification.rs. Charge the padded buffer, and bound the host parse: the builtin arm has no fuel, no epoch and no timeout.
  • Delegated symbol VISIBILITY is a phantom factory, and it is in none of the ten hazards. A <script> with no export yields Unknown-visibility symbols that enter cross-file candidate pools; precision_gate structurally cannot see it (JavaScript forbid_sites is 1). The record needs a third field: what SCOPE the region is.

And one prerequisite: nothing in this repository can measure this feature's cost. The pinned corpus holds zero .svelte files and cost-baseline.json installs no packages. A svelte_package_cost.rs is a prerequisite, not a follow-up.

BLOCKED ON, and blocking

  • #278 should land FIRST — read_file_claim's group_concat already takes a pathological plan and this feature would hand it more rows. Fixing it first is what makes any cost number here attributable (the #189 lesson).
  • This issue is what would restore the cross-directory component bind that #268 currently asserts as a ceiling. That test is written as an ASSERTION so it goes red the day this lands.


Original report (superseded 2026-09-15)

Kept verbatim: the comments on this issue cite it by section.

Found while reviewing #268 (Svelte package). Recorded as a HOST limit rather than a Svelte problem, because it is one.

The limit, measured

One grammar per package. crates/package/src/manifest.rs:142 declares:

pub grammar: Grammar,

Not Option<Grammar>, not Vec<Grammar>. A package names exactly one [grammar] block with one artifact and one exported_name, so a claimed file is parsed by one grammar and produces one tree.

No tree-sitter language injection anywhere in the tree. A search_text for injection across the workspace returns 37 hits and every one is fault injection, flag injection or SQL injection. There is no concept of a secondary grammar parsing a sub-range of a file.

Why that is a real gap

A large and growing class of source files is multi-language BY CONSTRUCTION — the embedded language is not an edge case, it is where the code lives:

format template embedded
.svelte Svelte markup TS/JS in <script>, CSS in <style>
.vue Vue SFC template TS/JS in <script>, CSS in <style>
.astro Astro markup TS in frontmatter fence
.mdx Markdown JSX/TS
.razor / .cshtml Razor markup C# in @{ }

For all of these, the community tree-sitter grammar parses the template and exposes the embedded block as raw text; editors recover the inner structure with an INJECTION query that runs a second grammar over that range. We have no equivalent, so an extractor receives an opaque token where the functions, imports and declarations are.

Concretely for #268: four of its eight planned extraction targets — props (export let, let { x } = $props()), runes ($state, $derived), script functions, and imports — are all inside <script>.

Why the XAML precedent does not cover it

tests/packages/xaml bridges into C# with scope = "paired_file", and that works because the C# is a SEPARATE FILE (MainWindow.xaml + MainWindow.xaml.cs), already indexed by the compiled-in C# plugin. The bridge joins two files.

In an SFC the second language is in the SAME file. There is no paired file to bridge to and no row to point at, so paired_file, same_directory and same_file all have nothing on the other end. crates/package/src/bridge.rs's evidence_for_scope admits only those three scopes.

What this does NOT claim

That injection is the only possible answer, or that it is cheap. The host runs guests in a sandboxed WASM worker with a per-package grammar loaded by plugin-host; a second grammar per file is a real cost in memory, in the kind-id tables each extractor compiles against (ruby_kind_table.rs pins 351 kind ids and 33 field ids for ONE grammar), and in the fact-ABI, which currently has no way to say "this fact came from a sub-range parsed by another grammar".

It also does not claim any file is currently WRONG. Nothing mis-indexes today; these formats are simply text-only, which is honest. This is about what a package is ABLE to express.

Options, none obviously right

  1. Injection in the host. A package declares a secondary grammar plus a range query; the host parses the sub-range and hands the guest both trees. Most general, largest change — touches manifest schema, plugin-host, the fact ABI and the digest (a second grammar is a second artifact to pin reproducibly).
  2. Sub-range delegation to an existing plugin. Rather than a second grammar in the package, the host routes the embedded range to whichever plugin already claims that language, and stamps the resulting facts with the outer file's path. Reuses the builtin TS/JS extractors; needs a way to express "these facts belong to a range of another file".
  3. A same_file bridge against facts the outer grammar emits as opaque. Cheapest, and weakest: the package emits a coarse ref for the script block and bridges within the file. Buys little — there is nothing structured on the other side.
  4. Leave it, and say so. Multi-language formats get template-side facts only, and _prdoc/guides/80-package-authoring.md gains a section stating the limit so the next package author designs around it instead of into it. Defensible; #268 can ship a useful v0.1.0 on this basis.

Option 4 is worth doing REGARDLESS of which of 1–3 is chosen, because the guide's silence is what let #268 be specified with script-block extraction in its milestones.

Scope note

Pre-existing; not introduced by #268, which is only the first package to run into it. No package in the tree is affected today — xaml (paired file), timeline (single language) and ruby (single language) all sit inside the limit.

# STATUS: DESIGN SETTLED AND CORRECTED — not yet implemented The original report is kept verbatim below the divider; the comments cite it by section. What changed, and where: | | | | :-- | :-- | | the substrate | [ALREADY EXISTS and is unexercised](https://git.h-dv.de/h-dv/code-index/issues/275#issuecomment-8451) — `file_contributions` carries `region_start`/`region_end` with the region in its UNIQUE key, and its own comment says single-owner **"today"**. MEASURED: 0 files with >1 contribution, all 888 rows at `region_start = 0`. | | a correction to that comment | [`refs.lang` and `imports.lang` DO exist](https://git.h-dv.de/h-dv/code-index/issues/275#issuecomment-8453) since m0044 — I asserted the opposite from one call site. This makes the design simpler, not harder. | | the design | **the guest emits the range, the host owns the language.** Not option 1 (a second grammar per package) and not a host-side `injections.scm` engine. A new optional fact tag, following `TAG_SYMBOL_BASENAME` (#229/#268): the guest says "bytes [a,b) are language L", and the language is named by INDEX into a host-owned table, never a string. | | scope | foundation + ONE real consumer: the delegated region goes to the compiled-in typescript/javascript extractor and is proven on `.svelte`. Delegating to another package's sandboxed extractor is OUT of v1 — and that exclusion is enforced by the TYPE, not by discipline. | | what a six-lane review changed | [the corrections comment](https://git.h-dv.de/h-dv/code-index/issues/275#issuecomment-8456) — **read it before implementing**. Summarised below. | ## THE FOUR SOCKETS THIS FEATURE FILLS This is not a feature being bolted on. The tree was shaped for it, in four places, and three of them are currently pinned by tests asserting they are UNUSED: 1. `file_contributions.region_start`/`region_end`, region in the UNIQUE key, "single-owner **today**". 2. **Three reason codes that exist and have never had a producer** — `fact.language_not_owned`, `fact.region_out_of_range`, `fact.region_depth_exceeded` — with `reason_producers.rs::PRODUCERLESS` pinning that they have none. 3. `EMBEDDED_DISPATCH_SEMANTICS_VERSION`, sitting at `0`, whose doc is a specification for this exact change: *"The day a `<script>` block inside HTML is dispatched to a second producer, the same bytes extract differently and every index built under the old semantics is stale."* 4. Per-row languages: `symbols.lang` since m0001, `refs.lang`/`imports.lang` since m0044. None of that licenses "it mostly works already". Every socket is untested by construction. A change filling them must edit the tests that pin them empty, in the same commit. ## WHAT THE REVIEW CHANGED — do not implement the first spec as written Four items are BLOCK-grade: * **Attribute delegated rows to the DELEGATING PACKAGE's key, not to `builtin`.** Routing them under `builtin/<lang>` reaches the outcome `packages.rs:1898` explicitly refuses ("it gains the whole authority model"), through a door that refusal does not watch. Minting under `de.h-dv.svelte/typescript` keeps the refusal holding, gives the rows the package's OWN operator-granted capabilities, closes the "indistinguishable from real TypeScript" hazard for free, and **dissolves three of the four open questions**. * **Hazard #1 in the original spec is FALSE.** Folding delegated symbols into the file-level outline hash is the wrong fix for the wrong problem: adds already survive a Scoped pass, the real hazard is REMOVE/RENAME, and folding in would push half to two-thirds of `.svelte` edits onto a Full resolve — measured at **400x the median Scoped pass** on this repository. Use a PER-CONTRIBUTION outline hash instead. * **The charge is on the wrong quantity.** 64 one-byte regions at EOF cost 64 x file_size, ~209,715x wire amplification — the identical mistake already recorded as a run mutation in `span_extent_amplification.rs`. Charge the padded buffer, and bound the host parse: the builtin arm has no fuel, no epoch and no timeout. * **Delegated symbol VISIBILITY is a phantom factory**, and it is in none of the ten hazards. A `<script>` with no `export` yields Unknown-visibility symbols that enter cross-file candidate pools; `precision_gate` structurally cannot see it (JavaScript `forbid_sites` is 1). The record needs a third field: what SCOPE the region is. And one prerequisite: **nothing in this repository can measure this feature's cost.** The pinned corpus holds zero `.svelte` files and `cost-baseline.json` installs no packages. A `svelte_package_cost.rs` is a prerequisite, not a follow-up. ## BLOCKED ON, and blocking * **#278** should land FIRST — `read_file_claim`'s `group_concat` already takes a pathological plan and this feature would hand it more rows. Fixing it first is what makes any cost number here attributable (the #189 lesson). * This issue is what would restore the cross-directory component bind that **#268** currently asserts as a ceiling. That test is written as an ASSERTION so it goes red the day this lands. --- --- # Original report (superseded 2026-09-15) *Kept verbatim: the comments on this issue cite it by section.* Found while reviewing #268 (Svelte package). Recorded as a HOST limit rather than a Svelte problem, because it is one. ## The limit, measured **One grammar per package.** `crates/package/src/manifest.rs:142` declares: ```rust pub grammar: Grammar, ``` Not `Option<Grammar>`, not `Vec<Grammar>`. A package names exactly one `[grammar]` block with one `artifact` and one `exported_name`, so a claimed file is parsed by one grammar and produces one tree. **No tree-sitter language injection anywhere in the tree.** A `search_text` for `injection` across the workspace returns 37 hits and every one is fault injection, flag injection or SQL injection. There is no concept of a secondary grammar parsing a sub-range of a file. ## Why that is a real gap A large and growing class of source files is multi-language BY CONSTRUCTION — the embedded language is not an edge case, it is where the code lives: | format | template | embedded | | :-- | :-- | :-- | | `.svelte` | Svelte markup | TS/JS in `<script>`, CSS in `<style>` | | `.vue` | Vue SFC template | TS/JS in `<script>`, CSS in `<style>` | | `.astro` | Astro markup | TS in frontmatter fence | | `.mdx` | Markdown | JSX/TS | | `.razor` / `.cshtml` | Razor markup | C# in `@{ }` | For all of these, the community tree-sitter grammar parses the template and exposes the embedded block as raw text; editors recover the inner structure with an INJECTION query that runs a second grammar over that range. We have no equivalent, so an extractor receives an opaque token where the functions, imports and declarations are. Concretely for #268: four of its eight planned extraction targets — props (`export let`, `let { x } = $props()`), runes (`$state`, `$derived`), script functions, and imports — are all inside `<script>`. ## Why the XAML precedent does not cover it `tests/packages/xaml` bridges into C# with `scope = "paired_file"`, and that works because the C# is a SEPARATE FILE (`MainWindow.xaml` + `MainWindow.xaml.cs`), already indexed by the compiled-in C# plugin. The bridge joins two files. In an SFC the second language is in the SAME file. There is no paired file to bridge to and no row to point at, so `paired_file`, `same_directory` and `same_file` all have nothing on the other end. `crates/package/src/bridge.rs`'s `evidence_for_scope` admits only those three scopes. ## What this does NOT claim That injection is the only possible answer, or that it is cheap. The host runs guests in a sandboxed WASM worker with a per-package grammar loaded by `plugin-host`; a second grammar per file is a real cost in memory, in the kind-id tables each extractor compiles against (`ruby_kind_table.rs` pins 351 kind ids and 33 field ids for ONE grammar), and in the fact-ABI, which currently has no way to say "this fact came from a sub-range parsed by another grammar". It also does not claim any file is currently WRONG. Nothing mis-indexes today; these formats are simply text-only, which is honest. This is about what a package is ABLE to express. ## Options, none obviously right 1. **Injection in the host.** A package declares a secondary grammar plus a range query; the host parses the sub-range and hands the guest both trees. Most general, largest change — touches manifest schema, `plugin-host`, the fact ABI and the digest (a second grammar is a second artifact to pin reproducibly). 2. **Sub-range delegation to an existing plugin.** Rather than a second grammar in the package, the host routes the embedded range to whichever plugin already claims that language, and stamps the resulting facts with the outer file's path. Reuses the builtin TS/JS extractors; needs a way to express "these facts belong to a range of another file". 3. **A `same_file` bridge against facts the outer grammar emits as opaque.** Cheapest, and weakest: the package emits a coarse ref for the script block and bridges within the file. Buys little — there is nothing structured on the other side. 4. **Leave it, and say so.** Multi-language formats get template-side facts only, and `_prdoc/guides/80-package-authoring.md` gains a section stating the limit so the next package author designs around it instead of into it. Defensible; #268 can ship a useful v0.1.0 on this basis. Option 4 is worth doing REGARDLESS of which of 1–3 is chosen, because the guide's silence is what let #268 be specified with script-block extraction in its milestones. ## Scope note Pre-existing; not introduced by #268, which is only the first package to run into it. No package in the tree is affected today — `xaml` (paired file), `timeline` (single language) and `ruby` (single language) all sit inside the limit.
Author
Member

Confirmed from the grammar side, independently

The issue above argues this limit from the HOST: one pub grammar: Grammar per package, no injection anywhere in the tree. That is an argument about what we cannot consume.

The grammars themselves make the same statement about what they PRODUCE, and they were written by people who have never seen this codebase. Measured on tree-sitter-svelte-ng 1.0.2 (crate sha256 ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37), queries/injections.scm:

((raw_text) @injection.content
  (#set! injection.language "javascript"))

((script_element
  (start_tag (attribute (attribute_name) @_attr
    (quoted_attribute_value (attribute_value) @_lang))))
  (raw_text) @injection.content)
  (#eq? @_attr "lang") (#any-of? @_lang "ts" "typescript")
  (#set! injection.language "typescript"))

((style_element … (#any-of? @_lang "scss" "postcss" "less")
  (#set! injection.language "scss"))

The entire <script> body is ONE raw_text node. The grammar ships an injection query because that is the only way anything sees inside it. tree-sitter-svelte-next 0.1.1 is a separate fork by a different author and does the same thing, mapping script to typescript and style to css.

Corroborating measurement from node-types.json, which separates what the template grammar really models from what it defers:

construct node types where it lives
{#snippet …} 11 template — modelled
{@render …} 4 template — modelled
$props 0 script block — deferred to injection
$state 0 script block — deferred to injection
$derived 0 script block — deferred to injection

Svelte 5's runes score zero not because the grammar is behind — it models Svelte 5's template additions fully — but because runes are JavaScript expressions and the grammar correctly declines to re-implement a JavaScript parser.

Why this strengthens the case rather than just restating it

Two independent sources agreeing matters more than either alone:

  • If only the host said it, the fix might be "our packages are shaped wrong".
  • If only the grammar said it, the fix might be "pick a different grammar".

Both say it, so neither is available. Every maintained tree-sitter grammar for a multi-language format draws this boundary the same way, because an injection query IS the ecosystem's answer to embedded languages. A host that cannot run one cannot reach the embedded half of ANY of these formats, no matter which grammar is pinned.

That also gives option 2 (sub-range delegation) a concrete shape it did not have when this issue was filed: the grammars already SHIP the range queries, as queries/injections.scm, naming both the capture range and the target language. The mapping from injection.language to an installed plugin is a lookup we could already perform against BUILTIN_CLAIMS. What is missing is a way to run the query and to attribute the resulting facts to a sub-range of the outer file — not a way to know which range or which language.

Scope note, unchanged

Nothing mis-indexes today; .svelte and its siblings are text-only, which is honest. This remains about what a package is ABLE to express.

## Confirmed from the grammar side, independently The issue above argues this limit from the HOST: one `pub grammar: Grammar` per package, no injection anywhere in the tree. That is an argument about what we cannot consume. The grammars themselves make the same statement about what they PRODUCE, and they were written by people who have never seen this codebase. Measured on `tree-sitter-svelte-ng` 1.0.2 (crate sha256 `ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37`), `queries/injections.scm`: ```scheme ((raw_text) @injection.content (#set! injection.language "javascript")) ((script_element (start_tag (attribute (attribute_name) @_attr (quoted_attribute_value (attribute_value) @_lang)))) (raw_text) @injection.content) (#eq? @_attr "lang") (#any-of? @_lang "ts" "typescript") (#set! injection.language "typescript")) ((style_element … (#any-of? @_lang "scss" "postcss" "less") (#set! injection.language "scss")) ``` The entire `<script>` body is ONE `raw_text` node. The grammar ships an injection query because that is the only way anything sees inside it. `tree-sitter-svelte-next` 0.1.1 is a separate fork by a different author and does the same thing, mapping script to `typescript` and style to `css`. Corroborating measurement from `node-types.json`, which separates what the template grammar really models from what it defers: | construct | node types | where it lives | | :-- | --: | :-- | | `{#snippet …}` | 11 | template — modelled | | `{@render …}` | 4 | template — modelled | | `$props` | 0 | script block — deferred to injection | | `$state` | 0 | script block — deferred to injection | | `$derived` | 0 | script block — deferred to injection | Svelte 5's runes score zero not because the grammar is behind — it models Svelte 5's template additions fully — but because runes are JavaScript expressions and the grammar correctly declines to re-implement a JavaScript parser. ## Why this strengthens the case rather than just restating it Two independent sources agreeing matters more than either alone: * If only the host said it, the fix might be "our packages are shaped wrong". * If only the grammar said it, the fix might be "pick a different grammar". Both say it, so neither is available. Every maintained tree-sitter grammar for a multi-language format draws this boundary the same way, because an injection query IS the ecosystem's answer to embedded languages. A host that cannot run one cannot reach the embedded half of ANY of these formats, no matter which grammar is pinned. That also gives option 2 (sub-range delegation) a concrete shape it did not have when this issue was filed: the grammars already SHIP the range queries, as `queries/injections.scm`, naming both the capture range and the target language. The mapping from `injection.language` to an installed plugin is a lookup we could already perform against `BUILTIN_CLAIMS`. What is missing is a way to run the query and to attribute the resulting facts to a sub-range of the outer file — not a way to know which range or which language. ## Scope note, unchanged Nothing mis-indexes today; `.svelte` and its siblings are text-only, which is honest. This remains about what a package is ABLE to express.
Author
Member

The substrate for this already exists, and has never been exercised

Grounding before implementation turned up something the issue as filed did not know: the schema was designed for sub-range contributions and is waiting for a caller.

file_contributions (m0043, producer_identity):

CREATE TABLE IF NOT EXISTS file_contributions (
    file_id       INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
    component_id  INTEGER NOT NULL REFERENCES extraction_components(id) ON DELETE CASCADE,
    generation_id INTEGER NOT NULL REFERENCES plugin_generations(id) ON DELETE CASCADE,
    -- Byte range of the file this contribution covers. Routing is
    -- single-owner today, so every row is [0, files.size).
    region_start  INTEGER NOT NULL,
    region_end    INTEGER NOT NULL,
    UNIQUE (file_id, component_id, generation_id, region_start)
);

region_start is IN THE UNIQUENESS KEY, and the comment says single-owner "today". Several other pieces line up the same way:

  • symbols.lang is per ROW, not per file — a .svelte file can already hold typescript symbols.
  • refs has no lang column and does not need one. A ref's language arrives through refs.contribution_id → file_contributions.component_id → extraction_components.lang; fill_bridge_refs already joins exactly that way. So a second contribution in another language stamps its refs correctly for free.
  • The generation machinery, the four denormalisation triggers and the cascade all key on contribution_id, so a second contribution inherits the whole lifecycle.

And it is unexercised, which is the part to be careful about

Measured against this repository's own live index:

files with >1 contribution:  0
region_start values in use:  0  (all 888 rows)

Every contribution in a real database sits at [0, size). So this is a designed-for capability with no caller, no test and no production evidence — the exact shape where latent defects live. Nothing here should be read as "it already works"; the claim is only that the storage model does not have to change.

Decided design: the guest emits the range, the host owns the language

Rather than the issue's option 1 (a second grammar per package) or a host-side query engine, the range comes from a new OPTIONAL FACT TAG, following the pattern TAG_SYMBOL_BASENAME just established in #229/#268:

the guest emits an annotation naming something the host then acts on, and no string a package controls crosses the boundary.

The Svelte extractor already knows the script range — its grammar parsed the <script> boundary and hands it back as one raw_text node. It does not need to parse the contents, and the host does not need to run injections.scm. The guest says "bytes [a, b) are language L"; the host routes that region to whichever component claims L, offsets the spans, and records a second file_contribution over exactly that region.

The language is named by INDEX into a host-owned table, not by string. This is the rule de.h-dv.ruby 0.5.0 records for REL_QUALIFIERS: "The wire carries a u16 INDEX into a table the HOST owns, so this package cannot express a qualifier the host did not write down, and there is no authority left for an operator to grant. A value that cannot be minted needs no permission to mint." A package must not be able to delegate a region to an arbitrary language it names itself.

Scope of the first change

Foundation plus one real consumer. The delegated region goes to the existing COMPILED-IN typescript/javascript extractors — mature, already trusted, already the correct answer for a .svelte script block — and it is proven end to end on .svelte. Delegating to another PACKAGE's sandboxed extractor is deferred: grants, sandbox budgets and digests would all have to compose across two packages, and none of that is needed to make the capability real.

Building the foundation with synthetic tests and no consumer was considered and rejected on this repository's own rule — a gate with no demand behind it is the shape that shipped 15k lines with zero production callers.

What this does NOT promise

That the cross-directory component bind from #268 comes back automatically. It should: the import becomes a fact, and a_component_tag_does_not_bind_across_directories_and_that_is_the_ceiling is written as an ASSERTION so it goes red the day that changes. But the resolver tiers have not been examined for it, and #268's ceiling comment will be re-earned by measurement rather than assumed.

## The substrate for this already exists, and has never been exercised Grounding before implementation turned up something the issue as filed did not know: **the schema was designed for sub-range contributions and is waiting for a caller.** `file_contributions` (m0043, `producer_identity`): ```sql CREATE TABLE IF NOT EXISTS file_contributions ( file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, component_id INTEGER NOT NULL REFERENCES extraction_components(id) ON DELETE CASCADE, generation_id INTEGER NOT NULL REFERENCES plugin_generations(id) ON DELETE CASCADE, -- Byte range of the file this contribution covers. Routing is -- single-owner today, so every row is [0, files.size). region_start INTEGER NOT NULL, region_end INTEGER NOT NULL, UNIQUE (file_id, component_id, generation_id, region_start) ); ``` `region_start` is IN THE UNIQUENESS KEY, and the comment says single-owner **"today"**. Several other pieces line up the same way: * **`symbols.lang` is per ROW**, not per file — a `.svelte` file can already hold `typescript` symbols. * **`refs` has no `lang` column and does not need one.** A ref's language arrives through `refs.contribution_id → file_contributions.component_id → extraction_components.lang`; `fill_bridge_refs` already joins exactly that way. So a second contribution in another language stamps its refs correctly for free. * The generation machinery, the four denormalisation triggers and the cascade all key on `contribution_id`, so a second contribution inherits the whole lifecycle. ### And it is unexercised, which is the part to be careful about Measured against this repository's own live index: ``` files with >1 contribution: 0 region_start values in use: 0 (all 888 rows) ``` Every contribution in a real database sits at `[0, size)`. So this is a designed-for capability with **no caller, no test and no production evidence** — the exact shape where latent defects live. Nothing here should be read as "it already works"; the claim is only that the storage model does not have to change. ## Decided design: the guest emits the range, the host owns the language Rather than the issue's option 1 (a second grammar per package) or a host-side query engine, the range comes from a new OPTIONAL FACT TAG, following the pattern `TAG_SYMBOL_BASENAME` just established in #229/#268: > the guest emits an annotation naming something the host then acts on, and no string a package controls crosses the boundary. The Svelte extractor **already knows the script range** — its grammar parsed the `<script>` boundary and hands it back as one `raw_text` node. It does not need to parse the contents, and the host does not need to run `injections.scm`. The guest says "bytes [a, b) are language L"; the host routes that region to whichever component claims L, offsets the spans, and records a second `file_contribution` over exactly that region. **The language is named by INDEX into a host-owned table, not by string.** This is the rule `de.h-dv.ruby` 0.5.0 records for `REL_QUALIFIERS`: "The wire carries a u16 INDEX into a table the HOST owns, so this package cannot express a qualifier the host did not write down, and there is no authority left for an operator to grant. A value that cannot be minted needs no permission to mint." A package must not be able to delegate a region to an arbitrary language it names itself. ### Scope of the first change **Foundation plus one real consumer.** The delegated region goes to the existing COMPILED-IN typescript/javascript extractors — mature, already trusted, already the correct answer for a `.svelte` script block — and it is proven end to end on `.svelte`. Delegating to another PACKAGE's sandboxed extractor is deferred: grants, sandbox budgets and digests would all have to compose across two packages, and none of that is needed to make the capability real. Building the foundation with synthetic tests and no consumer was considered and rejected on this repository's own rule — a gate with no demand behind it is the shape that shipped 15k lines with zero production callers. ### What this does NOT promise That the cross-directory component bind from #268 comes back automatically. It should: the import becomes a fact, and `a_component_tag_does_not_bind_across_directories_and_that_is_the_ceiling` is written as an ASSERTION so it goes red the day that changes. But the resolver tiers have not been examined for it, and #268's ceiling comment will be re-earned by measurement rather than assumed.
Author
Member

Correction, and the extent of the anticipation

One claim in my previous comment is WRONG

I wrote: "refs has no lang column and does not need one. A ref's language arrives through refs.contribution_id → file_contributions.component_id → extraction_components.lang."

refs.lang exists. So does imports.lang. Both since m0044 (crates/indexer/src/migrations/m0044_row_language.rs). Measured:

$ sqlite3 .code-index/index.db "pragma table_info(refs)"   # col 14
14|lang|TEXT|1|''|0

The resolver splices refs.lang directly (crates/indexer/src/index.rs ~4245, row_lang), with the files join only as a pre-m0044 fallback. The component→lang path I described is the bridge path specifically (fill_bridge_refs), which is where I read it from and over-generalised.

This makes the design simpler, not harder: the delegated pass passes its language straight to writer::insert_refs, which already takes lang: &str. No new column, no join change, no inference. But the correction matters on its own terms — I asserted a schema fact from one call site, and one call site is not the schema.

The codebase anticipated this feature in FOUR places, not one

My previous comment named the file_contributions regions. There are three more, and together they change how this should be read — this is not a feature being bolted on, it is a socket being filled.

1. The region columns, as reported: region_start/region_end, region in the UNIQUE key, comment says single-owner "today".

2. THREE REASON CODES ALREADY EXIST FOR THIS AND HAVE NEVER HAD A PRODUCER (crates/abi/src/reason.rs:211-215):

fact.language_not_owned
fact.region_out_of_range
fact.region_depth_exceeded

And crates/abi/tests/reason_producers.rs::PRODUCERLESS pins that they have none — so that test goes red on this change and is edited as part of it, which is the gate confirming the socket is being filled rather than a new one cut.

3. EMBEDDED_DISPATCH_SEMANTICS_VERSION (crates/indexer/src/activation.rs:94) is already folded into the activation identity, sitting at 0, and its doc is a specification for this exact change:

ZERO MEANS WHOLE-FILE SINGLE-OWNER ROUTING: classify_walked sends each file to exactly one producer and every file_contributions row covers [0, files.size). The day a <script> block inside HTML is dispatched to a second producer, the same bytes extract differently and every index built under the old semantics is stale — which is a change of identity, not a change of package.

That constant exists to be spent on this, and spending it is what makes every existing index correctly re-activate.

4. Per-row languages — symbols.lang since m0001, refs.lang/imports.lang since m0044 — so a file holding two languages needs no schema change at all.

What that does and does not license

It does NOT license "this mostly works already". The measurement stands: 0 files with more than one contribution, all 888 rows at region_start = 0. Every one of these sockets is untested by construction, and three of them are pinned by tests asserting they are unused.

What it does license is confidence that the design is the one the tree was shaped for, rather than one imposed on it — and a concrete obligation: a change that fills these sockets must edit the tests that pin them empty, in the same commit, or those tests are the ones that quietly stop meaning anything.

One hazard the spec surfaced that is worth stating here

m0044's backfill is UPDATE refs SET lang = (SELECT f.lang FROM files f WHERE f.id = refs.file_id) — unconditional, idempotent only under the very invariant delegation breaks. It cannot re-run in normal operation, but upgrade_equivalence and contribution_generation_key::strip_to_v50 deliberately roll versions BACK. A rollback reaching 44 would relanguage every delegated row to the file's route language, and the fresh-vs-migrated projection comparison would then differ. The fix is a repair migration ordered after 44, on the same "a rollback that reaches 51 always reaches 52" argument m0052 already makes — not an edit to a shipped migration.

## Correction, and the extent of the anticipation ### One claim in my previous comment is WRONG I wrote: *"`refs` has no `lang` column and does not need one. A ref's language arrives through `refs.contribution_id → file_contributions.component_id → extraction_components.lang`."* **`refs.lang` exists.** So does `imports.lang`. Both since **m0044** (`crates/indexer/src/migrations/m0044_row_language.rs`). Measured: ``` $ sqlite3 .code-index/index.db "pragma table_info(refs)" # col 14 14|lang|TEXT|1|''|0 ``` The resolver splices `refs.lang` directly (`crates/indexer/src/index.rs` ~4245, `row_lang`), with the `files` join only as a pre-m0044 fallback. The component→lang path I described is the **bridge** path specifically (`fill_bridge_refs`), which is where I read it from and over-generalised. This makes the design **simpler**, not harder: the delegated pass passes its language straight to `writer::insert_refs`, which already takes `lang: &str`. No new column, no join change, no inference. But the correction matters on its own terms — I asserted a schema fact from one call site, and one call site is not the schema. ### The codebase anticipated this feature in FOUR places, not one My previous comment named the `file_contributions` regions. There are three more, and together they change how this should be read — this is not a feature being bolted on, it is a socket being filled. **1. The region columns**, as reported: `region_start`/`region_end`, region in the UNIQUE key, comment says single-owner *"today"*. **2. THREE REASON CODES ALREADY EXIST FOR THIS AND HAVE NEVER HAD A PRODUCER** (`crates/abi/src/reason.rs:211-215`): ``` fact.language_not_owned fact.region_out_of_range fact.region_depth_exceeded ``` And `crates/abi/tests/reason_producers.rs::PRODUCERLESS` **pins that they have none** — so that test goes red on this change and is edited as part of it, which is the gate confirming the socket is being filled rather than a new one cut. **3. `EMBEDDED_DISPATCH_SEMANTICS_VERSION`** (`crates/indexer/src/activation.rs:94`) is already folded into the activation identity, sitting at `0`, and its doc is a specification for this exact change: > ZERO MEANS WHOLE-FILE SINGLE-OWNER ROUTING: `classify_walked` sends each file to exactly one producer and every `file_contributions` row covers `[0, files.size)`. **The day a `<script>` block inside HTML is dispatched to a second producer**, the same bytes extract differently and every index built under the old semantics is stale — which is a change of identity, not a change of package. That constant exists to be spent on this, and spending it is what makes every existing index correctly re-activate. **4. Per-row languages** — `symbols.lang` since m0001, `refs.lang`/`imports.lang` since m0044 — so a file holding two languages needs no schema change at all. ### What that does and does not license It does NOT license "this mostly works already". The measurement stands: **0 files with more than one contribution, all 888 rows at `region_start = 0`.** Every one of these sockets is untested by construction, and three of them are pinned by tests asserting they are *unused*. What it does license is confidence that the design is the one the tree was shaped for, rather than one imposed on it — and a concrete obligation: a change that fills these sockets must edit the tests that pin them empty, in the same commit, or those tests are the ones that quietly stop meaning anything. ### One hazard the spec surfaced that is worth stating here `m0044`'s backfill is `UPDATE refs SET lang = (SELECT f.lang FROM files f WHERE f.id = refs.file_id)` — **unconditional**, idempotent only under the very invariant delegation breaks. It cannot re-run in normal operation, but `upgrade_equivalence` and `contribution_generation_key::strip_to_v50` deliberately roll versions BACK. A rollback reaching 44 would relanguage every delegated row to the file's route language, and the fresh-vs-migrated projection comparison would then differ. The fix is a repair migration ordered after 44, on the same "a rollback that reaches 51 always reaches 52" argument m0052 already makes — not an edit to a shipped migration.
Author
Member

Six-lane adversarial review — the design changes before implementation

The mechanism is right. Three things must change, one listed hazard is false, and one prerequisite is missing entirely.

1. Attribute delegated rows to the DELEGATING PACKAGE, not to builtin

crates/indexer/src/packages.rs:1898-1926 already refuses, as IndexerError::Integrity, any package claim resolving to a reserved key:

a package that gets a reserved key past this line does not gain one capability, it gains the whole authority model.

Routing delegated rows under builtin/typescript reaches that exact outcome through a door the refusal does not watch. Verified: is_reserved_key has three production call sites (packages.rs:1921, packages.rs:2171, activation.rs:499) and all three are on the grant/identity side. The three contribution writers (writer.rs:306, writer.rs:463, build.rs:1377) call ensure_component with no check at all. The only thing standing between a package and grant_all today is that the component is derived from the file's own language — an accident of single-owner routing, not a guard, and delegation is precisely what removes it.

Mint the component under the delegating package's key — de.h-dv.svelte/typescript, lang = "typescript". extraction_components is already UNIQUE (package_id, component) with its own lang column, so:

  • symbols.lang/refs.lang/imports.lang still read typescript; the resolver splices exactly as designed
  • is_reserved_key is false, grant_all never fires, and the existing refusal keeps holding
  • rows carry the package's own operator-granted resolver list — a package with resolver = [] gets searchable-but-inert delegated rows, which is the documented correct default
  • open questions 1, 3 and 4 disappear: no new capability key, no manifest field, no extraction_identity move, no approval-store field, no schema.v1.json change
  • hazard #8 closes for free — component_id becomes the discriminator. Under the original design a delegated row joins the same builtin/typescript component as real .ts files and only region_start <> 0 tells them apart.

Correction to my earlier comment while I am here: it is eight pool capabilities, not four (RESOLVER_CAPABILITIES, manifest.rs:64-98).

2. HAZARD #1 IS FALSE, and the proposed fix would be expensive

The spec says delegated symbols MUST be folded into compute_outline_hash or "a function added to a <script> is never resolvable from another file."

Measured, that does not follow for ADD. writer.rs:409-412 puts the file in resolve_scope_files gated on outline_hash.is_some() — not on outline_changed — and the Scoped predicate (index.rs:3967-3974) carries a project-wide NAME arm, with the candidate pool built unscoped from the whole symbols table. A new symbol is a candidate on a Scoped pass exactly as on a Full one.

The real hazard is REMOVE / RENAME, and it is not in the spec. Delete a function from a <script>: hash unchanged ⇒ Scoped; write_upsert deletes the files row; refs.target_id … ON DELETE SET NULL NULLs every cross-file ref that pointed at it; the scope name arm asks whether that name is a symbol in the scope file, which it no longer is. Those refs sit outside the Scoped population and stay unresolved, with a latched stale resolved_by, until an unrelated change forces a Full pass. This silently falsifies influence.rs:157-168 ("any symbol added or removed … forces ResolveScope::Full").

And folding in would cost. typescript.rs's emit_var puts the initializer into the hashed signature via signature_until_body, so $state(0) → $state(1) and any $derived(expr) tweak would move the hash. Measured on this repository — 888 files, 226,116 refs — a Scoped pass touches a median of 565 refs (0.250%), max 38,765 (17.1%): the firewall is worth 400x on the median edit and 5.8x on the worst. index.rs:724-728 records 17 ms → 17.9 s (~1,050x) on rust-analyzer when one delete forced a full re-decide. Folding delegations into the file-level hash would push roughly half to two-thirds of .svelte edits onto the Full path.

Do this instead: a PER-CONTRIBUTION outline_hash and outline_changed. file_contributions already keys on (file_id, component_id, generation_id, region_start) and m0054_promotion_classification.rs:92-99 already carries new_outline_hash/prior_outline_hash. That restores the influence.rs invariant without making an initializer edit global.

3. The charge is on the wrong quantity

The spec charges region_end - region_start; the allocation is the padded buffer, bounded by region_end — i.e. by the FILE. 64 one-byte regions at EOF: ~640 wire bytes, 64 x file_size allocated and parsed. At the 2 MiB default that is 128 MiB per file, ~1.0 s of pure tree-sitter; at the 1 GiB operator ceiling, 64 GiB.

This is the identical mistake crates/abi/tests/span_extent_amplification.rs:88-94 already records as a RUN mutation ("charge the span's START instead of its extent — every span starts at 0, so the charge is 0").

It also relocates attacker-steered work to the unbounded arm. index.rs:10769-10783 states the asymmetry: the package arm is bounded by fuel, epoch, memory ceiling and the parent's kill; the builtin arm "parses in this process, on this thread". Measured: set_timeout_micros and set_cancellation_flag have zero hits tree-wide. Charge the padded buffer length, and add a per-file max_delegated_bytes ceiling.

4. Delegated symbol VISIBILITY is a phantom factory — not in the ten hazards

TypeScriptPlugin::extract decides module semantics from the bytes handed to it: is_esm = any top-level export_statement, else collect_cjs_exports, else the plugin's own comment — "those stay Unknown so the resolver never gates them". Unknown passes the cross-file visibility gate.

A Svelte <script> with no export — an ordinary component with no props — therefore yields top-level symbols with Unknown visibility, in a language the resolver treats as ordinary TypeScript. A helper named format in Foo.svelte becomes a cross-file candidate for every unresolved format in every .ts file in the project. Svelte component scripts are not importable by name, except <script module> — exactly the distinction a (start, end, lang) triple cannot express.

No gate here can see this. precision_gate never indexes a corpus repo and scores phantoms only against DECLARED decoys; JavaScript's forbid_sites is 1.

The record needs a third field: what SCOPE the region is (module vs local). .vue <script> vs <script setup>, .astro frontmatter and .razor @code all ask the same question, so it belongs in the record rather than in the Svelte consumer.

5. Prerequisite: nothing in this repository can measure the cost

Measured: the pinned corpus is 9 repos and contains zero .svelte files; cost-baseline.json installs no packages (plugin_path_cost.rs says so: "the pinned corpus installs no packages, so the guest path never executes under it"). Ship #275 as specified and it is cost-unmeasured by construction.

A .svelte fixture tree plus a svelte_package_cost.rs modelled on ruby_package_cost.rs — both legs, wall_ratio_pct with a ceiling AND a floor — is a prerequisite, not a follow-up.

Smaller corrections

  • Hazard #9 is wrong: only TWO of the three reason codes gain a producer. The non-overlap rule refuses nesting outright, so FactRegionDepthExceeded stays producerless and its rationale in reason_producers.rs must be REWRITTEN rather than deleted.
  • The activation-gate fix is under-keyed. AND afc.region_start = fc.region_start is not a contribution's identity; add AND afc.component_id = fc.component_id. It works today only because a <script> can never start at byte 0 — an accident, not a structural fact.
  • write_upsert's container rule becomes a cross-contribution write. CONTAINER_SCOPE_FILE is refs.file_id = ?1 AND s.file_id = ?1; its sibling write_pending_contribution already documents why that is wrong with two contributions. The svelte module symbol spans the whole file, so a component in test.svelte would mark every delegated TypeScript ref as test code. Move to CONTAINER_SCOPE_CONTRIBUTION.
  • Open question 2 settles AGAINST reuse. FactLanguageNotOwned's declared meaning is claim-level ("a fact claimed a language the dispatched component does not own"). The FactRelQualifierUnknown precedent cuts FOR a new code: an index outside a closed registry gets its own refusal, because "degrading there would let a producer choose between two host behaviours by picking an index". Mint FactRegionLanguageUnknown.
  • conform.rs is a separate door and the spec never mentions it. conform.rs:107-115 already says where this lands: "When embedded dispatch ships, its comparison belongs in expect::grade." host.extract( appears in exactly two places, so plugin check --repo does not pass through extract_for and gets no delegation unless someone puts it there.
  • DELEGABLE_LANGUAGES is the feature's entire population and nothing grades its size. Adding "css" later would silently change extraction semantics for every existing .svelte index. Pin the table's contents beside the version so widening it without bumping is red.
  • The padded-input trick is right, but for a different reason than the spec gives. It credits PosMap; measured, typescript.rs has FOUR sites computing node.start_position().row + 1 directly, bypassing PosMap (484, 1165, 1290, 1408), plus one each in five other plugins. It works because tree-sitter itself counts the synthetic rows. Record that, or the obvious "simplify to a PosMap origin offset" refactor silently breaks four fields.
  • A claimed "units bug" (padding byte-vs-codepoint columns) was raised and refuted: the padding is ASCII, where one byte is one codepoint, so (C-1) spaces put region_start at codepoint column C. The design is correct as written.

Strength worth recording

"Delegating to another package is out of v1" is enforced by the type, not by discipline: select_plugin(path, &[Arc<dyn LanguagePlugin>]) can only return a compiled-in plugin, and packages reach the indexer through PackageHost::extract, a different trait with one call site. A synthetic .ts path cannot reach a package claiming .ts. That is the structural clause this repo asks for and it is already true — say so in the spec.

One alternative the spec should record and reject in writing

Eight of the ten listed hazards are consequences of ONE shape change — two contributions per file. That suggests a fifth option nobody named: declare the region as a virtual sub-file (Foo.svelte#script.ts) routed through ordinary classify_walked/select_plugin. files.lang stays honest, contributions stay one-per-file, and the outline hash, container rule, coverage DTO and read_file_claim all keep their single-producer shapes. It is probably disqualified — a files row with no bytes on disk, no mtime, and an index_coverage answer for a path nobody can open, against freshness machinery that keys on real stat triples — but "eight hazards are one shape change" is the strongest argument an implementer will meet, and the spec currently has no written answer to it.

## Six-lane adversarial review — the design changes before implementation The mechanism is right. Three things must change, one listed hazard is **false**, and one prerequisite is missing entirely. ### 1. Attribute delegated rows to the DELEGATING PACKAGE, not to `builtin` `crates/indexer/src/packages.rs:1898-1926` already refuses, as `IndexerError::Integrity`, any package claim resolving to a reserved key: > a package that gets a reserved key past this line does not gain one capability, it gains the whole authority model. Routing delegated rows under `builtin/typescript` reaches that exact outcome through a door the refusal does not watch. Verified: `is_reserved_key` has **three** production call sites (`packages.rs:1921`, `packages.rs:2171`, `activation.rs:499`) and **all three are on the grant/identity side**. The three contribution writers (`writer.rs:306`, `writer.rs:463`, `build.rs:1377`) call `ensure_component` with no check at all. The only thing standing between a package and `grant_all` today is that the component is derived from the file's own language — an accident of single-owner routing, not a guard, and delegation is precisely what removes it. **Mint the component under the delegating package's key** — `de.h-dv.svelte/typescript`, `lang = "typescript"`. `extraction_components` is already `UNIQUE (package_id, component)` with its own `lang` column, so: * `symbols.lang`/`refs.lang`/`imports.lang` still read `typescript`; the resolver splices exactly as designed * `is_reserved_key` is false, `grant_all` never fires, and the existing refusal keeps holding * rows carry **the package's own operator-granted `resolver` list** — a package with `resolver = []` gets searchable-but-inert delegated rows, which is the documented correct default * **open questions 1, 3 and 4 disappear**: no new capability key, no manifest field, no `extraction_identity` move, no approval-store field, no `schema.v1.json` change * hazard #8 closes for free — `component_id` becomes the discriminator. Under the original design a delegated row joins the *same* `builtin/typescript` component as real `.ts` files and only `region_start <> 0` tells them apart. Correction to my earlier comment while I am here: it is **eight** pool capabilities, not four (`RESOLVER_CAPABILITIES`, `manifest.rs:64-98`). ### 2. HAZARD #1 IS FALSE, and the proposed fix would be expensive The spec says delegated symbols MUST be folded into `compute_outline_hash` or "a function added to a `<script>` is never resolvable from another file." **Measured, that does not follow for ADD.** `writer.rs:409-412` puts the file in `resolve_scope_files` gated on `outline_hash.is_some()` — **not** on `outline_changed` — and the Scoped predicate (`index.rs:3967-3974`) carries a project-wide NAME arm, with the candidate pool built unscoped from the whole `symbols` table. A new symbol is a candidate on a Scoped pass exactly as on a Full one. **The real hazard is REMOVE / RENAME, and it is not in the spec.** Delete a function from a `<script>`: hash unchanged ⇒ Scoped; `write_upsert` deletes the `files` row; `refs.target_id … ON DELETE SET NULL` NULLs every cross-file ref that pointed at it; the scope name arm asks whether that name is a symbol *in the scope file*, which it no longer is. Those refs sit outside the Scoped population and stay unresolved, with a latched stale `resolved_by`, until an unrelated change forces a Full pass. This silently falsifies `influence.rs:157-168` ("any symbol added or removed … forces `ResolveScope::Full`"). **And folding in would cost.** `typescript.rs`'s `emit_var` puts the initializer into the hashed signature via `signature_until_body`, so `$state(0)` → `$state(1)` and any `$derived(expr)` tweak would move the hash. Measured on this repository — 888 files, 226,116 refs — a Scoped pass touches a **median of 565 refs (0.250%)**, max 38,765 (17.1%): the firewall is worth **400x on the median edit** and 5.8x on the worst. `index.rs:724-728` records 17 ms → 17.9 s (~1,050x) on rust-analyzer when one delete forced a full re-decide. Folding delegations into the file-level hash would push roughly half to two-thirds of `.svelte` edits onto the Full path. **Do this instead: a PER-CONTRIBUTION `outline_hash` and `outline_changed`.** `file_contributions` already keys on `(file_id, component_id, generation_id, region_start)` and `m0054_promotion_classification.rs:92-99` already carries `new_outline_hash`/`prior_outline_hash`. That restores the `influence.rs` invariant without making an initializer edit global. ### 3. The charge is on the wrong quantity The spec charges `region_end - region_start`; the allocation is the padded buffer, bounded by `region_end` — i.e. by the FILE. **64 one-byte regions at EOF: ~640 wire bytes, 64 x file_size allocated and parsed.** At the 2 MiB default that is 128 MiB per file, ~1.0 s of pure tree-sitter; at the 1 GiB operator ceiling, 64 GiB. This is the identical mistake `crates/abi/tests/span_extent_amplification.rs:88-94` already records as a RUN mutation ("charge the span's START instead of its extent — every span starts at 0, so the charge is 0"). It also relocates attacker-steered work to the **unbounded** arm. `index.rs:10769-10783` states the asymmetry: the package arm is bounded by fuel, epoch, memory ceiling and the parent's kill; the builtin arm "parses in this process, on this thread". Measured: `set_timeout_micros` and `set_cancellation_flag` have **zero hits tree-wide**. Charge the padded buffer length, and add a per-file `max_delegated_bytes` ceiling. ### 4. Delegated symbol VISIBILITY is a phantom factory — not in the ten hazards `TypeScriptPlugin::extract` decides module semantics from the bytes handed to it: `is_esm` = any top-level `export_statement`, else `collect_cjs_exports`, else the plugin's own comment — *"those stay Unknown so the resolver never gates them"*. Unknown passes the cross-file visibility gate. A Svelte `<script>` with no `export` — an ordinary component with no props — therefore yields top-level symbols with **Unknown** visibility, in a language the resolver treats as ordinary TypeScript. A helper named `format` in `Foo.svelte` becomes a cross-file candidate for every unresolved `format` in every `.ts` file in the project. Svelte component scripts are **not** importable by name, except `<script module>` — exactly the distinction a `(start, end, lang)` triple cannot express. **No gate here can see this.** `precision_gate` never indexes a corpus repo and scores phantoms only against DECLARED decoys; JavaScript's `forbid_sites` is **1**. **The record needs a third field: what SCOPE the region is** (module vs local). `.vue` `<script>` vs `<script setup>`, `.astro` frontmatter and `.razor` `@code` all ask the same question, so it belongs in the record rather than in the Svelte consumer. ### 5. Prerequisite: nothing in this repository can measure the cost Measured: the pinned corpus is 9 repos and contains **zero** `.svelte` files; `cost-baseline.json` installs no packages (`plugin_path_cost.rs` says so: "the pinned corpus installs no packages, so the guest path never executes under it"). Ship #275 as specified and it is cost-unmeasured **by construction**. A `.svelte` fixture tree plus a `svelte_package_cost.rs` modelled on `ruby_package_cost.rs` — both legs, `wall_ratio_pct` with a ceiling AND a floor — is a prerequisite, not a follow-up. ### Smaller corrections * **Hazard #9 is wrong: only TWO of the three reason codes gain a producer.** The non-overlap rule refuses nesting outright, so `FactRegionDepthExceeded` stays producerless and its rationale in `reason_producers.rs` must be REWRITTEN rather than deleted. * **The activation-gate fix is under-keyed.** `AND afc.region_start = fc.region_start` is not a contribution's identity; add `AND afc.component_id = fc.component_id`. It works today only because a `<script>` can never start at byte 0 — an accident, not a structural fact. * **`write_upsert`'s container rule becomes a cross-contribution write.** `CONTAINER_SCOPE_FILE` is `refs.file_id = ?1 AND s.file_id = ?1`; its sibling `write_pending_contribution` already documents why that is wrong with two contributions. The svelte module symbol spans the whole file, so a component in `test.svelte` would mark every delegated TypeScript ref as test code. Move to `CONTAINER_SCOPE_CONTRIBUTION`. * **Open question 2 settles AGAINST reuse.** `FactLanguageNotOwned`'s declared meaning is claim-level ("a fact claimed a language the dispatched component does not own"). The `FactRelQualifierUnknown` precedent cuts FOR a new code: an index outside a closed registry gets its own refusal, because "degrading there would let a producer choose between two host behaviours by picking an index". Mint `FactRegionLanguageUnknown`. * **`conform.rs` is a separate door and the spec never mentions it.** `conform.rs:107-115` already says where this lands: *"When embedded dispatch ships, its comparison belongs in `expect::grade`."* `host.extract(` appears in exactly two places, so `plugin check --repo` does not pass through `extract_for` and gets no delegation unless someone puts it there. * **`DELEGABLE_LANGUAGES` is the feature's entire population and nothing grades its size.** Adding `"css"` later would silently change extraction semantics for every existing `.svelte` index. Pin the table's contents beside the version so widening it without bumping is red. * **The padded-input trick is right, but for a different reason than the spec gives.** It credits `PosMap`; measured, `typescript.rs` has FOUR sites computing `node.start_position().row + 1` directly, bypassing `PosMap` (484, 1165, 1290, 1408), plus one each in five other plugins. It works because **tree-sitter itself counts the synthetic rows**. Record that, or the obvious "simplify to a PosMap origin offset" refactor silently breaks four fields. * A claimed "units bug" (padding byte-vs-codepoint columns) was raised and **refuted**: the padding is ASCII, where one byte is one codepoint, so `(C-1)` spaces put `region_start` at codepoint column `C`. The design is correct as written. ### Strength worth recording "Delegating to another package is out of v1" is enforced by the **type**, not by discipline: `select_plugin(path, &[Arc<dyn LanguagePlugin>])` can only return a compiled-in plugin, and packages reach the indexer through `PackageHost::extract`, a different trait with one call site. A synthetic `.ts` path cannot reach a package claiming `.ts`. That is the structural clause this repo asks for and it is already true — say so in the spec. ### One alternative the spec should record and reject in writing Eight of the ten listed hazards are consequences of ONE shape change — two contributions per file. That suggests a fifth option nobody named: declare the region as a **virtual sub-file** (`Foo.svelte#script.ts`) routed through ordinary `classify_walked`/`select_plugin`. `files.lang` stays honest, contributions stay one-per-file, and the outline hash, container rule, coverage DTO and `read_file_claim` all keep their single-producer shapes. It is probably disqualified — a `files` row with no bytes on disk, no mtime, and an `index_coverage` answer for a path nobody can open, against freshness machinery that keys on real stat triples — but "eight hazards are one shape change" is the strongest argument an implementer will meet, and the spec currently has no written answer to it.
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#275
No description provided.