Plugin Package: Svelte & SvelteKit Support (de.h-dv.svelte) #268
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#268
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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-ngexposes the entire script body as ONEraw_textnode and shipsqueries/injections.scmbecause 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
Button,+page,+layout,+error)SymbolKind::Module,Visibility::Exported{#snippet row(item)}node-types.jsonSymbolKind::Function{@render row(x)}RefKind::Call<Header />,<Modal />RefKind::Typeonclick={handleClick}RefKind::Call{data.title}RefKind::Read{#if},{#each},{#await},{#key}Component resolution is same-language:
<Button />resolves toButton.sveltethroughexported_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>:export let x(Svelte 3/4) andlet { x } = $props()(Svelte 5)$state,$derived,$effect(MEASURED: 0 node types each; they are JavaScript expressions, not template syntax)function handleClick() {}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]acceptssuffixesas well asextensions, andpath_eligibility_infalls through toselect_pluginwhendisplacing_routedeclines, so ordinary.tsand.jsfiles still reach the compiled-in plugins.It requires consent against the builtins:
THE CONSENT OVERSTATES WHAT IS TAKEN, AND THAT IS ACCEPTED RATHER THAN UNNOTICED.
builtin::displaced_bykeys consent on the BUILTIN's whole key, so the operator confirmation reads "displacestypescript 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:
*.svelte.ts/*.svelte.jsand that ordinary.ts/.jscontinue to the built-in plugins. An operator reading only the consent prompt would conclude otherwise.svelte_package_e2e.rsMUST contain a test proving it: a project holdinga.ts,b.js,c.svelte.tsandd.svelte.js, with this package enabled, indexes the first two with the BUILTIN languages and the last two withde.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 = 1targets are all correct as originally written.Corrected manifest (
tests/packages/svelte/plugin.toml)Runes.sveltestays 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.svelteis the parse-error decoy both shipped packages carry (Broken.xaml,broken.rb.rbx) and the original spec omitted.Revised milestones
tests/grammars/build-tree-sitter-svelte.sh, followingbuild-tree-sitter-ruby.shbyte for byte: fetch the pinned.crate,sha256sum -cit, compilesrc/parser.c+src/scanner.cto wasm,sha256sum -cthe output.VERSION=1.0.2,CRATE=tree-sitter-svelte-ng,CRATE_SHA256=ef0a71f9cf5e94373cc86c64893630c8a29bb25d3390a248268d08af2165fa37tree-sitter-svelte.wasmfrom the npm package —grammar_provenance.rspinsWASM_ARTIFACTSby hash and a vendored binary has no recipe anyone can re-run.grammar_provenance.rs'sWASM_ARTIFACTS.crates/guest/svelte(Cargo.toml,build.sh, kind-id table).+page/+layout/+errorforms.{#snippet}→Function;{@render}→Call.RefKind::Type.Call; mustache reads →Read.raw_text. A<script>body is opaque at this ABI and must not be regex-scanned — see "rejected approach" below.bridge_source.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 thexaml_package_e2e.rsconvention: install, enable, thensearch_symbols/find_callers/file_outline/get_symbolover.svelte.<Button />inApp.svelteresolves toButton.svelte— the claim this package exists to make.Pack the Svelte language packagestep inrelease.yml.tests/packages/svelte.digest.distribution/registry.v1.json's derived set (the catalog enumeratestests/packages/*/plugin.tomlfrom disk, so this is automatic — confirm it, do not author it).Revised acceptance criteria
code-index plugin validate de.h-dv.svelte-0.1.0.cippasses with zero diagnostics.plugin installandplugin enablepromote the package into the active generation.search_symbolsfinds Svelte components and snippets in.sveltefiles.find_referenceson a component resolves<Button />usages toButton.svelte, same-language.$state,$propsand a script-localfunctionyields 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.a.ts,b.js,c.svelte.tsandd.svelte.jsindexes the first two with the BUILT-IN typescript/javascript languages and the last two withde.h-dv.svelte. The consent prompt says the builtin losesext:ts; this criterion is what proves the behaviour is narrower than the sentence.#![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-indextreats.sveltefiles as text-only (indexed_as: "text"). They participate in full-text search via FTS5, but are completely symbol-blind:<Button />,<Modal />).+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.sveltedynamic WebAssembly plugin package (Option B under Epic #75 / Spec 05).Filetype & System Scope
Based on the official Svelte 5 & SvelteKit documentation:
*.svelte<script>,<script module>, markup,<style>, runes, snippetsde.h-dv.svelte(Claimsext("svelte"))*.svelte.ts,*.svelte.js[[displaces]])+page.svelte,+layout.svelte,+error.sveltePageProps/LayoutPropsde.h-dv.svelte+page.ts/.js,+page.server.ts/.js,+server.ts/.jshooks.client.ts/.js,hooks.server.ts/.jsArchitectural & Package Design
The plugin will be distributed as an external, sandboxed CIP package (
de.h-dv.svelte-0.1.0.cip) compliant withfact_major = 1,host_min = 1, andpackage_format = 1.1. Package Manifest (
tests/packages/svelte/plugin.toml)2. Guest Extractor (
crates/guest/svelte)#![no_std]andpanic = "abort", linkingcode-index-guest.tree-sitter-svelteAST to emit:+page,+layout,Button).export let propandlet { prop } = $props()→SymbolKind::Field,Visibility::Exported.$state(...),$derived(...)→SymbolKind::Variable.function handleClick()→SymbolKind::Function.{#snippet row(item)}→SymbolKind::Function.<Header />,<Modal />) →RefKind::TypeorRefKind::Call.onclick={handleClick},{data.title}→RefKind::Call/RefKind::Read.import ... from '...'→enc.import(...)+RefKind::Import.3. Build & Deterministic Packaging Pipeline
CARGO_INCREMENTAL=0 cargo build --release --target wasm32-unknown-unknowncode-index-plugin-host --strip-name-sectioncode-index-plugin-host --inject-globalscode-index plugin pack tests/packages/svelte --out de.h-dv.svelte-0.1.0.ciptests/packages/svelte.digest.Work Breakdown & Milestones
tree-sitter-svelte.wasm(ABI 13–15).crates/guest/sveltewithCargo.toml,build.sh, andgen_kinds.py.<script>, and<script module>parsing.export let+ Svelte 5$props()).$state,$derived).{#snippet ...}).<Component />), event directives, mustaches.tests/packages/svelte/plugin.tomlwith resolver pool grants.svelte↔typescript/javascript.Simple.svelte(Svelte 3/4) andRunes.svelte(Svelte 5) fixtures with.expectedfacts.crates/daemon/tests/svelte_package_e2e.rsvalidating package install, enable, and MCP queries (search_symbols,find_callers,file_outline,get_symbol).Pack the Svelte language packagejob in release workflow.tests/packages/svelte.digest.Acceptance Criteria
code-index plugin validate de.h-dv.svelte-0.1.0.cippasses with zero diagnostics.code-index plugin installandenablecleanly promotes the package into the active generation.search_symbolsdiscovers Svelte components, props, state, and snippets in.sveltefiles.find_referenceson imported components resolves to their usages across<template>markup.#![deny(warnings)], Clippy, and zero test regressions.Review
Measured against the tree at
77f843drather than read on its own terms. The package is worth building and the file-type scoping is right, but four things would stop it atplugin validateor 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:Acceptance criterion 1 ("
plugin validatepasses with zero diagnostics") fails as written.B2 — the bridges point at the wrong destination anyway.
<Button />resolves toButton.svelte, not to a TypeScripttypeor a JavaScriptclass. Svelte-to-Svelte is SAME-LANGUAGE resolution throughexported_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_sourceis missing from[capabilities] resolver.tests/packages/xaml— the only working bridge package in the tree — declaresresolver = ["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:142ispub grammar: Grammar(notOption, notVec), 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>, whichtree-sitter-svelteexposes 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,.mdxand.razorare the same shape.Good news — two places this issue is too cautious
.svelteneeds no shadow extension.BUILTIN_CLAIMSholds rust, python, typescript, javascript, csharp, php and ruby —svelteis not among them, soext("svelte")is claimable outright. None of the.rbx-style contortiontests/packages/rubyneeded applies here..svelte.ts/.svelte.jsARE expressible today.[claims.include]acceptssuffixesas well asextensions, sosuffixes = [".svelte.ts"]plus[[displaces]] typescript ext:tsworks, and routing falls through correctly —path_eligibility_incallsselect_pluginwhendisplacing_routedeclines, so ordinary.tsfiles 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
Broken.xaml,broken.rb.rbx); this spec has none. The manifest also declares one fixture while Milestone 4 names two.tree-sitter-sveltein-tree —tests/grammars/holds only ruby and xml. Milestone 1 is genuinely new external work, and thets_abi_min/max = 13..15claim is unverified against the actual grammar._prdoc/guides/80-package-authoring.mdsays 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 validateis a real verb (crates/cli/src/plugin.rs:180);crates/daemon/tests/svelte_package_e2e.rsmatches thexaml_package_e2e.rsconvention;tests/packages/svelte.digestmatches 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:
Button,+page,+layout){#snippet row(item)}->Function<Header />->typeonclick={handleClick},{data.title}<Button />->Button.sveltethroughexported_candidateas same-language — drop both bridgesThat is a real package:
.sveltestops 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.Grammar and ABI: measured
My review above said the
ts_abi_min/max = 13..15claim 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-ng1.0.2,src/parser.c:7:ABI 14 — inside this host's
13..=15window, and the same ABI as thetree-sitter-ruby(14) andtree-sitter-xml(14) artifacts already intests/grammars/. Nothing about the ABI blocks this package.Milestone 1's source name would fetch a 2022 grammar
tree-sitter-sveltetree-sitter-sveltetree-sitter-svelte-ngtree-sitter-svelte-nextMilestone 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 istree-sitter-svelte-ng, which is the tree-sitter-grammars org fork and matches the provenance of the existingtree-sitter-xmlpin.-nextis 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, becausebuild-tree-sitter-ruby.shandbuild-tree-sitter-xml.shcompilesrc/parser.c+src/scanner.cto 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.shwould pin, following the existing scripts byte for byte:Do not use the prebuilt
tree-sitter-svelte.wasmthat ships in the npm package.grammar_provenance.rspinsWASM_ARTIFACTSby hash and the house pattern builds from pinned source withsha256sum -con 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:{#snippet …}{@render …}$props$state$derivedSo 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:The whole
<script>body is a singleraw_textnode, 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-ng1.0.2, nottree-sitter-svelte.Shipped —
de.h-dv.svelte0.1.0Landed in
12bfe8f(the package and the ABI input it needed) and09305a3(the SDK pin that could only move in a second commit). Closing..svelteis no longer symbol-blind, and<Button />resolves toButton.svelte.Revised acceptance criteria
plugin validatepasses with zero diagnosticsok, exit 0install+enablepromote into the active generationsvelte_package_e2esearch_symbolsfinds components and snippetsfind_referencesresolves<Button />toButton.svelte$state/$props/a script function yields NO symbols, and the doc says whythe_script_block_yields_nothing_and_that_is_the_boundaryplusScriptOnly.expected's twelve[[absent]]decoys*.svelte.tswas in scope. That claim was dropped, so the package declares no[[displaces]]table and there is nothing to route. Recorded rather than ticked.09305a3The one thing to read before using it: THE BIND IS DIRECTORY-LOCAL
<Button />binds toButton.sveltewhen both sit in ONE DIRECTORY, decided bytier1b_same_directory. A component insrc/lib/components/referenced fromsrc/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_candidateis 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 resolveButtonto whicheverButton.sveltecame first — in a tree holding bothsrc/lib/components/Button.svelteandsrc/routes/admin/Button.sveltethat 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_ceilinggoes 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.jsare NOT claimed, having been moved into v0.1.0 and then back out. The claim mechanism works —suffixesplus[[displaces]], withdisplacing_routefalling through so ordinary.tsstill 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, soconst x = { foo }parses as a mustache and yields a bogusreadref. 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>, whichtree-sitter-sveltehands over as a singleraw_textnode — it shipsqueries/injections.scmprecisely 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 shapecrates/abi/src/record.rshad 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_MINORmoved 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'snamehas 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 bracketsfact_minor_min = 2and an older host REFUSES the package outright (measured:archive_refused: manifest.abi_unsupported (ABI_MINOR = 1, observed 2)).Corrections worth recording
tree-sitter-svelte0.10.2 — 2022, abandoned, and predating Svelte 5 entirely. The maintained fork istree-sitter-svelte-ng1.0.2 (ABI 14), now pinned and byte-reproducible.scope = "workspace"is not a valueevidence_for_scopeadmits, and a component is not a TypeScript type or a JavaScript class. Svelte-to-Svelte is same-language resolution; there are no bridges.<script>guard entirely left the suite GREEN, because a script body is a singleraw_textLEAF and no rule reads a bareraw_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
plugin addgrantsderived_namesby default and never names it on the screen the operator answers #277