Plugin Package: Svelte & SvelteKit Support (de.h-dv.svelte) #268

Closed
opened 2026-09-12 17:49:28 +02:00 by buildagent · 3 comments
Member

SCOPE REVISED 2026-09-15 — v0.1.0 is template-side only

The original specification is kept verbatim below the divider, because the review comments on this issue cite it by section and a spec that moves under its own review is unreadable.

What changed and why: the review (#268 comment) found four blockers, three of them cheap manifest errors and one architectural. The architectural one is filed as #275: a package gets exactly one grammar and this host has no tree-sitter injection, so the contents of <script> are unreachable. That was then confirmed from the grammar side (measurements) — tree-sitter-svelte-ng exposes the entire script body as ONE raw_text node and ships queries/injections.scm because that is the only way anything sees inside it.

So v0.1.0 ships every fact the grammar actually produces, and claims nothing it cannot derive. Script-block facts land when #275 does.


v0.1.0 scope

IN — backed by real grammar nodes

fact source emitted as
component module symbol file basename (Button, +page, +layout, +error) SymbolKind::Module, Visibility::Exported
Svelte 5 snippets {#snippet row(item)} 11 node types in node-types.json SymbolKind::Function
snippet renders {@render row(x)} 4 node types RefKind::Call
component tag refs <Header />, <Modal /> capitalised element tags RefKind::Type
event directives onclick={handleClick} attribute + mustache RefKind::Call
mustache reads {data.title} mustache expression RefKind::Read
block structures {#if}, {#each}, {#await}, {#key} real block keywords scope/structure only

Component resolution is same-language: <Button /> resolves to Button.svelte through exported_candidate. That is not a bridge and must not be written as one.

OUT — deferred to #275, stated rather than implied

Everything inside <script> / <script module>:

  • props — export let x (Svelte 3/4) and let { x } = $props() (Svelte 5)
  • runes — $state, $derived, $effect (MEASURED: 0 node types each; they are JavaScript expressions, not template syntax)
  • script-local functions — function handleClick() {}
  • imports — import Button from './Button.svelte'

The package documentation MUST state this limit in the operator-facing text. A package that silently returns fewer symbols than a reader expects is the disclosure defect this project keeps finding; a package that says which half it covers is honest.

IN — *.svelte.ts / *.svelte.js (decided 2026-09-15)

Svelte 5 universal reactivity modules ship in v0.1.0. MEASURED as feasible: [claims.include] accepts suffixes as well as extensions, and path_eligibility_in falls through to select_plugin when displacing_route declines, so ordinary .ts and .js files still reach the compiled-in plugins.

It requires consent against the builtins:

[claims.include]
extensions = ["svelte"]
suffixes   = [".svelte.ts", ".svelte.js"]

# `DisplaceDecl` names the BUILTIN's own id and the keys THAT language
# loses, in the same five-list shape a claim uses -- not this package's
# vocabulary. "The compiled-in `typescript` loses `ext:ts`" is the
# sentence an operator has to answer.
[[displaces]]
builtin = "typescript"
[displaces.keys]
extensions = ["ts"]

[[displaces]]
builtin = "javascript"
[displaces.keys]
extensions = ["js"]

THE CONSENT OVERSTATES WHAT IS TAKEN, AND THAT IS ACCEPTED RATHER THAN UNNOTICED. builtin::displaced_by keys consent on the BUILTIN's whole key, so the operator confirmation reads "displaces typescript ext:ts" — "this package takes all your TypeScript" — to grant something that only ever routes *.svelte.ts. The routing is correct; the SENTENCE is wider than the behaviour.

Two consequences follow, and both are requirements on this package rather than observations:

  1. The package documentation MUST state, in operator-facing text, that it routes only *.svelte.ts / *.svelte.js and that ordinary .ts / .js continue to the built-in plugins. An operator reading only the consent prompt would conclude otherwise.
  2. svelte_package_e2e.rs MUST contain a test proving it: a project holding a.ts, b.js, c.svelte.ts and d.svelte.js, with this package enabled, indexes the first two with the BUILTIN languages and the last two with de.h-dv.svelte. A claim this easy to get wrong and this alarming when misread does not travel on prose.

Narrowing the consent surface so a package can displace an intersection rather than a whole key is a separate product question and is NOT in this issue's scope.

Unchanged from the original

The file-type table, the SvelteKit routing conventions, and the fact_major = 1 / host_min = 1 / package_format = 1 targets are all correct as originally written.


Corrected manifest (tests/packages/svelte/plugin.toml)

package_format = 1

[package]
id      = "de.h-dv.svelte"
version = "0.1.0"
license = "MIT OR Apache-2.0"

[abi]
fact_major     = 1
fact_minor_min = 0
host_min       = 1
host_max       = 1

[grammar]
# tree-sitter-svelte-ng 1.0.2 from crates.io -- NOT `tree-sitter-svelte`,
# which is the abandoned 2022 grammar and predates Svelte 5.
# MEASURED: src/parser.c:7 `#define LANGUAGE_VERSION 14`, inside this
# host's 13..=15 window and the same ABI as the ruby and xml artifacts.
artifact      = "grammar.wasm"
exported_name = "svelte"
ts_abi_min    = 13
ts_abi_max    = 15

[[languages]]
id = "svelte"

[[claims]]
language  = "svelte"
tier      = "executable"
component = "extractor.wasm"

[claims.include]
# `svelte` is NOT in BUILTIN_CLAIMS, so this is claimable outright --
# no `.rbx`-style shadow extension of the kind tests/packages/ruby needs.
extensions = ["svelte"]
# Svelte 5 universal reactivity modules. `.ts`/`.js` DO belong to
# builtins, so these two suffixes need the consent below.
suffixes = [".svelte.ts", ".svelte.js"]

[[displaces]]
builtin = "typescript"
[displaces.keys]
extensions = ["ts"]

[[displaces]]
builtin = "javascript"
[displaces.keys]
extensions = ["js"]

[capabilities]
resolver = [
  "same_file_candidate",
  "exported_candidate",
  "reachability_anchor",
  "qualified_candidate",
  "type_position_candidate",
  "member_candidate",
]

# NO BRIDGES, and that is the package saying so rather than an omission.
# `<Button />` resolves to `Button.svelte` -- the SAME language -- which
# `exported_candidate` already does. The original spec's svelte->typescript
# and svelte->javascript bridges were wrong twice over: `scope = "workspace"`
# is not a value `evidence_for_scope` admits, and a component is not a TS
# type or a JS class. Because there are no bridges, `bridge_source` is not
# requested either.

[[fixtures]]
input    = "fixtures/Simple.svelte"
expected = "fixtures/Simple.expected"

[[fixtures]]
input    = "fixtures/Runes.svelte"
expected = "fixtures/Runes.expected"

[[fixtures]]
input    = "fixtures/Broken.svelte"
expected = "fixtures/Broken.expected"

Runes.svelte stays in the fixture set even though runes are out of scope — it is the fixture that PINS the boundary. It must assert that the script block yields the component module symbol and no rune symbols, so that the day #275 lands, the change shows up as a fixture diff instead of as a silent gain.

Broken.svelte is the parse-error decoy both shipped packages carry (Broken.xaml, broken.rb.rbx) and the original spec omitted.


Revised milestones

  • M1: Grammar and scaffolding
    • tests/grammars/build-tree-sitter-svelte.sh, following build-tree-sitter-ruby.sh byte for byte: fetch the pinned .crate, sha256sum -c it, compile src/parser.c + src/scanner.c to wasm, sha256sum -c the output.
      VERSION=1.0.2, CRATE=tree-sitter-svelte-ng,
      CRATE_SHA256=ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37
    • Do NOT vendor the prebuilt tree-sitter-svelte.wasm from the npm package — grammar_provenance.rs pins WASM_ARTIFACTS by hash and a vendored binary has no recipe anyone can re-run.
    • Record the artifact in grammar_provenance.rs's WASM_ARTIFACTS.
    • Scaffold crates/guest/svelte (Cargo.toml, build.sh, kind-id table).
  • M2: Extractor — template side only
    • Component module symbol from basename, including the SvelteKit +page / +layout / +error forms.
    • {#snippet} → Function; {@render} → Call.
    • Capitalised element tags → RefKind::Type.
    • Event directives → Call; mustache reads → Read.
    • Emit NOTHING from raw_text. A <script> body is opaque at this ABI and must not be regex-scanned — see "rejected approach" below.
  • M3: Manifest and capabilities
    • The manifest above. No bridges, no bridge_source.
  • M4: Fixtures and validation
    • Simple.svelte (Svelte 3/4), Runes.svelte (Svelte 5, pinning the boundary), Broken.svelte (parse-error decoy), counter.svelte.ts (universal reactivity module), each with .expected.
    • crates/daemon/tests/svelte_package_e2e.rs, matching the xaml_package_e2e.rs convention: install, enable, then search_symbols / find_callers / file_outline / get_symbol over .svelte.
    • A test asserting <Button /> in App.svelte resolves to Button.svelte — the claim this package exists to make.
  • M5: CI and release packaging
    • Pack the Svelte language package step in release.yml.
    • Commit the canonical digest to tests/packages/svelte.digest.
    • Add the package to distribution/registry.v1.json's derived set (the catalog enumerates tests/packages/*/plugin.toml from disk, so this is automatic — confirm it, do not author it).

Revised acceptance criteria

  1. code-index plugin validate de.h-dv.svelte-0.1.0.cip passes with zero diagnostics.
  2. plugin install and plugin enable promote the package into the active generation.
  3. search_symbols finds Svelte components and snippets in .svelte files.
  4. find_references on a component resolves <Button /> usages to Button.svelte, same-language.
  5. A NEGATIVE criterion, because this package's honesty is the point: a fixture containing $state, $props and a script-local function yields NO symbols for them, and the package doc says why. Passing criterion 3 while silently missing half a file is the failure mode this replaces.
  6. Displacement routes only what it claims: a project holding a.ts, b.js, c.svelte.ts and d.svelte.js indexes the first two with the BUILT-IN typescript/javascript languages and the last two with de.h-dv.svelte. The consent prompt says the builtin loses ext:ts; this criterion is what proves the behaviour is narrower than the sentence.
  7. All CI checks pass: #![deny(warnings)], Clippy, zero test regressions.

Rejected approach, recorded so it is not re-proposed

Hand-rolling a JavaScript/TypeScript scanner in the guest to read the <script> body. It re-implements a parser and manufactures facts from a format the package cannot fully parse — the class of defect I066 spent a release removing from the distribution catalog. The grammar itself declines to do this, correctly. If the script block is worth reading, the answer is #275, not a regex.



Original specification (superseded 2026-09-15)

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

Summary & Problem Statement

Currently, code-index treats .svelte files as text-only (indexed_as: "text"). They participate in full-text search via FTS5, but are completely symbol-blind:

  1. No symbols (components, props, state, runes, snippets) are extracted.
  2. No call graph or reference resolution exists for component instantiations (<Button />, <Modal />).
  3. Agents cannot evaluate refactoring impact or blast radius for Svelte components.
  4. SvelteKit route conventions (+page.svelte, +layout.svelte, +error.svelte) and Svelte 5 universal reactivity modules (.svelte.ts, .svelte.js) lack structural intelligence.

This issue tracks the creation, verification, and distribution of the official de.h-dv.svelte dynamic WebAssembly plugin package (Option B under Epic #75 / Spec 05).


Filetype & System Scope

Based on the official Svelte 5 & SvelteKit documentation:

File Pattern Purpose / Constructs Handled By
*.svelte Svelte Single File Components (SFC): <script>, <script module>, markup, <style>, runes, snippets de.h-dv.svelte (Claims ext("svelte"))
*.svelte.ts, *.svelte.js Universal Reactivity Modules (Svelte 5 Runes outside components) Built-in TypeScript plugin + bridge (or claimed suffix with [[displaces]])
+page.svelte, +layout.svelte, +error.svelte SvelteKit route UI templates; consume PageProps / LayoutProps de.h-dv.svelte
+page.ts/.js, +page.server.ts/.js, +server.ts/.js SvelteKit universal/server load functions, form actions, API endpoints Built-in TypeScript plugin
hooks.client.ts/.js, hooks.server.ts/.js SvelteKit application middleware and routing hooks Built-in TypeScript plugin

Architectural & Package Design

The plugin will be distributed as an external, sandboxed CIP package (de.h-dv.svelte-0.1.0.cip) compliant with fact_major = 1, host_min = 1, and package_format = 1.

1. Package Manifest (tests/packages/svelte/plugin.toml)

package_format = 1

[package]
id      = "de.h-dv.svelte"
version = "0.1.0"
license = "MIT OR Apache-2.0"

[abi]
fact_major     = 1
fact_minor_min = 0
host_min       = 1
host_max       = 1

[grammar]
artifact      = "grammar.wasm"
exported_name = "svelte"
ts_abi_min    = 13
ts_abi_max    = 15

[[languages]]
id = "svelte"

[[claims]]
language  = "svelte"
tier      = "executable"
component = "extractor.wasm"

[claims.include]
extensions = ["svelte"]

[capabilities]
resolver = [
    "same_file_candidate",
    "exported_candidate",
    "reachability_anchor",
    "qualified_candidate",
    "type_position_candidate",
    "member_candidate",
]

# Bridge: Connect component usage (<Button />) to TypeScript definitions
[[capabilities.bridges]]
source_language         = "svelte"
source_ref_kind         = "type"
destination_language    = "typescript"
destination_symbol_kind = "type"
scope                   = "workspace"
name_map                = "identity"
ambiguity               = "unique"
evidence                = "conventional"

# Bridge: Connect component usage (<Button />) to JavaScript definitions
[[capabilities.bridges]]
source_language         = "svelte"
source_ref_kind         = "type"
destination_language    = "javascript"
destination_symbol_kind = "class"
scope                   = "workspace"
name_map                = "identity"
ambiguity               = "unique"
evidence                = "conventional"

[[fixtures]]
input    = "fixtures/Simple.svelte"
expected = "fixtures/Simple.expected"

2. Guest Extractor (crates/guest/svelte)

  • Implemented in Rust with #![no_std] and panic = "abort", linking code-index-guest.
  • Traverses the tree-sitter-svelte AST to emit:
    • Component Module Symbol: Derived from file basename (+page, +layout, Button).
    • Props (Svelte 3/4 & 5): export let prop and let { prop } = $props() → SymbolKind::Field, Visibility::Exported.
    • Reactivity & Runes: $state(...), $derived(...) → SymbolKind::Variable.
    • Script Functions: function handleClick() → SymbolKind::Function.
    • Svelte 5 Snippets: {#snippet row(item)} → SymbolKind::Function.
    • Component Tag References: Capitalized markup elements (<Header />, <Modal />) → RefKind::Type or RefKind::Call.
    • Bindings & Handlers: onclick={handleClick}, {data.title} → RefKind::Call / RefKind::Read.
    • Imports: import ... from '...' → enc.import(...) + RefKind::Import.

3. Build & Deterministic Packaging Pipeline

  • CARGO_INCREMENTAL=0 cargo build --release --target wasm32-unknown-unknown
  • Strip platform metadata: code-index-plugin-host --strip-name-section
  • Patch globals: code-index-plugin-host --inject-globals
  • Pack CIP: code-index plugin pack tests/packages/svelte --out de.h-dv.svelte-0.1.0.cip
  • Measure canonical SHA-256 digest into tests/packages/svelte.digest.

Work Breakdown & Milestones

  • Milestone 1: Grammar & Workspace Scaffolding
    • Source and compile tree-sitter-svelte.wasm (ABI 13–15).
    • Scaffold crates/guest/svelte with Cargo.toml, build.sh, and gen_kinds.py.
  • Milestone 2: Svelte Extractor Implementation
    • Implement component module, <script>, and <script module> parsing.
    • Implement prop extraction (legacy export let + Svelte 5 $props()).
    • Implement runes extraction ($state, $derived).
    • Implement Svelte 5 snippet extraction ({#snippet ...}).
    • Implement template AST walker: element tags (<Component />), event directives, mustaches.
  • Milestone 3: Manifest, Capabilities & Bridges
    • Write tests/packages/svelte/plugin.toml with resolver pool grants.
    • Define bi-directional language bridges between svelte ↔ typescript / javascript.
  • Milestone 4: Test Fixtures & Validation
    • Create Simple.svelte (Svelte 3/4) and Runes.svelte (Svelte 5) fixtures with .expected facts.
    • Add crates/daemon/tests/svelte_package_e2e.rs validating package install, enable, and MCP queries (search_symbols, find_callers, file_outline, get_symbol).
  • Milestone 5: CI Integration & Release Packaging
    • Add Pack the Svelte language package job in release workflow.
    • Commit canonical digest to tests/packages/svelte.digest.

Acceptance Criteria

  1. code-index plugin validate de.h-dv.svelte-0.1.0.cip passes with zero diagnostics.
  2. code-index plugin install and enable cleanly promotes the package into the active generation.
  3. search_symbols discovers Svelte components, props, state, and snippets in .svelte files.
  4. find_references on imported components resolves to their usages across <template> markup.
  5. All CI checks pass: #![deny(warnings)], Clippy, and zero test regressions.
# SCOPE REVISED 2026-09-15 — v0.1.0 is template-side only The original specification is kept verbatim below the divider, because the review comments on this issue cite it by section and a spec that moves under its own review is unreadable. **What changed and why:** the review ([#268 comment](https://git.h-dv.de/h-dv/code-index/issues/268#issuecomment-8434)) found four blockers, three of them cheap manifest errors and one architectural. The architectural one is filed as **#275**: a package gets exactly one grammar and this host has no tree-sitter injection, so the contents of `<script>` are unreachable. That was then confirmed from the grammar side ([measurements](https://git.h-dv.de/h-dv/code-index/issues/268#issuecomment-8436)) — `tree-sitter-svelte-ng` exposes the entire script body as ONE `raw_text` node and ships `queries/injections.scm` because that is the only way anything sees inside it. So v0.1.0 ships every fact the grammar actually produces, and claims nothing it cannot derive. Script-block facts land when #275 does. --- ## v0.1.0 scope ### IN — backed by real grammar nodes | fact | source | emitted as | | :-- | :-- | :-- | | component module symbol | file basename (`Button`, `+page`, `+layout`, `+error`) | `SymbolKind::Module`, `Visibility::Exported` | | Svelte 5 snippets `{#snippet row(item)}` | 11 node types in `node-types.json` | `SymbolKind::Function` | | snippet renders `{@render row(x)}` | 4 node types | `RefKind::Call` | | component tag refs `<Header />`, `<Modal />` | capitalised element tags | `RefKind::Type` | | event directives `onclick={handleClick}` | attribute + mustache | `RefKind::Call` | | mustache reads `{data.title}` | mustache expression | `RefKind::Read` | | block structures `{#if}`, `{#each}`, `{#await}`, `{#key}` | real block keywords | scope/structure only | Component resolution is **same-language**: `<Button />` resolves to `Button.svelte` through `exported_candidate`. That is not a bridge and must not be written as one. ### OUT — deferred to #275, stated rather than implied Everything inside `<script>` / `<script module>`: * props — `export let x` (Svelte 3/4) and `let { x } = $props()` (Svelte 5) * runes — `$state`, `$derived`, `$effect` (MEASURED: 0 node types each; they are JavaScript expressions, not template syntax) * script-local functions — `function handleClick() {}` * imports — `import Button from './Button.svelte'` The package documentation MUST state this limit in the operator-facing text. A package that silently returns fewer symbols than a reader expects is the disclosure defect this project keeps finding; a package that says which half it covers is honest. ### IN — `*.svelte.ts` / `*.svelte.js` (decided 2026-09-15) Svelte 5 universal reactivity modules ship in v0.1.0. MEASURED as feasible: `[claims.include]` accepts `suffixes` as well as `extensions`, and `path_eligibility_in` falls through to `select_plugin` when `displacing_route` declines, so ordinary `.ts` and `.js` files still reach the compiled-in plugins. It requires consent against the builtins: ```toml [claims.include] extensions = ["svelte"] suffixes = [".svelte.ts", ".svelte.js"] # `DisplaceDecl` names the BUILTIN's own id and the keys THAT language # loses, in the same five-list shape a claim uses -- not this package's # vocabulary. "The compiled-in `typescript` loses `ext:ts`" is the # sentence an operator has to answer. [[displaces]] builtin = "typescript" [displaces.keys] extensions = ["ts"] [[displaces]] builtin = "javascript" [displaces.keys] extensions = ["js"] ``` **THE CONSENT OVERSTATES WHAT IS TAKEN, AND THAT IS ACCEPTED RATHER THAN UNNOTICED.** `builtin::displaced_by` keys consent on the BUILTIN's whole key, so the operator confirmation reads *"displaces `typescript ext:ts`"* — "this package takes all your TypeScript" — to grant something that only ever routes `*.svelte.ts`. The routing is correct; the SENTENCE is wider than the behaviour. Two consequences follow, and both are requirements on this package rather than observations: 1. The package documentation MUST state, in operator-facing text, that it routes only `*.svelte.ts` / `*.svelte.js` and that ordinary `.ts` / `.js` continue to the built-in plugins. An operator reading only the consent prompt would conclude otherwise. 2. `svelte_package_e2e.rs` MUST contain a test proving it: a project holding `a.ts`, `b.js`, `c.svelte.ts` and `d.svelte.js`, with this package enabled, indexes the first two with the BUILTIN languages and the last two with `de.h-dv.svelte`. A claim this easy to get wrong and this alarming when misread does not travel on prose. Narrowing the consent surface so a package can displace an intersection rather than a whole key is a separate product question and is NOT in this issue's scope. ### Unchanged from the original The file-type table, the SvelteKit routing conventions, and the `fact_major = 1` / `host_min = 1` / `package_format = 1` targets are all correct as originally written. --- ## Corrected manifest (`tests/packages/svelte/plugin.toml`) ```toml package_format = 1 [package] id = "de.h-dv.svelte" version = "0.1.0" license = "MIT OR Apache-2.0" [abi] fact_major = 1 fact_minor_min = 0 host_min = 1 host_max = 1 [grammar] # tree-sitter-svelte-ng 1.0.2 from crates.io -- NOT `tree-sitter-svelte`, # which is the abandoned 2022 grammar and predates Svelte 5. # MEASURED: src/parser.c:7 `#define LANGUAGE_VERSION 14`, inside this # host's 13..=15 window and the same ABI as the ruby and xml artifacts. artifact = "grammar.wasm" exported_name = "svelte" ts_abi_min = 13 ts_abi_max = 15 [[languages]] id = "svelte" [[claims]] language = "svelte" tier = "executable" component = "extractor.wasm" [claims.include] # `svelte` is NOT in BUILTIN_CLAIMS, so this is claimable outright -- # no `.rbx`-style shadow extension of the kind tests/packages/ruby needs. extensions = ["svelte"] # Svelte 5 universal reactivity modules. `.ts`/`.js` DO belong to # builtins, so these two suffixes need the consent below. suffixes = [".svelte.ts", ".svelte.js"] [[displaces]] builtin = "typescript" [displaces.keys] extensions = ["ts"] [[displaces]] builtin = "javascript" [displaces.keys] extensions = ["js"] [capabilities] resolver = [ "same_file_candidate", "exported_candidate", "reachability_anchor", "qualified_candidate", "type_position_candidate", "member_candidate", ] # NO BRIDGES, and that is the package saying so rather than an omission. # `<Button />` resolves to `Button.svelte` -- the SAME language -- which # `exported_candidate` already does. The original spec's svelte->typescript # and svelte->javascript bridges were wrong twice over: `scope = "workspace"` # is not a value `evidence_for_scope` admits, and a component is not a TS # type or a JS class. Because there are no bridges, `bridge_source` is not # requested either. [[fixtures]] input = "fixtures/Simple.svelte" expected = "fixtures/Simple.expected" [[fixtures]] input = "fixtures/Runes.svelte" expected = "fixtures/Runes.expected" [[fixtures]] input = "fixtures/Broken.svelte" expected = "fixtures/Broken.expected" ``` `Runes.svelte` stays in the fixture set even though runes are out of scope — it is the fixture that PINS the boundary. It must assert that the script block yields the component module symbol and no rune symbols, so that the day #275 lands, the change shows up as a fixture diff instead of as a silent gain. `Broken.svelte` is the parse-error decoy both shipped packages carry (`Broken.xaml`, `broken.rb.rbx`) and the original spec omitted. --- ## Revised milestones - [ ] **M1: Grammar and scaffolding** - [ ] `tests/grammars/build-tree-sitter-svelte.sh`, following `build-tree-sitter-ruby.sh` byte for byte: fetch the pinned `.crate`, `sha256sum -c` it, compile `src/parser.c` + `src/scanner.c` to wasm, `sha256sum -c` the output. `VERSION=1.0.2`, `CRATE=tree-sitter-svelte-ng`, `CRATE_SHA256=ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37` - [ ] Do NOT vendor the prebuilt `tree-sitter-svelte.wasm` from the npm package — `grammar_provenance.rs` pins `WASM_ARTIFACTS` by hash and a vendored binary has no recipe anyone can re-run. - [ ] Record the artifact in `grammar_provenance.rs`'s `WASM_ARTIFACTS`. - [ ] Scaffold `crates/guest/svelte` (`Cargo.toml`, `build.sh`, kind-id table). - [ ] **M2: Extractor — template side only** - [ ] Component module symbol from basename, including the SvelteKit `+page` / `+layout` / `+error` forms. - [ ] `{#snippet}` &rarr; `Function`; `{@render}` &rarr; `Call`. - [ ] Capitalised element tags &rarr; `RefKind::Type`. - [ ] Event directives &rarr; `Call`; mustache reads &rarr; `Read`. - [ ] Emit NOTHING from `raw_text`. A `<script>` body is opaque at this ABI and must not be regex-scanned — see "rejected approach" below. - [ ] **M3: Manifest and capabilities** - [ ] The manifest above. No bridges, no `bridge_source`. - [ ] **M4: Fixtures and validation** - [ ] `Simple.svelte` (Svelte 3/4), `Runes.svelte` (Svelte 5, pinning the boundary), `Broken.svelte` (parse-error decoy), `counter.svelte.ts` (universal reactivity module), each with `.expected`. - [ ] `crates/daemon/tests/svelte_package_e2e.rs`, matching the `xaml_package_e2e.rs` convention: install, enable, then `search_symbols` / `find_callers` / `file_outline` / `get_symbol` over `.svelte`. - [ ] A test asserting `<Button />` in `App.svelte` resolves to `Button.svelte` — the claim this package exists to make. - [ ] **M5: CI and release packaging** - [ ] `Pack the Svelte language package` step in `release.yml`. - [ ] Commit the canonical digest to `tests/packages/svelte.digest`. - [ ] Add the package to `distribution/registry.v1.json`'s derived set (the catalog enumerates `tests/packages/*/plugin.toml` from disk, so this is automatic — confirm it, do not author it). --- ## Revised acceptance criteria 1. `code-index plugin validate de.h-dv.svelte-0.1.0.cip` passes with zero diagnostics. 2. `plugin install` and `plugin enable` promote the package into the active generation. 3. `search_symbols` finds Svelte components and snippets in `.svelte` files. 4. `find_references` on a component resolves `<Button />` usages to `Button.svelte`, same-language. 5. **A NEGATIVE criterion, because this package's honesty is the point:** a fixture containing `$state`, `$props` and a script-local `function` yields NO symbols for them, and the package doc says why. Passing criterion 3 while silently missing half a file is the failure mode this replaces. 6. **Displacement routes only what it claims:** a project holding `a.ts`, `b.js`, `c.svelte.ts` and `d.svelte.js` indexes the first two with the BUILT-IN typescript/javascript languages and the last two with `de.h-dv.svelte`. The consent prompt says the builtin loses `ext:ts`; this criterion is what proves the behaviour is narrower than the sentence. 7. All CI checks pass: `#![deny(warnings)]`, Clippy, zero test regressions. --- ## Rejected approach, recorded so it is not re-proposed Hand-rolling a JavaScript/TypeScript scanner in the guest to read the `<script>` body. It re-implements a parser and manufactures facts from a format the package cannot fully parse — the class of defect I066 spent a release removing from the distribution catalog. The grammar itself declines to do this, correctly. If the script block is worth reading, the answer is #275, not a regex. --- --- # Original specification (superseded 2026-09-15) *Kept verbatim: the review comments on this issue cite it by section.* ## Summary &amp; Problem Statement Currently, `code-index` treats `.svelte` files as text-only (`indexed_as: "text"`). They participate in full-text search via FTS5, but are completely symbol-blind: 1. No symbols (components, props, state, runes, snippets) are extracted. 2. No call graph or reference resolution exists for component instantiations (`<Button />`, `<Modal />`). 3. Agents cannot evaluate refactoring impact or blast radius for Svelte components. 4. SvelteKit route conventions (`+page.svelte`, `+layout.svelte`, `+error.svelte`) and Svelte 5 universal reactivity modules (`.svelte.ts`, `.svelte.js`) lack structural intelligence. This issue tracks the creation, verification, and distribution of the official **`de.h-dv.svelte`** dynamic WebAssembly plugin package (Option B under Epic #75 / Spec 05). --- ## Filetype &amp; System Scope Based on the official Svelte 5 &amp; SvelteKit documentation: | File Pattern | Purpose / Constructs | Handled By | | :--- | :--- | :--- | | `*.svelte` | Svelte Single File Components (SFC): `<script>`, `<script module>`, markup, `<style>`, runes, snippets | `de.h-dv.svelte` (Claims `ext("svelte")`) | | `*.svelte.ts`, `*.svelte.js` | **Universal Reactivity Modules** (Svelte 5 Runes outside components) | Built-in TypeScript plugin + bridge (or claimed suffix with `[[displaces]]`) | | `+page.svelte`, `+layout.svelte`, `+error.svelte` | SvelteKit route UI templates; consume `PageProps` / `LayoutProps` | `de.h-dv.svelte` | | `+page.ts/.js`, `+page.server.ts/.js`, `+server.ts/.js` | SvelteKit universal/server load functions, form actions, API endpoints | Built-in TypeScript plugin | | `hooks.client.ts/.js`, `hooks.server.ts/.js` | SvelteKit application middleware and routing hooks | Built-in TypeScript plugin | --- ## Architectural &amp; Package Design The plugin will be distributed as an external, sandboxed CIP package (`de.h-dv.svelte-0.1.0.cip`) compliant with `fact_major = 1`, `host_min = 1`, and `package_format = 1`. ### 1. Package Manifest (`tests/packages/svelte/plugin.toml`) ```toml package_format = 1 [package] id = "de.h-dv.svelte" version = "0.1.0" license = "MIT OR Apache-2.0" [abi] fact_major = 1 fact_minor_min = 0 host_min = 1 host_max = 1 [grammar] artifact = "grammar.wasm" exported_name = "svelte" ts_abi_min = 13 ts_abi_max = 15 [[languages]] id = "svelte" [[claims]] language = "svelte" tier = "executable" component = "extractor.wasm" [claims.include] extensions = ["svelte"] [capabilities] resolver = [ "same_file_candidate", "exported_candidate", "reachability_anchor", "qualified_candidate", "type_position_candidate", "member_candidate", ] # Bridge: Connect component usage (<Button />) to TypeScript definitions [[capabilities.bridges]] source_language = "svelte" source_ref_kind = "type" destination_language = "typescript" destination_symbol_kind = "type" scope = "workspace" name_map = "identity" ambiguity = "unique" evidence = "conventional" # Bridge: Connect component usage (<Button />) to JavaScript definitions [[capabilities.bridges]] source_language = "svelte" source_ref_kind = "type" destination_language = "javascript" destination_symbol_kind = "class" scope = "workspace" name_map = "identity" ambiguity = "unique" evidence = "conventional" [[fixtures]] input = "fixtures/Simple.svelte" expected = "fixtures/Simple.expected" ``` ### 2. Guest Extractor (`crates/guest/svelte`) - Implemented in Rust with `#![no_std]` and `panic = "abort"`, linking `code-index-guest`. - Traverses the `tree-sitter-svelte` AST to emit: - **Component Module Symbol**: Derived from file basename (`+page`, `+layout`, `Button`). - **Props (Svelte 3/4 &amp; 5)**: `export let prop` and `let { prop } = $props()` &rarr; `SymbolKind::Field`, `Visibility::Exported`. - **Reactivity &amp; Runes**: `$state(...)`, `$derived(...)` &rarr; `SymbolKind::Variable`. - **Script Functions**: `function handleClick()` &rarr; `SymbolKind::Function`. - **Svelte 5 Snippets**: `{#snippet row(item)}` &rarr; `SymbolKind::Function`. - **Component Tag References**: Capitalized markup elements (`<Header />`, `<Modal />`) &rarr; `RefKind::Type` or `RefKind::Call`. - **Bindings &amp; Handlers**: `onclick={handleClick}`, `{data.title}` &rarr; `RefKind::Call` / `RefKind::Read`. - **Imports**: `import ... from '...'` &rarr; `enc.import(...)` + `RefKind::Import`. ### 3. Build &amp; Deterministic Packaging Pipeline - `CARGO_INCREMENTAL=0 cargo build --release --target wasm32-unknown-unknown` - Strip platform metadata: `code-index-plugin-host --strip-name-section` - Patch globals: `code-index-plugin-host --inject-globals` - Pack CIP: `code-index plugin pack tests/packages/svelte --out de.h-dv.svelte-0.1.0.cip` - Measure canonical SHA-256 digest into `tests/packages/svelte.digest`. --- ## Work Breakdown &amp; Milestones - [ ] **Milestone 1: Grammar &amp; Workspace Scaffolding** - [ ] Source and compile `tree-sitter-svelte.wasm` (ABI 13–15). - [ ] Scaffold `crates/guest/svelte` with `Cargo.toml`, `build.sh`, and `gen_kinds.py`. - [ ] **Milestone 2: Svelte Extractor Implementation** - [ ] Implement component module, `<script>`, and `<script module>` parsing. - [ ] Implement prop extraction (legacy `export let` + Svelte 5 `$props()`). - [ ] Implement runes extraction (`$state`, `$derived`). - [ ] Implement Svelte 5 snippet extraction (`{#snippet ...}`). - [ ] Implement template AST walker: element tags (`<Component />`), event directives, mustaches. - [ ] **Milestone 3: Manifest, Capabilities &amp; Bridges** - [ ] Write `tests/packages/svelte/plugin.toml` with resolver pool grants. - [ ] Define bi-directional language bridges between `svelte` &harr; `typescript` / `javascript`. - [ ] **Milestone 4: Test Fixtures &amp; Validation** - [ ] Create `Simple.svelte` (Svelte 3/4) and `Runes.svelte` (Svelte 5) fixtures with `.expected` facts. - [ ] Add `crates/daemon/tests/svelte_package_e2e.rs` validating package install, enable, and MCP queries (`search_symbols`, `find_callers`, `file_outline`, `get_symbol`). - [ ] **Milestone 5: CI Integration &amp; Release Packaging** - [ ] Add `Pack the Svelte language package` job in release workflow. - [ ] Commit canonical digest to `tests/packages/svelte.digest`. --- ## Acceptance Criteria 1. `code-index plugin validate de.h-dv.svelte-0.1.0.cip` passes with zero diagnostics. 2. `code-index plugin install` and `enable` cleanly promotes the package into the active generation. 3. `search_symbols` discovers Svelte components, props, state, and snippets in `.svelte` files. 4. `find_references` on imported components resolves to their usages across `<template>` markup. 5. All CI checks pass: `#![deny(warnings)]`, Clippy, and zero test regressions.
Author
Member

Review

Measured against the tree at 77f843d rather than read on its own terms. The package is worth building and the file-type scoping is right, but four things would stop it at plugin validate or produce wrong binds, and one is architectural.

Blockers

B1 — scope = "workspace" is not a valid bridge scope. Both declared bridges use it. crates/package/src/bridge.rs:

pub fn evidence_for_scope(scope: &str) -> Option<&'static str> {
    Some(match scope {
        "same_file" => "structural",
        "paired_file" | "same_directory" => "conventional",
        _ => return None,          // <- "workspace" lands here
    })
}

Acceptance criterion 1 ("plugin validate passes with zero diagnostics") fails as written.

B2 — the bridges point at the wrong destination anyway. <Button /> resolves to Button.svelte, not to a TypeScript type or a JavaScript class. Svelte-to-Svelte is SAME-LANGUAGE resolution through exported_candidate; it is not a bridge at all. As declared, these would bind components to unrelated same-named TS types — a phantom generator, not a feature.

B3 — bridge_source is missing from [capabilities] resolver. tests/packages/xaml — the only working bridge package in the tree — declares resolver = ["bridge_source"]. This manifest lists the six pool capabilities and omits it while declaring two bridges.

B4 — a Svelte SFC is a multi-language file, and a package gets exactly one grammar. manifest.rs:142 is pub grammar: Grammar (not Option, not Vec), and there is no tree-sitter language injection anywhere in the tree. Four of Milestone 2's eight extraction targets — props (export let, $props()), runes, script functions, imports — are all inside <script>, which tree-sitter-svelte exposes as a raw block for editors to inject into.

The XAML precedent does not transfer: XAML reaches C# with scope = "paired_file" because the C# is a SEPARATE FILE. Svelte's script is in the same file, so there is nothing on the other end of any scope.

Filed as #275 against the host, because "a package cannot see inside a multi-language file" is a platform limit and .vue, .astro, .mdx and .razor are the same shape.

Good news — two places this issue is too cautious

.svelte needs no shadow extension. BUILTIN_CLAIMS holds rust, python, typescript, javascript, csharp, php and ruby — svelte is not among them, so ext("svelte") is claimable outright. None of the .rbx-style contortion tests/packages/ruby needed applies here.

.svelte.ts / .svelte.js ARE expressible today. [claims.include] accepts suffixes as well as extensions, so suffixes = [".svelte.ts"] plus [[displaces]] typescript ext:ts works, and routing falls through correctly — path_eligibility_in calls select_plugin when displacing_route declines, so ordinary .ts files still reach the builtin.

One caveat worth designing for rather than discovering later: displacement consent is keyed on the BUILTIN's whole key, so the operator confirmation will read "displaces typescript ext:ts" — i.e. "this package takes all your TypeScript" — when it would in fact take only *.svelte.ts. That is a trust problem, not a correctness one.

Smaller gaps

  • No broken-input fixture. Both shipped packages carry a parse-error decoy (Broken.xaml, broken.rb.rbx); this spec has none. The manifest also declares one fixture while Milestone 4 names two.
  • No tree-sitter-svelte in-tree — tests/grammars/ holds only ruby and xml. Milestone 1 is genuinely new external work, and the ts_abi_min/max = 13..15 claim is unverified against the actual grammar.
  • _prdoc/guides/80-package-authoring.md says nothing about embedded/multi-language files. That silence is most likely why script-block extraction ended up in the milestones; covered as option 4 in #275.

Confirmed fine: plugin validate is a real verb (crates/cli/src/plugin.rs:180); crates/daemon/tests/svelte_package_e2e.rs matches the xaml_package_e2e.rs convention; tests/packages/svelte.digest matches existing naming; the manifest skeleton otherwise matches the real schema.

Suggested reshape

Scope v0.1.0 to what one grammar can honestly deliver, and say so in the package doc rather than implying more:

  • component module symbol from the basename (Button, +page, +layout)
  • Svelte 5 snippets {#snippet row(item)} -> Function
  • component tag refs <Header /> -> type
  • event/mustache refs onclick={handleClick}, {data.title}
  • resolve <Button /> -> Button.svelte through exported_candidate as same-language — drop both bridges

That is a real package: .svelte stops being symbol-blind, components get a call graph, and every fact in it is one the grammar actually produced. Script-block facts land when #275 does.

The tempting third option — hand-rolling a JS scanner in the guest for the <script> body — is worth rejecting explicitly. It re-implements a parser and manufactures facts from a format it cannot fully parse, which is the class of defect I066 spent a release removing.

## Review Measured against the tree at `77f843d` rather than read on its own terms. The package is worth building and the file-type scoping is right, but four things would stop it at `plugin validate` or produce wrong binds, and one is architectural. ### Blockers **B1 — `scope = "workspace"` is not a valid bridge scope.** Both declared bridges use it. `crates/package/src/bridge.rs`: ```rust pub fn evidence_for_scope(scope: &str) -> Option<&'static str> { Some(match scope { "same_file" => "structural", "paired_file" | "same_directory" => "conventional", _ => return None, // <- "workspace" lands here }) } ``` Acceptance criterion 1 ("`plugin validate` passes with zero diagnostics") fails as written. **B2 — the bridges point at the wrong destination anyway.** `<Button />` resolves to `Button.svelte`, not to a TypeScript `type` or a JavaScript `class`. Svelte-to-Svelte is SAME-LANGUAGE resolution through `exported_candidate`; it is not a bridge at all. As declared, these would bind components to unrelated same-named TS types — a phantom generator, not a feature. **B3 — `bridge_source` is missing from `[capabilities] resolver`.** `tests/packages/xaml` — the only working bridge package in the tree — declares `resolver = ["bridge_source"]`. This manifest lists the six pool capabilities and omits it while declaring two bridges. **B4 — a Svelte SFC is a multi-language file, and a package gets exactly one grammar.** `manifest.rs:142` is `pub grammar: Grammar` (not `Option`, not `Vec`), and there is no tree-sitter language injection anywhere in the tree. Four of Milestone 2's eight extraction targets — props (`export let`, `$props()`), runes, script functions, imports — are all inside `<script>`, which `tree-sitter-svelte` exposes as a raw block for editors to inject into. The XAML precedent does not transfer: XAML reaches C# with `scope = "paired_file"` because the C# is a SEPARATE FILE. Svelte's script is in the same file, so there is nothing on the other end of any scope. Filed as **#275** against the host, because "a package cannot see inside a multi-language file" is a platform limit and `.vue`, `.astro`, `.mdx` and `.razor` are the same shape. ### Good news — two places this issue is too cautious **`.svelte` needs no shadow extension.** `BUILTIN_CLAIMS` holds rust, python, typescript, javascript, csharp, php and ruby — `svelte` is not among them, so `ext("svelte")` is claimable outright. None of the `.rbx`-style contortion `tests/packages/ruby` needed applies here. **`.svelte.ts` / `.svelte.js` ARE expressible today.** `[claims.include]` accepts `suffixes` as well as `extensions`, so `suffixes = [".svelte.ts"]` plus `[[displaces]] typescript ext:ts` works, and routing falls through correctly — `path_eligibility_in` calls `select_plugin` when `displacing_route` declines, so ordinary `.ts` files still reach the builtin. One caveat worth designing for rather than discovering later: displacement consent is keyed on the BUILTIN's whole key, so the operator confirmation will read "displaces `typescript ext:ts`" — i.e. "this package takes all your TypeScript" — when it would in fact take only `*.svelte.ts`. That is a trust problem, not a correctness one. ### Smaller gaps * **No broken-input fixture.** Both shipped packages carry a parse-error decoy (`Broken.xaml`, `broken.rb.rbx`); this spec has none. The manifest also declares one fixture while Milestone 4 names two. * **No `tree-sitter-svelte` in-tree** — `tests/grammars/` holds only ruby and xml. Milestone 1 is genuinely new external work, and the `ts_abi_min/max = 13..15` claim is unverified against the actual grammar. * **`_prdoc/guides/80-package-authoring.md` says nothing about embedded/multi-language files.** That silence is most likely why script-block extraction ended up in the milestones; covered as option 4 in #275. Confirmed fine: `plugin validate` is a real verb (`crates/cli/src/plugin.rs:180`); `crates/daemon/tests/svelte_package_e2e.rs` matches the `xaml_package_e2e.rs` convention; `tests/packages/svelte.digest` matches existing naming; the manifest skeleton otherwise matches the real schema. ### Suggested reshape Scope **v0.1.0 to what one grammar can honestly deliver**, and say so in the package doc rather than implying more: * component module symbol from the basename (`Button`, `+page`, `+layout`) * Svelte 5 snippets `{#snippet row(item)}` -> `Function` * component tag refs `<Header />` -> `type` * event/mustache refs `onclick={handleClick}`, `{data.title}` * resolve `<Button />` -> `Button.svelte` through `exported_candidate` as same-language — **drop both bridges** That is a real package: `.svelte` stops being symbol-blind, components get a call graph, and every fact in it is one the grammar actually produced. Script-block facts land when #275 does. The tempting third option — hand-rolling a JS scanner in the guest for the `<script>` body — is worth rejecting explicitly. It re-implements a parser and manufactures facts from a format it cannot fully parse, which is the class of defect I066 spent a release removing.
Author
Member

Grammar and ABI: measured

My review above said the ts_abi_min/max = 13..15 claim was unverified. It is now verified, and it is correct — but the grammar SOURCE named in Milestone 1 is a trap, and the Svelte 5 story splits cleanly.

The ABI claim holds

tree-sitter-svelte-ng 1.0.2, src/parser.c:7:

#define LANGUAGE_VERSION 14

ABI 14 — inside this host's 13..=15 window, and the same ABI as the tree-sitter-ruby (14) and tree-sitter-xml (14) artifacts already in tests/grammars/. Nothing about the ABI blocks this package.

Milestone 1's source name would fetch a 2022 grammar

crate / package version date note
crates.io tree-sitter-svelte 0.10.2 2022-04-15 abandoned; predates Svelte 5 entirely
npm tree-sitter-svelte 0.11.0 2023-04-03 same author, also abandoned
crates.io tree-sitter-svelte-ng 1.0.2 2024-09-08 tree-sitter-grammars org, 363k downloads, ABI 14
crates.io tree-sitter-svelte-next 0.1.1 2026-02-11 personal fork, 197k downloads, ABI 14

Milestone 1 reads "source and compile tree-sitter-svelte.wasm". Taken literally against crates.io that pulls the 2022 grammar, written before runes existed. The name wanted is tree-sitter-svelte-ng, which is the tree-sitter-grammars org fork and matches the provenance of the existing tree-sitter-xml pin.

-next is newer by date but is a personal repo, and its advertised "tree-sitter 0.25+ compatibility" concerns the RUST BINDING, not the ABI — irrelevant here, because build-tree-sitter-ruby.sh and build-tree-sitter-xml.sh compile src/parser.c + src/scanner.c to wasm and never touch the Rust binding. Both forks measure ABI 14 and carry identical template node sets, so recency buys nothing over provenance.

Values a tests/grammars/build-tree-sitter-svelte.sh would pin, following the existing scripts byte for byte:

VERSION=1.0.2
CRATE=tree-sitter-svelte-ng
CRATE_SHA256=ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37
# source dir: tree-sitter-svelte-ng-1.0.2/src  (parser.c, scanner.c, tag.h, tree_sitter/)

Do not use the prebuilt tree-sitter-svelte.wasm that ships in the npm package. grammar_provenance.rs pins WASM_ARTIFACTS by hash and the house pattern builds from pinned source with sha256sum -c on both the crate and the output. A vendored binary has no recipe anyone can re-run.

Svelte 5 support splits exactly along the #275 line

Measured from node-types.json:

construct node types verdict
{#snippet …} 11 REAL template grammar
{@render …} 4 REAL template grammar
$props 0 not template syntax
$state 0 not template syntax
$derived 0 not template syntax

So Milestone 2's snippet extraction is feasible today. The runes are not missing from the grammar by oversight — they are JavaScript expressions living in the script block, and the grammar says so itself. From 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"))

The whole <script> body is a single raw_text node, and the grammar ships an injection query precisely because that is the only way to see inside it.

That is worth stating plainly: B4 in my review was inferred from the HOST side — one grammar per package, no injection. The grammar confirms the identical boundary from the other side, independently. The two agree.

Net effect

The recommended reshape does not change; it gets firmer. Every template-side fact in it is backed by real grammar nodes, and every script-side fact waits on #275. The only edit this adds is to Milestone 1: pin tree-sitter-svelte-ng 1.0.2, not tree-sitter-svelte.

## Grammar and ABI: measured My review above said the `ts_abi_min/max = 13..15` claim was unverified. It is now verified, and **it is correct** — but the grammar SOURCE named in Milestone 1 is a trap, and the Svelte 5 story splits cleanly. ### The ABI claim holds `tree-sitter-svelte-ng` 1.0.2, `src/parser.c:7`: ```c #define LANGUAGE_VERSION 14 ``` ABI 14 — inside this host's `13..=15` window, and the same ABI as the `tree-sitter-ruby` (14) and `tree-sitter-xml` (14) artifacts already in `tests/grammars/`. Nothing about the ABI blocks this package. ### Milestone 1's source name would fetch a 2022 grammar | crate / package | version | date | note | | :-- | :-- | :-- | :-- | | crates.io `tree-sitter-svelte` | 0.10.2 | **2022-04-15** | abandoned; predates Svelte 5 entirely | | npm `tree-sitter-svelte` | 0.11.0 | 2023-04-03 | same author, also abandoned | | **crates.io `tree-sitter-svelte-ng`** | **1.0.2** | 2024-09-08 | tree-sitter-grammars org, 363k downloads, ABI 14 | | crates.io `tree-sitter-svelte-next` | 0.1.1 | 2026-02-11 | personal fork, 197k downloads, ABI 14 | Milestone 1 reads "source and compile `tree-sitter-svelte.wasm`". Taken literally against crates.io that pulls the **2022** grammar, written before runes existed. The name wanted is **`tree-sitter-svelte-ng`**, which is the tree-sitter-grammars org fork and matches the provenance of the existing `tree-sitter-xml` pin. `-next` is newer by date but is a personal repo, and its advertised "tree-sitter 0.25+ compatibility" concerns the RUST BINDING, not the ABI — irrelevant here, because `build-tree-sitter-ruby.sh` and `build-tree-sitter-xml.sh` compile `src/parser.c` + `src/scanner.c` to wasm and never touch the Rust binding. Both forks measure ABI 14 and carry identical template node sets, so recency buys nothing over provenance. Values a `tests/grammars/build-tree-sitter-svelte.sh` would pin, following the existing scripts byte for byte: ``` VERSION=1.0.2 CRATE=tree-sitter-svelte-ng CRATE_SHA256=ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37 # source dir: tree-sitter-svelte-ng-1.0.2/src (parser.c, scanner.c, tag.h, tree_sitter/) ``` **Do not use the prebuilt `tree-sitter-svelte.wasm` that ships in the npm package.** `grammar_provenance.rs` pins `WASM_ARTIFACTS` by hash and the house pattern builds from pinned source with `sha256sum -c` on both the crate and the output. A vendored binary has no recipe anyone can re-run. ### Svelte 5 support splits exactly along the #275 line Measured from `node-types.json`: | construct | node types | verdict | | :-- | --: | :-- | | `{#snippet …}` | 11 | REAL template grammar | | `{@render …}` | 4 | REAL template grammar | | `$props` | 0 | not template syntax | | `$state` | 0 | not template syntax | | `$derived` | 0 | not template syntax | So Milestone 2's snippet extraction is feasible today. The runes are not missing from the grammar by oversight — they are JavaScript expressions living in the script block, and the grammar says so itself. From `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")) ``` The whole `<script>` body is a single **`raw_text`** node, and the grammar ships an injection query precisely because that is the only way to see inside it. That is worth stating plainly: B4 in my review was inferred from the HOST side — one grammar per package, no injection. The grammar confirms the identical boundary from the other side, independently. The two agree. ### Net effect The recommended reshape does not change; it gets firmer. Every template-side fact in it is backed by real grammar nodes, and every script-side fact waits on #275. The only edit this adds is to Milestone 1: pin `tree-sitter-svelte-ng` 1.0.2, not `tree-sitter-svelte`.
Author
Member

Shipped — de.h-dv.svelte 0.1.0

Landed in 12bfe8f (the package and the ABI input it needed) and 09305a3 (the SDK pin that could only move in a second commit). Closing.

.svelte is no longer symbol-blind, and <Button /> resolves to Button.svelte.

Revised acceptance criteria

# criterion verdict
1 plugin validate passes with zero diagnostics met — ok, exit 0
2 install + enable promote into the active generation met — svelte_package_e2e
3 search_symbols finds components and snippets met — same
4 find_references resolves <Button /> to Button.svelte met, with a ceiling — see below
5 NEGATIVE: a fixture with $state/$props/a script function yields NO symbols, and the doc says why met — the_script_block_yields_nothing_and_that_is_the_boundary plus ScriptOnly.expected's twelve [[absent]] decoys
6 displacement routes only what it claims MOOT, not met — this criterion was written while *.svelte.ts was in scope. That claim was dropped, so the package declares no [[displaces]] table and there is nothing to route. Recorded rather than ticked.
7 all CI green met — Linux and native-Windows legs on 09305a3

The one thing to read before using it: THE BIND IS DIRECTORY-LOCAL

<Button /> binds to Button.svelte when both sit in ONE DIRECTORY, decided by tier1b_same_directory. A component in src/lib/components/ referenced from src/routes/ — the layout nearly every real SvelteKit project uses — does not bind.

This was MEASURED after the same-directory test already passed, and it corrects reasoning recorded earlier in this issue. exported_candidate is workspace-wide, and this work argued from that to a cross-directory edge. The pool part is right; the conclusion was not. The grant is NECESSARY AND NOT SUFFICIENT — it admits the module rows to a pool, and dropping it reddens the same-directory bind, but it supplies no evidence a directory-crossing tier could decide on.

What would carry a component reference across a directory is its IMPORT, and the import is inside <script>, which this package structurally cannot read. Binding on bare name across a workspace instead would resolve Button to whichever Button.svelte came first — in a tree holding both src/lib/components/Button.svelte and src/routes/admin/Button.svelte that is a coin toss presented as an answer. A missing edge is recoverable; a confident wrong one is not.

So the ceiling is an ASSERTION and not an #[ignore]: a_component_tag_does_not_bind_across_directories_and_that_is_the_ceiling goes RED the day the import becomes a fact, and whoever sees it reads why. #275 is the unblocking work.

Two scope changes, both on measurement

*.svelte.ts / *.svelte.js are NOT claimed, having been moved into v0.1.0 and then back out. The claim mechanism works — suffixes plus [[displaces]], with displacing_route falling through so ordinary .ts still reaches the builtin. It is still wrong: those files are valid TypeScript that the compiled-in plugin indexes correctly today, and this package holds only the Svelte grammar, so const x = { foo } parses as a mustache and yields a bogus read ref. Claiming them would trade working facts for misparse noise.

Script-block facts are out by construction, not omission. Props, runes ($state, $props, $derived), script-local functions and imports all live inside <script>, which tree-sitter-svelte hands over as a single raw_text node — it ships queries/injections.scm precisely because injection is the only way in, and this host has none. Tracked as #275 for every format of this shape (.vue, .astro, .mdx, .razor).

#229's missing input, built

A component's name is its FILENAME and appears nowhere in its bytes, so <Button /> had nothing to resolve to. TAG_SYMBOL_BASENAME (0x8004) is the shape crates/abi/src/record.rs had specified and left unbuilt: the guest emits an ordinal naming a symbol it already emitted, the host substitutes the stem of the path it routed the file by, and no string crosses the sandbox boundary.

ABI_MINOR moved 1 → 2 with it, and that was a debt rather than a courtesy. An optional tag normally degrades safely; this one cannot, because a symbol's name has no absent encoding — an older host stores the guest's placeholder AS IF IT WERE A NAME. It misleads, which is worse than degrading and worse than refusing. So the manifest brackets fact_minor_min = 2 and an older host REFUSES the package outright (measured: archive_refused: manifest.abi_unsupported (ABI_MINOR = 1, observed 2)).

Corrections worth recording

  • The grammar named in the original Milestone 1 would have fetched tree-sitter-svelte 0.10.2 — 2022, abandoned, and predating Svelte 5 entirely. The maintained fork is tree-sitter-svelte-ng 1.0.2 (ABI 14), now pinned and byte-reproducible.
  • The original spec's two bridges were wrong twice over: scope = "workspace" is not a value evidence_for_scope admits, and a component is not a TypeScript type or a JavaScript class. Svelte-to-Svelte is same-language resolution; there are no bridges.
  • The boundary fixture was vacuous at first. Disabling the <script> guard entirely left the suite GREEN, because a script body is a single raw_text LEAF and no rule reads a bare raw_text — descending into one and refusing to are byte-identical output. It grades something only because the fixtures now carry a mustache on the boundary element's OWN open tag.

Filed from this work

  • #275 — packages cannot see inside a multi-language file (one grammar per package, no injection). The unblocking work for the script block and for the cross-directory bind.
  • #276 — CI formats, lints and rustdocs no shipped guest crate. All four are outside the workspace and outside every gate; they were run by hand for this package.
## Shipped — `de.h-dv.svelte` 0.1.0 Landed in `12bfe8f` (the package and the ABI input it needed) and `09305a3` (the SDK pin that could only move in a second commit). Closing. `.svelte` is no longer symbol-blind, and `<Button />` resolves to `Button.svelte`. ### Revised acceptance criteria | # | criterion | verdict | | :-- | :-- | :-- | | 1 | `plugin validate` passes with zero diagnostics | **met** — `ok`, exit 0 | | 2 | `install` + `enable` promote into the active generation | **met** — `svelte_package_e2e` | | 3 | `search_symbols` finds components and snippets | **met** — same | | 4 | `find_references` resolves `<Button />` to `Button.svelte` | **met, with a ceiling** — see below | | 5 | NEGATIVE: a fixture with `$state`/`$props`/a script function yields NO symbols, and the doc says why | **met** — `the_script_block_yields_nothing_and_that_is_the_boundary` plus `ScriptOnly.expected`'s twelve `[[absent]]` decoys | | 6 | displacement routes only what it claims | **MOOT, not met** — this criterion was written while `*.svelte.ts` was in scope. That claim was dropped, so the package declares no `[[displaces]]` table and there is nothing to route. Recorded rather than ticked. | | 7 | all CI green | **met** — Linux and native-Windows legs on `09305a3` | ### The one thing to read before using it: THE BIND IS DIRECTORY-LOCAL `<Button />` binds to `Button.svelte` when both sit in ONE DIRECTORY, decided by `tier1b_same_directory`. A component in `src/lib/components/` referenced from `src/routes/` — the layout nearly every real SvelteKit project uses — **does not bind**. This was MEASURED after the same-directory test already passed, and it corrects reasoning recorded earlier in this issue. `exported_candidate` is workspace-wide, and this work argued from that to a cross-directory edge. The pool part is right; the conclusion was not. The grant is NECESSARY AND NOT SUFFICIENT — it admits the module rows to a pool, and dropping it reddens the same-directory bind, but it supplies no evidence a directory-crossing tier could decide on. What would carry a component reference across a directory is its IMPORT, and the import is inside `<script>`, which this package structurally cannot read. Binding on bare name across a workspace instead would resolve `Button` to whichever `Button.svelte` came first — in a tree holding both `src/lib/components/Button.svelte` and `src/routes/admin/Button.svelte` that is a coin toss presented as an answer. A missing edge is recoverable; a confident wrong one is not. So the ceiling is an ASSERTION and not an `#[ignore]`: `a_component_tag_does_not_bind_across_directories_and_that_is_the_ceiling` goes RED the day the import becomes a fact, and whoever sees it reads why. #275 is the unblocking work. ### Two scope changes, both on measurement **`*.svelte.ts` / `*.svelte.js` are NOT claimed**, having been moved into v0.1.0 and then back out. The claim mechanism works — `suffixes` plus `[[displaces]]`, with `displacing_route` falling through so ordinary `.ts` still reaches the builtin. It is still wrong: those files are valid TypeScript that the compiled-in plugin indexes correctly today, and this package holds only the Svelte grammar, so `const x = { foo }` parses as a mustache and yields a bogus `read` ref. Claiming them would trade working facts for misparse noise. **Script-block facts are out by construction, not omission.** Props, runes (`$state`, `$props`, `$derived`), script-local functions and imports all live inside `<script>`, which `tree-sitter-svelte` hands over as a single `raw_text` node — it ships `queries/injections.scm` precisely because injection is the only way in, and this host has none. Tracked as #275 for every format of this shape (`.vue`, `.astro`, `.mdx`, `.razor`). ### #229's missing input, built A component's name is its FILENAME and appears nowhere in its bytes, so `<Button />` had nothing to resolve to. `TAG_SYMBOL_BASENAME` (0x8004) is the shape `crates/abi/src/record.rs` had specified and left unbuilt: the guest emits an ordinal naming a symbol it already emitted, the host substitutes the stem of the path it routed the file by, and no string crosses the sandbox boundary. `ABI_MINOR` moved 1 → 2 with it, and that was a debt rather than a courtesy. An optional tag normally degrades safely; this one cannot, because a symbol's `name` has no absent encoding — an older host stores the guest's placeholder AS IF IT WERE A NAME. It misleads, which is worse than degrading and worse than refusing. So the manifest brackets `fact_minor_min = 2` and an older host REFUSES the package outright (measured: `archive_refused: manifest.abi_unsupported (ABI_MINOR = 1, observed 2)`). ### Corrections worth recording * The grammar named in the original Milestone 1 would have fetched `tree-sitter-svelte` 0.10.2 — **2022**, abandoned, and predating Svelte 5 entirely. The maintained fork is `tree-sitter-svelte-ng` 1.0.2 (ABI 14), now pinned and byte-reproducible. * The original spec's two bridges were wrong twice over: `scope = "workspace"` is not a value `evidence_for_scope` admits, and a component is not a TypeScript type or a JavaScript class. Svelte-to-Svelte is same-language resolution; there are no bridges. * The boundary fixture was **vacuous at first**. Disabling the `<script>` guard entirely left the suite GREEN, because a script body is a single `raw_text` LEAF and no rule reads a bare `raw_text` — descending into one and refusing to are byte-identical output. It grades something only because the fixtures now carry a mustache on the boundary element's OWN open tag. ### Filed from this work * **#275** — packages cannot see inside a multi-language file (one grammar per package, no injection). The unblocking work for the script block and for the cross-directory bind. * **#276** — CI formats, lints and rustdocs no shipped guest crate. All four are outside the workspace and outside every gate; they were run by hand for this package.
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#268
No description provided.