@svelte-vitals/core 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -16
- package/dist/internal.d.ts +380 -264
- package/dist/internal.js +976 -1069
- package/dist/json-OdgY5Bpw.d.ts +1287 -0
- package/dist/markdown-CUNuKq3Z.js +9895 -0
- package/package.json +2 -2
- package/dist/chunk-M7RA6QQW.js +0 -7994
- package/dist/index-Cx3Mi_d4.d.ts +0 -1255
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
//#region src/component.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Component-body facts for the Correctness category — the source-analysis boundary
|
|
4
|
+
* (mirrors images.ts / headings.ts). Collected by the static (CLI) provider only;
|
|
5
|
+
* the rendered provider can't see reactivity, so correctness rules no-op there.
|
|
6
|
+
*/
|
|
7
|
+
/** An `{#each}` block in a component template. */
|
|
8
|
+
interface EachBlockFact {
|
|
9
|
+
/** True when the block has a key, e.g. `{#each items as item (item.id)}`. */
|
|
10
|
+
hasKey: boolean;
|
|
11
|
+
/** 1-based source line, or 0 if unknown. */
|
|
12
|
+
line: number;
|
|
13
|
+
/** Set when the block's key expression is its index binding or a trivial coercion of it — `(i)`, `(String(i))`, `(Number(i))`, `` (`${i}`) ``, `(i.toString())`, `(i + '')` — correctness/each-index-key. */
|
|
14
|
+
indexKey?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** An `$effect(...)` / `$effect.pre(...)` call in a component's instance script. */
|
|
17
|
+
interface EffectFact {
|
|
18
|
+
/** 1-based source line, or 0 if unknown. */
|
|
19
|
+
line: number;
|
|
20
|
+
/** True when the effect body only assigns to `$state` variables (the "use $derived" smell). */
|
|
21
|
+
assignsOnlyState: boolean;
|
|
22
|
+
/** True when this $effect has a NON-EMPTY body that reads no reactive value and makes no bare call — it never re-runs, so it should be onMount (correctness/effect-as-onmount). */
|
|
23
|
+
mountOnly: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** A `$effect` guaranteed to run outside component initialisation — it throws `effect_orphan` at runtime (correctness/orphan-effect). */
|
|
26
|
+
interface OrphanEffectFact {
|
|
27
|
+
/** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
|
|
28
|
+
line: number;
|
|
29
|
+
/** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor creates a bare effect. */
|
|
30
|
+
kind: 'top-level' | 'constructor-instantiated';
|
|
31
|
+
/** Class name when kind is 'constructor-instantiated' (used in the finding message). */
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A svelte lifecycle/context call guaranteed to run outside component initialisation — it throws `lifecycle_outside_component` at runtime (correctness/orphan-lifecycle). */
|
|
35
|
+
interface OrphanLifecycleCallFact {
|
|
36
|
+
/** Canonical svelte export name (alias-resolved), e.g. 'onMount'. */
|
|
37
|
+
name: string;
|
|
38
|
+
/** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
|
|
39
|
+
line: number;
|
|
40
|
+
/** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor calls a tracked function. */
|
|
41
|
+
kind: 'top-level' | 'constructor-instantiated';
|
|
42
|
+
/** Class name when kind is 'constructor-instantiated' (used in the finding message). */
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
/** A browser-only global read in code that runs on the server — SSR crashes with "<name> is not defined" (correctness/server-browser-global, correctness/instance-browser-global). */
|
|
46
|
+
interface BrowserGlobalRefFact {
|
|
47
|
+
/** The global's name, e.g. 'window'. */
|
|
48
|
+
name: string;
|
|
49
|
+
/** 1-based source line, or 0 if unknown. */
|
|
50
|
+
line: number;
|
|
51
|
+
/** 'module' = module evaluation (script module / runes module — correctness/server-browser-global); 'instance' = component-init top level (runs on the server during SSR — correctness/instance-browser-global). */
|
|
52
|
+
context: 'module' | 'instance';
|
|
53
|
+
}
|
|
54
|
+
/** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
|
|
55
|
+
interface SourceSpan {
|
|
56
|
+
/** 1-based source line, or 0 if unknown. */
|
|
57
|
+
line: number;
|
|
58
|
+
}
|
|
59
|
+
/** An inline `svelte-vitals-disable-next-line` directive found in the component's source (issue #92). */
|
|
60
|
+
interface SuppressionDirective {
|
|
61
|
+
/** 1-based line the directive suppresses (the line immediately after the comment). */
|
|
62
|
+
line: number;
|
|
63
|
+
/** Rule ids suppressed on that line; undefined = suppress every rule on that line. */
|
|
64
|
+
ruleIds?: string[];
|
|
65
|
+
}
|
|
66
|
+
/** An `<input type="checkbox">` / `<input type="radio">` element carrying a `bind:value`
|
|
67
|
+
* directive — `bind:value` observes the DOM `value` property, which checkbox/radio
|
|
68
|
+
* interaction never changes, so the bound state silently never updates
|
|
69
|
+
* (correctness/checkable-bind-value). */
|
|
70
|
+
interface CheckableBindValueFact {
|
|
71
|
+
/** Which checkable input type was flagged — selects the message wording. */
|
|
72
|
+
kind: 'checkbox' | 'radio';
|
|
73
|
+
/** 1-based source line, or 0 if unknown. */
|
|
74
|
+
line: number;
|
|
75
|
+
}
|
|
76
|
+
/** A root-relative navigation literal — broken when the app is served under `kit.paths.base`
|
|
77
|
+
* (correctness/base-path-navigation). Shared by the component and Kit-module channels. */
|
|
78
|
+
interface BasePathLinkFact {
|
|
79
|
+
/** Which navigation surface it was written on — selects the message wording. */
|
|
80
|
+
kind: 'href' | 'goto' | 'redirect';
|
|
81
|
+
/** The literal path as written, e.g. '/about'. */
|
|
82
|
+
path: string;
|
|
83
|
+
/** 1-based source line, or 0 if unknown. */
|
|
84
|
+
line: number;
|
|
85
|
+
}
|
|
86
|
+
/** An interactive element (e.g. `<button>`) found nested inside another interactive
|
|
87
|
+
* container (e.g. `<a href>`) (a11y/interactive-nesting). */
|
|
88
|
+
interface InteractiveNestingFact {
|
|
89
|
+
containerTag: string;
|
|
90
|
+
/** The container's literal `role`, when that is what made it a container rather than its tag. */
|
|
91
|
+
containerRole?: string;
|
|
92
|
+
descendantTag: string;
|
|
93
|
+
/** 1-based source line of the descendant, or 0 if unknown. */
|
|
94
|
+
line: number;
|
|
95
|
+
}
|
|
96
|
+
/** A `button`/`a href`/`input type="image"` with no computable accessible name (a11y/accessible-name). */
|
|
97
|
+
interface UnnamedInteractiveFact {
|
|
98
|
+
tag: string;
|
|
99
|
+
/** 1-based source line, or 0 if unknown. */
|
|
100
|
+
line: number;
|
|
101
|
+
}
|
|
102
|
+
/** An element carrying a `role` and/or `aria-*` attribute(s) (a11y ARIA rules). */
|
|
103
|
+
interface AriaElementFact {
|
|
104
|
+
tag: string;
|
|
105
|
+
/** 1-based source line, or 0 if unknown. */
|
|
106
|
+
line: number;
|
|
107
|
+
/** literal role value; undefined = no role attr; { expression: true } = dynamic */
|
|
108
|
+
role?: {
|
|
109
|
+
literal?: string;
|
|
110
|
+
expression?: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** every aria-* attribute on the element */
|
|
113
|
+
aria: {
|
|
114
|
+
name: string;
|
|
115
|
+
literal?: string;
|
|
116
|
+
expression?: boolean;
|
|
117
|
+
line: number;
|
|
118
|
+
}[];
|
|
119
|
+
/** literal `type` of an `<input>`, lowercased; undefined for non-inputs or a dynamic type */
|
|
120
|
+
inputType?: string;
|
|
121
|
+
/** an `<input>` carrying a `list` attribute — its implicit role is `combobox` and the host supplies `aria-expanded` */
|
|
122
|
+
hasList?: true;
|
|
123
|
+
/**
|
|
124
|
+
* A `<select>`'s native role: `combobox` with no `multiple` and no `size` above 1, `listbox`
|
|
125
|
+
* otherwise; absent for a non-select or when a dynamic `size` leaves it unknowable.
|
|
126
|
+
*/
|
|
127
|
+
selectKind?: 'combobox' | 'listbox';
|
|
128
|
+
/** Set when the element also carries a spread attribute — its full attribute set is
|
|
129
|
+
* unknowable, so required-prop presence checks must treat it as satisfied (a11y/required-aria-props). */
|
|
130
|
+
hasSpread?: true;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Every element in a component with its literal attribute names — the input for the rules that
|
|
134
|
+
* judge against the HTML spec data (a11y/deprecated-element, a11y/deprecated-attr, and the rest of
|
|
135
|
+
* that family). Tag and attribute names are lowercased, matching how HTML parses them.
|
|
136
|
+
*/
|
|
137
|
+
interface ElementFact {
|
|
138
|
+
tag: string;
|
|
139
|
+
/** 1-based source line, or 0 if unknown. */
|
|
140
|
+
line: number;
|
|
141
|
+
/**
|
|
142
|
+
* Literal attribute names on the element (directives, spreads and expression-only names excluded).
|
|
143
|
+
* The per-attribute line is not what the deprecation rules anchor to — they anchor at the start
|
|
144
|
+
* tag so a `disable-next-line` directive can reach a multi-line element — but a value-level rule
|
|
145
|
+
* (`invalid-attr`) may want it for its message.
|
|
146
|
+
*/
|
|
147
|
+
attrs: {
|
|
148
|
+
name: string;
|
|
149
|
+
line: number;
|
|
150
|
+
value?: string;
|
|
151
|
+
}[];
|
|
152
|
+
/**
|
|
153
|
+
* Inside an `<svg>` subtree, or in a component declaring `<svelte:options namespace="svg" />`.
|
|
154
|
+
* `<foreignObject>` returns to HTML. Names collide across the two namespaces (`a`, `script`,
|
|
155
|
+
* `style`, `title`), so HTML-only rules must skip these.
|
|
156
|
+
*/
|
|
157
|
+
inSvg?: true;
|
|
158
|
+
/**
|
|
159
|
+
* Index of the nearest literal ancestor element in the same array (push-before-children DFS
|
|
160
|
+
* keeps it sound), looking through `{#if}`/`{#each}`/`{#await}`/`{#key}`. Absent at template
|
|
161
|
+
* root and after every construct whose rendering position is not lexical — a component,
|
|
162
|
+
* `<svelte:element>`, `<slot>`, `{@render}`, `{@html}`, a custom element or unknown tag,
|
|
163
|
+
* a `{#snippet}` body root, `<svelte:head>` children — so `a11y/permitted-contents` never
|
|
164
|
+
* judges across one.
|
|
165
|
+
*/
|
|
166
|
+
parent?: number;
|
|
167
|
+
/** A spread attribute is present — every attribute test on this element is unknowable. */
|
|
168
|
+
hasSpread?: true;
|
|
169
|
+
/**
|
|
170
|
+
* A direct child the static walk cannot see through (component, `{@html}`, `{@render}`,
|
|
171
|
+
* `<slot />`, `<svelte:element>`, a custom element or unknown tag) — `:has(...)` over this
|
|
172
|
+
* element's subtree is unknowable.
|
|
173
|
+
*/
|
|
174
|
+
unknownContent?: true;
|
|
175
|
+
}
|
|
176
|
+
/** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
|
|
177
|
+
interface ComponentFacts {
|
|
178
|
+
/** Source file the component came from. */
|
|
179
|
+
file: string;
|
|
180
|
+
eachBlocks: EachBlockFact[];
|
|
181
|
+
effects: EffectFact[];
|
|
182
|
+
/** `{@html …}` occurrences — raw-HTML render surfaces (security/raw-html). */
|
|
183
|
+
htmlTags: SourceSpan[];
|
|
184
|
+
/** Element attributes with a literal `javascript:` URL (security/javascript-url). */
|
|
185
|
+
javascriptUrls: SourceSpan[];
|
|
186
|
+
/** Source line count of the component file (architecture/component-size). */
|
|
187
|
+
loc: number;
|
|
188
|
+
/** Named props destructured from `$props()`; 0 when unknowable (rest / non-destructured) (architecture/prop-count). */
|
|
189
|
+
propCount: number;
|
|
190
|
+
/** Module specifiers of every `import` in the instance + module scripts (performance/heavy-import). */
|
|
191
|
+
imports: string[];
|
|
192
|
+
/**
|
|
193
|
+
* Module specifiers of every `import`, each with its source line (performance/heavy-import,
|
|
194
|
+
* architecture/route-component-import). `type` marks a declaration that contributes **no runtime
|
|
195
|
+
* value binding** — either `import type …`, or one whose every specifier is inline-typed
|
|
196
|
+
* (`import { type A } from …`). A specifier-less side-effect import is not marked: it still loads
|
|
197
|
+
* the module. Optional, so existing external constructors of `ComponentFacts` are unaffected.
|
|
198
|
+
*/
|
|
199
|
+
importSpans: {
|
|
200
|
+
source: string;
|
|
201
|
+
line: number;
|
|
202
|
+
type?: true;
|
|
203
|
+
}[];
|
|
204
|
+
/** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — performance/namespace-import. */
|
|
205
|
+
namespaceImports: {
|
|
206
|
+
source: string;
|
|
207
|
+
line: number;
|
|
208
|
+
}[];
|
|
209
|
+
/** `$state` declarations never written or escaped anywhere in the component — candidates for const (correctness/unmutated-state). */
|
|
210
|
+
constableStates: {
|
|
211
|
+
name: string;
|
|
212
|
+
line: number;
|
|
213
|
+
}[];
|
|
214
|
+
/** Mutations of a non-`$bindable` prop from `$props()`, or a legacy `export let` prop — member writes, `delete`, or a mutating method call (correctness/prop-mutation). `legacy` distinguishes which mode the prop was declared in (absent/false: `$props()`), since the fix differs — optional so existing external constructors of `ComponentFacts` are unaffected. */
|
|
215
|
+
mutatedProps: {
|
|
216
|
+
name: string;
|
|
217
|
+
line: number;
|
|
218
|
+
legacy?: boolean;
|
|
219
|
+
}[];
|
|
220
|
+
/** Top-level const/let bindings computed from a $props() or legacy `export let` prop without $derived (or `$:`), never reassigned or escaped, and referenced (eagerly) in the template — frozen at init (correctness/stale-prop-derivation). `legacy` distinguishes which mode the prop was declared in, since the fix differs — optional so existing external constructors of `ComponentFacts` are unaffected. */
|
|
221
|
+
stalePropDerivations: {
|
|
222
|
+
name: string;
|
|
223
|
+
line: number;
|
|
224
|
+
legacy?: boolean;
|
|
225
|
+
}[];
|
|
226
|
+
/** Object/array-literal $state bindings reassigned at least once but never mutated, escaped, aliased, or item-edited — $state.raw candidates (performance/state-raw). */
|
|
227
|
+
rawableStates: {
|
|
228
|
+
name: string;
|
|
229
|
+
line: number;
|
|
230
|
+
}[];
|
|
231
|
+
/** Plain built-in instances (Map/Set/Date/URL/URLSearchParams) in $state whose type-specific mutations were observed inside functions, with no exempting reassignment — untracked by reactivity (correctness/nonreactive-builtin-state). */
|
|
232
|
+
nonreactiveBuiltinStates: {
|
|
233
|
+
name: string;
|
|
234
|
+
type: string;
|
|
235
|
+
line: number;
|
|
236
|
+
}[];
|
|
237
|
+
/** `<input type="checkbox">` / `<input type="radio">` elements bound with `bind:value`
|
|
238
|
+
* instead of `bind:checked`/`bind:group` (correctness/checkable-bind-value). */
|
|
239
|
+
checkableBindValues: CheckableBindValueFact[];
|
|
240
|
+
/** Root-relative `<a href>` and `goto()` literals in this component (correctness/base-path-navigation). */
|
|
241
|
+
basePathLinks: BasePathLinkFact[];
|
|
242
|
+
/** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-effect). */
|
|
243
|
+
orphanEffects: OrphanEffectFact[];
|
|
244
|
+
/** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-lifecycle). */
|
|
245
|
+
orphanLifecycleCalls: OrphanLifecycleCallFact[];
|
|
246
|
+
/** Browser-global reads in server-executed positions of this file (correctness/server-browser-global, correctness/instance-browser-global). */
|
|
247
|
+
browserGlobalRefs: BrowserGlobalRefFact[];
|
|
248
|
+
/** Module-scope `$state` declarations in a `.svelte.ts`/`.svelte.js` runes module — on a server, one instance shared by every request (security/shared-state-import). Always empty for `.svelte` files. */
|
|
249
|
+
moduleStateDecls: {
|
|
250
|
+
name: string;
|
|
251
|
+
line: number;
|
|
252
|
+
}[];
|
|
253
|
+
/** Inline `svelte-vitals-disable-next-line` directives found in this file's source — component-rule escape hatch (issue #92). Optional: absent is equivalent to no directives, so existing external constructors of `ComponentFacts` are unaffected. */
|
|
254
|
+
suppressions?: SuppressionDirective[];
|
|
255
|
+
/** Markdown links `[label](url)` appearing inside a comment (architecture/doc-link-target). */
|
|
256
|
+
commentLinks: {
|
|
257
|
+
url: string;
|
|
258
|
+
line: number;
|
|
259
|
+
}[];
|
|
260
|
+
/** Elements carrying a role or any aria-* attribute (a11y ARIA rules). */
|
|
261
|
+
ariaElements?: AriaElementFact[];
|
|
262
|
+
/** Every element with its attribute names and SVG-namespace flag (the HTML spec-data rules). */
|
|
263
|
+
elements?: ElementFact[];
|
|
264
|
+
/** Interactive elements nested inside another interactive container (a11y/interactive-nesting). */
|
|
265
|
+
interactiveNestings?: InteractiveNestingFact[];
|
|
266
|
+
/** `button`/`a href`/`input type="image"` elements with no computable accessible name (a11y/accessible-name). */
|
|
267
|
+
unnamedInteractive?: UnnamedInteractiveFact[];
|
|
268
|
+
/** `<label>` elements with neither a `for` attribute nor a wrapped labelable descendant (a11y/label-has-control). */
|
|
269
|
+
unassociatedLabels?: {
|
|
270
|
+
line: number;
|
|
271
|
+
}[];
|
|
272
|
+
/** Text nodes whose trimmed content opens with a bullet character followed by whitespace, outside any `li` (a11y/use-list). */
|
|
273
|
+
bulletTexts?: {
|
|
274
|
+
line: number;
|
|
275
|
+
char: string;
|
|
276
|
+
}[];
|
|
277
|
+
/** `<select required>` (no `multiple`, display size absent or ≤ 1) whose first `option` element
|
|
278
|
+
* child is not a placeholder label option (a11y/placeholder-label-option). */
|
|
279
|
+
selectsMissingPlaceholder?: {
|
|
280
|
+
line: number;
|
|
281
|
+
}[];
|
|
282
|
+
/** `<time>` with no `datetime` attribute whose literal text content is not machine-readable (a11y/require-datetime). */
|
|
283
|
+
timesMissingDatetime?: {
|
|
284
|
+
line: number;
|
|
285
|
+
text: string;
|
|
286
|
+
}[];
|
|
287
|
+
/** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
|
|
288
|
+
parseFailed?: true;
|
|
289
|
+
/** Set when the file could not be READ — an environment problem (permissions, a descriptor
|
|
290
|
+
* limit), not a malformed component. Reported separately so one does not masquerade as the other. */
|
|
291
|
+
readFailed?: true;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Warnings for files a collector could not read or parse: the file contributes empty facts, so any
|
|
295
|
+
* findings it would have produced are simply missing rather than reported as fixed. The two causes
|
|
296
|
+
* are reported separately — an unreadable file is an environment problem (permissions, a descriptor
|
|
297
|
+
* limit) and a malformed one is the author's, and sharing a message is how a descriptor limit once
|
|
298
|
+
* read as hundreds of broken components. Capped at 10 inline paths so one badly-broken directory
|
|
299
|
+
* cannot flood the terminal.
|
|
300
|
+
*/
|
|
301
|
+
declare function skippedFileWarnings(facts: readonly {
|
|
302
|
+
file: string;
|
|
303
|
+
parseFailed?: true;
|
|
304
|
+
readFailed?: true;
|
|
305
|
+
}[]): string[];
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/types.d.ts
|
|
308
|
+
type Severity = 'critical' | 'warning' | 'info';
|
|
309
|
+
/** Where a head tag is set, relative to the route being evaluated (design §4). */
|
|
310
|
+
type Presence = 'own' | 'inherited' | 'none';
|
|
311
|
+
/** How a tag's value is determined (design §4). */
|
|
312
|
+
type Value = 'static' | 'dynamic' | 'absent';
|
|
313
|
+
/**
|
|
314
|
+
* Two-axis detection result. Kept independent so combinations such as
|
|
315
|
+
* "inherited + dynamic" remain expressible (design §4).
|
|
316
|
+
*/
|
|
317
|
+
interface Detection {
|
|
318
|
+
presence: Presence;
|
|
319
|
+
value: Value;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* One compiled SvelteKit alias entry, in the order Kit builds them (`get_config_aliases` in
|
|
323
|
+
* `@sveltejs/kit/src/exports/vite/utils.js`): `$lib` first, then `kit.alias` in declaration
|
|
324
|
+
* order. Resolution takes the FIRST matching entry, exactly as Vite's alias plugin does, so
|
|
325
|
+
* **position is precedence** — the list is never sorted and a longer `find` never wins on
|
|
326
|
+
* length alone.
|
|
327
|
+
*/
|
|
328
|
+
interface KitAlias {
|
|
329
|
+
/** The alias key, with any trailing `/*` removed. */
|
|
330
|
+
find: string;
|
|
331
|
+
/**
|
|
332
|
+
* The project-relative target: posixified, with any trailing `/*` and any trailing slashes
|
|
333
|
+
* removed. `null` when the config's value is not a string literal — such an entry still
|
|
334
|
+
* matches (holding its position and its mode) but resolves to undefined, so a specifier we
|
|
335
|
+
* cannot resolve stays unresolved instead of falling through to a later entry.
|
|
336
|
+
*/
|
|
337
|
+
replacement: string | null;
|
|
338
|
+
/**
|
|
339
|
+
* How `find` matches a specifier, mirroring Kit's three compiled entry shapes:
|
|
340
|
+
* - `prefix` — `spec === find` or `spec.startsWith(find + '/')`; a plain key.
|
|
341
|
+
* - `contents` — `spec.startsWith(find + '/')` only; from a `key/*` key, which Kit
|
|
342
|
+
* documents as matching "the contents of a directory, not the directory itself".
|
|
343
|
+
* - `exact` — `spec === find` only; a plain key whose `key/*` form is ALSO declared, which
|
|
344
|
+
* is how Kit stops the plain key from swallowing the nested specifiers.
|
|
345
|
+
*/
|
|
346
|
+
match: 'prefix' | 'contents' | 'exact';
|
|
347
|
+
}
|
|
348
|
+
/** Project-wide facts precomputed by the runtime layer for project-scope rules (design §10). */
|
|
349
|
+
interface Project {
|
|
350
|
+
hasRobotsTxt: boolean;
|
|
351
|
+
hasSitemap: boolean;
|
|
352
|
+
/** <html lang> from app.html: presence 'own' when the attribute exists ('none' otherwise); value 'static' if non-empty, 'absent' if empty. */
|
|
353
|
+
htmlLang: Detection;
|
|
354
|
+
/** Whether the static static/robots.txt references a sitemap (`Sitemap:` line). Undefined for a +server endpoint / absent / unreadable. */
|
|
355
|
+
robotsReferencesSitemap?: boolean;
|
|
356
|
+
/**
|
|
357
|
+
* Set when the Vite config disables minification for production builds (performance/minify-disabled).
|
|
358
|
+
* `file` is the config path relative to the analyzed root (posix, may start with `../`
|
|
359
|
+
* in monorepos); unset for inline programmatic configs. `line` is 1-based and set only
|
|
360
|
+
* when the literal `minify: false` was located in that file; unset when the value was
|
|
361
|
+
* resolved at build time (plugin/conditional config). `suppressions` carries the config file's
|
|
362
|
+
* own inline directives, so a line-anchored finding in it can be silenced like any other.
|
|
363
|
+
*/
|
|
364
|
+
viteMinifyDisabled?: {
|
|
365
|
+
file?: string;
|
|
366
|
+
line?: number;
|
|
367
|
+
suppressions?: SuppressionDirective[];
|
|
368
|
+
};
|
|
369
|
+
/**
|
|
370
|
+
* Set when the project configures a non-empty `kit.paths.base` — read from the `sveltekit()`
|
|
371
|
+
* Vite plugin config, else `svelte.config.{js,ts}` (correctness/base-path-navigation).
|
|
372
|
+
* `value` is the literal base when statically resolvable, unset when the config computes it
|
|
373
|
+
* (e.g. `dev ? '' : '/repo'`). `file` is the config path relative to the analyzed root (posix).
|
|
374
|
+
* Absent means the app is served at the root — the rule stays silent.
|
|
375
|
+
*/
|
|
376
|
+
kitPathsBase?: {
|
|
377
|
+
value?: string;
|
|
378
|
+
file: string;
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* The project's compiled SvelteKit alias entries, in Kit's own order (`$lib` first), read from
|
|
382
|
+
* `svelte.config.{js,ts}`. Absent means no config was read — resolution then falls back to
|
|
383
|
+
* `$lib` → `src/lib`, which is what this analyzer assumed unconditionally before. A collected
|
|
384
|
+
* list is never empty: `$lib` is always prepended.
|
|
385
|
+
*/
|
|
386
|
+
kitAliases?: KitAlias[];
|
|
387
|
+
/**
|
|
388
|
+
* Whether `src/app.html` opens with `<!doctype html>` (a11y/doctype). Set from the same read
|
|
389
|
+
* as `htmlLang`; absent when the file wasn't read (missing or unreadable) — the rule stays
|
|
390
|
+
* silent then, like `viteMinifyDisabled`'s absent convention.
|
|
391
|
+
*/
|
|
392
|
+
appHtmlDoctype?: boolean;
|
|
393
|
+
/** Literal ids in src/app.html with the line each first appears on — shell content present on every rendered route. Absent when the file wasn't read. */
|
|
394
|
+
appHtmlIds?: {
|
|
395
|
+
id: string;
|
|
396
|
+
line: number;
|
|
397
|
+
}[];
|
|
398
|
+
/** Distinct lowercased tag names inside `app.html`'s `<body>` (a11y/required-element's presence set; static mode). */
|
|
399
|
+
appHtmlBodyTags?: string[];
|
|
400
|
+
}
|
|
401
|
+
declare const defaultProject: Project;
|
|
402
|
+
/** A concrete, agent-actionable remediation for a finding (design §10, issue #18). */
|
|
403
|
+
interface Fix {
|
|
404
|
+
/** One-line imperative instruction, e.g. 'Add a <meta name="description"> inside <svelte:head>.' */
|
|
405
|
+
description: string;
|
|
406
|
+
/** Concrete code to insert or a file's contents to create. */
|
|
407
|
+
snippet?: string;
|
|
408
|
+
/** Markdown fenced-code language for `snippet` (default 'svelte'). */
|
|
409
|
+
lang?: string;
|
|
410
|
+
}
|
|
411
|
+
/** A single rule finding for one route (or the whole project). */
|
|
412
|
+
interface Result {
|
|
413
|
+
/** Rule id, e.g. 'seo/title-presence'. */
|
|
414
|
+
id: string;
|
|
415
|
+
severity: Severity;
|
|
416
|
+
detection: Detection;
|
|
417
|
+
/** Route path, e.g. '/blog/[slug]'. Omitted for project-scoped rules. */
|
|
418
|
+
route?: string;
|
|
419
|
+
/** Source location, e.g. 'src/routes/blog/[slug]/+page.svelte'. */
|
|
420
|
+
location?: string;
|
|
421
|
+
message: string;
|
|
422
|
+
recommendation?: string;
|
|
423
|
+
docsUrl?: string;
|
|
424
|
+
/** Agent-actionable remediation (issue #18). */
|
|
425
|
+
fix?: Fix;
|
|
426
|
+
/** Vitals category this finding belongs to (default 'seo' when absent). */
|
|
427
|
+
category?: Category;
|
|
428
|
+
/** 1-based source line for element-level findings (e.g. a specific <img>). */
|
|
429
|
+
line?: number;
|
|
430
|
+
}
|
|
431
|
+
type Scope = 'route' | 'project' | 'component';
|
|
432
|
+
type Category = 'seo' | 'performance' | 'correctness' | 'security' | 'architecture' | 'a11y';
|
|
433
|
+
/**
|
|
434
|
+
* Every category, as a runtime list — for validating a user-supplied category
|
|
435
|
+
* name and naming the known ones in the error. One definition so a category
|
|
436
|
+
* added to `Category` can't be accepted by one validator and rejected by
|
|
437
|
+
* another. Not an ordering: reporters keep their own display order.
|
|
438
|
+
*/
|
|
439
|
+
declare const CATEGORIES: readonly Category[];
|
|
440
|
+
/** How dynamic (`{data.title}`) values are treated by scoring (design §4, §12). */
|
|
441
|
+
type TreatDynamicAs = 'pass' | 'warn' | 'fail';
|
|
442
|
+
/** Resolved option values handed to a rule at check time. */
|
|
443
|
+
type RuleOptions = Record<string, unknown>;
|
|
444
|
+
/**
|
|
445
|
+
* Object form of a rule setting. `severity` omitted keeps the rule's built-in
|
|
446
|
+
* severity — the common case when only a threshold is being moved.
|
|
447
|
+
* `{ severity: 'off', … }` disables the rule and any `options` beside it are
|
|
448
|
+
* inert (equivalent to the bare `'off'` string, not an error).
|
|
449
|
+
*/
|
|
450
|
+
interface RuleSettingObject {
|
|
451
|
+
severity?: Severity | 'off';
|
|
452
|
+
options?: RuleOptions;
|
|
453
|
+
}
|
|
454
|
+
/** Per-rule override: disable, change severity, and/or set options. */
|
|
455
|
+
type RuleSetting = 'off' | Severity | RuleSettingObject;
|
|
456
|
+
/**
|
|
457
|
+
* Scoped rule override (design 2026-07-18), applied to results after analysis.
|
|
458
|
+
* An entry matches a finding when any `route` glob matches its route id or any
|
|
459
|
+
* `files` glob matches its source location; at least one of the two must be
|
|
460
|
+
* set. Glob syntax: `*` matches within a segment, `**` across segments, a
|
|
461
|
+
* trailing `/**` also matches the bare prefix, and all other characters
|
|
462
|
+
* (including SvelteKit's `(`, `)`, `[`, `]`) are literal.
|
|
463
|
+
*/
|
|
464
|
+
interface RuleOverride {
|
|
465
|
+
/**
|
|
466
|
+
* Route-id glob(s), e.g. '/admin/**'. Note route ids drop `(group)` segments
|
|
467
|
+
* (`src/routes/(app)/dashboard` reports as '/dashboard') — target a group
|
|
468
|
+
* via `files` instead.
|
|
469
|
+
*/
|
|
470
|
+
route?: string | string[];
|
|
471
|
+
/** Source-path glob(s) matched against a finding's location, e.g. 'src/routes/(app)/**'. */
|
|
472
|
+
files?: string | string[];
|
|
473
|
+
/** Keys are rule ids ('seo/title-presence') or category names ('seo'). Rule id beats category within an entry. */
|
|
474
|
+
rules: Record<string, RuleSetting>;
|
|
475
|
+
}
|
|
476
|
+
interface Config {
|
|
477
|
+
treatDynamicAs: TreatDynamicAs;
|
|
478
|
+
/** Component names treated as meta sources of unknown content (design §11 layer 4). */
|
|
479
|
+
metaComponents: string[];
|
|
480
|
+
/** Per-rule overrides keyed by rule id (design §6). */
|
|
481
|
+
rules: Record<string, RuleSetting>;
|
|
482
|
+
/** Minimum severity that fails the run / CI (design §6). */
|
|
483
|
+
failOn: Severity;
|
|
484
|
+
/** Per-category weights for the combined Health score (default: equal, 1 each) (#10). */
|
|
485
|
+
weights?: Partial<Record<Category, number>>;
|
|
486
|
+
/** Route-/file-scoped rule overrides, applied to results after analysis (later entries win). */
|
|
487
|
+
overrides?: RuleOverride[];
|
|
488
|
+
}
|
|
489
|
+
declare const defaultConfig: Config;
|
|
490
|
+
/** Merge user config over defaults. Identity helper for config files (design §6). */
|
|
491
|
+
declare function defineConfig(config?: Partial<Config>): Config;
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/summary.d.ts
|
|
494
|
+
interface Summary {
|
|
495
|
+
critical: number;
|
|
496
|
+
warning: number;
|
|
497
|
+
info: number;
|
|
498
|
+
/** Passed (not penalized), including dynamic. */
|
|
499
|
+
passed: number;
|
|
500
|
+
/** Subset of passed that resolved dynamically (↯). */
|
|
501
|
+
dynamic: number;
|
|
502
|
+
}
|
|
503
|
+
/** Classify a single result for display/scoring (design §7, §12). */
|
|
504
|
+
type Classification = 'fail' | 'pass' | 'dynamic';
|
|
505
|
+
declare function classify(result: Result, config: Config): Classification;
|
|
506
|
+
/** A penalized dynamic finding is a warning under treatDynamicAs 'warn'; otherwise the rule's severity. */
|
|
507
|
+
declare function effectiveSeverity(result: Result, config: Config): Severity;
|
|
508
|
+
declare function summarize(results: Result[], config: Config): Summary;
|
|
509
|
+
/** Whether the run should fail the build/CI per the minimum failing severity. */
|
|
510
|
+
declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/reporter/github.d.ts
|
|
513
|
+
/**
|
|
514
|
+
* Render penalized findings as GitHub Actions workflow commands (issue #18, design slice 5).
|
|
515
|
+
* GitHub turns these into inline PR annotations and run-annotation entries. Returns '' when clean.
|
|
516
|
+
*/
|
|
517
|
+
declare function formatGithubReport(results: Result[], config: Config): string;
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/reporter/markdown.d.ts
|
|
520
|
+
/**
|
|
521
|
+
* Render a compact Markdown summary — Health score, per-category table, severity counts, and
|
|
522
|
+
* a findings table — suitable for a GitHub Actions job summary or a sticky PR comment
|
|
523
|
+
* (`svelte-vitals ci install`). Delegates all aggregation to `buildJsonReport` so the numbers
|
|
524
|
+
* never drift from the JSON/console reporters.
|
|
525
|
+
*/
|
|
526
|
+
declare function formatMarkdownReport(results: Result[], config: Config, meta: {
|
|
527
|
+
version: string;
|
|
528
|
+
}): string;
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/runtime.d.ts
|
|
531
|
+
/**
|
|
532
|
+
* Runtime abstraction (design §8). Core defines only the interface; concrete
|
|
533
|
+
* adapters (Node / Deno / Bun) live in the CLI package and are the only place
|
|
534
|
+
* allowed to touch runtime-specific I/O APIs. Providers and rules use this
|
|
535
|
+
* interface exclusively, which keeps them runtime-agnostic and lets tests inject
|
|
536
|
+
* an in-memory implementation.
|
|
537
|
+
*/
|
|
538
|
+
interface Runtime {
|
|
539
|
+
/** Read a UTF-8 text file. Rejects if the file does not exist. */
|
|
540
|
+
readFile(path: string): Promise<string>;
|
|
541
|
+
/** Whether a path exists. */
|
|
542
|
+
exists(path: string): Promise<boolean>;
|
|
543
|
+
/**
|
|
544
|
+
* Paths matching `pattern`, relative to `cwd`.
|
|
545
|
+
*
|
|
546
|
+
* **Dot files and dot directories are excluded**, and an adapter must keep it that way: the
|
|
547
|
+
* directory-shaped Architecture rules derive their directory set from these paths, and one of them
|
|
548
|
+
* enumerates a parent's children exhaustively, so a `.server/` appearing here would be reported as
|
|
549
|
+
* an undeclared name. Both shipped adapters pass `dot: false`.
|
|
550
|
+
*
|
|
551
|
+
* **Every returned path is a file, never a directory**, and an adapter must keep that true too:
|
|
552
|
+
* `architecture/reserved-directory-names`' unit test takes a directory's immediate children from
|
|
553
|
+
* this same inventory and asks whether one of them is a file named after the directory, so an
|
|
554
|
+
* adapter that let a directory through here would let a bare `Card/Card` satisfy that test as if it
|
|
555
|
+
* were an entry file. Both shipped adapters get this for free from their glob library's default,
|
|
556
|
+
* which returns files only unless asked to include directories.
|
|
557
|
+
*/
|
|
558
|
+
glob(pattern: string, cwd: string): Promise<string[]>;
|
|
559
|
+
/** Join path segments without depending on `node:path`. */
|
|
560
|
+
join(...parts: string[]): string;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* How many file reads may be in flight at once. Analysis reads every `.svelte` file in a project
|
|
564
|
+
* in parallel, which on a large project opens more descriptors than the process is allowed: at
|
|
565
|
+
* `ulimit -n 1024` — a common container default — a 1 681-route project raised `EMFILE`, and
|
|
566
|
+
* because a failed read lands in the same `catch` as a malformed component, 682 files were dropped
|
|
567
|
+
* and the run still reported a normal score. The cap is what keeps the analysis whole.
|
|
568
|
+
*
|
|
569
|
+
* 64 is chosen to sit well under the stock 256 on macOS while leaving descriptors for everything
|
|
570
|
+
* else the process holds open. It is not a throughput knob: reads are a few percent of the work.
|
|
571
|
+
*/
|
|
572
|
+
declare const READ_CONCURRENCY = 64;
|
|
573
|
+
/**
|
|
574
|
+
* `readFile` with at most `limit` reads in flight. A plain counter plus a queue of waiters —
|
|
575
|
+
* deliberately not a dependency, and pure enough to live in core.
|
|
576
|
+
*/
|
|
577
|
+
declare function withReadLimit(readFile: (path: string) => Promise<string>, limit?: number): (path: string) => Promise<string>;
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/head.d.ts
|
|
580
|
+
/**
|
|
581
|
+
* A normalized head tag. The mode-independent boundary (design §8): the static
|
|
582
|
+
* SourceHeadProvider (CLI, via the runtime-abstracted `HeadProvider` below) and
|
|
583
|
+
* the rendered collector (`@svelte-vitals/vite`, build-time Node) both emit
|
|
584
|
+
* these, so rules never need to know which mode produced them.
|
|
585
|
+
*/
|
|
586
|
+
interface HeadTag {
|
|
587
|
+
kind: 'title' | 'meta' | 'link' | 'jsonld' | 'script';
|
|
588
|
+
/** <meta name="...">. */
|
|
589
|
+
name?: string;
|
|
590
|
+
/** <meta property="..."> (e.g. og:image). */
|
|
591
|
+
property?: string;
|
|
592
|
+
/** <link rel="...">. */
|
|
593
|
+
rel?: string;
|
|
594
|
+
/** <link as="..."> keyword (e.g. 'font') when statically literal; undefined when absent or dynamically bound. */
|
|
595
|
+
as?: string;
|
|
596
|
+
/** True when a <link> has an `as` attribute at all (literal or dynamic). Distinguishes "no as" from "dynamic as". */
|
|
597
|
+
hasAs?: boolean;
|
|
598
|
+
/** True when a <link> has a `crossorigin` attribute (presence only; value is irrelevant to the checks). */
|
|
599
|
+
hasCrossorigin?: boolean;
|
|
600
|
+
/** True when a <meta name="robots"> literal content contains `noindex`/`none`. Undefined when dynamic or absent. */
|
|
601
|
+
noindex?: boolean;
|
|
602
|
+
/** Literal `<script type="application/ld+json">` content, set only when the script is static. Undefined when dynamic. */
|
|
603
|
+
jsonld?: string;
|
|
604
|
+
/** Literal visible text of a static <title> or <meta name="description"> content, set only when static. Undefined when dynamic. */
|
|
605
|
+
text?: string;
|
|
606
|
+
/** Literal `hreflang` of a `<link rel="alternate">` (e.g. 'en', 'en-US', 'x-default'). Undefined when dynamic/absent. */
|
|
607
|
+
hreflang?: string;
|
|
608
|
+
/** Literal href (link) / src (script) URL when static — used for third-party origin analysis (performance/preconnect). */
|
|
609
|
+
href?: string;
|
|
610
|
+
/** True for a render-blocking `<script src>` in <head> (no defer/async/module) (performance/render-blocking-script). */
|
|
611
|
+
blocking?: boolean;
|
|
612
|
+
/** Where this tag was set relative to the route. Never 'none' (absence = no tag). */
|
|
613
|
+
presence: Exclude<Presence, 'none'>;
|
|
614
|
+
/** Whether the tag's value is static/dynamic/absent (design §4). */
|
|
615
|
+
value: Value;
|
|
616
|
+
/** Source file the tag came from (static mode); unset on a rendered head. */
|
|
617
|
+
file?: string;
|
|
618
|
+
}
|
|
619
|
+
/** Resolved effective head for a single route (design §8). */
|
|
620
|
+
interface ResolvedHead {
|
|
621
|
+
/** Route path, e.g. '/blog/[slug]'. */
|
|
622
|
+
route: string;
|
|
623
|
+
/** Which provider produced this. */
|
|
624
|
+
source: 'static' | 'rendered';
|
|
625
|
+
/** Effective head tags after layout-chain composition. */
|
|
626
|
+
tags: HeadTag[];
|
|
627
|
+
/** Representative source file for the route (used for issue locations). */
|
|
628
|
+
file: string;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Supplies ResolvedHead[] for a project through the runtime abstraction. The
|
|
632
|
+
* static (CLI) mode implements this; rendered mode reads prerendered HTML at
|
|
633
|
+
* build time and emits the same ResolvedHead[] without the runtime indirection.
|
|
634
|
+
*/
|
|
635
|
+
interface HeadProvider {
|
|
636
|
+
mode: 'static' | 'rendered';
|
|
637
|
+
collect(rt: Runtime, cwd: string, config?: Config): Promise<ResolvedHead[]>;
|
|
638
|
+
}
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/images.d.ts
|
|
641
|
+
/**
|
|
642
|
+
* A normalized <img> occurrence — the mode-independent boundary for Performance
|
|
643
|
+
* rules (mirrors head.ts). Attribute presence only: a dynamically-bound attribute
|
|
644
|
+
* (width={w}) still counts as present, so dynamic values are never flagged.
|
|
645
|
+
*/
|
|
646
|
+
interface ImageInfo {
|
|
647
|
+
hasWidth: boolean;
|
|
648
|
+
hasHeight: boolean;
|
|
649
|
+
hasLoading: boolean;
|
|
650
|
+
/** True when the <img> has an `alt` attribute at all (incl. empty `alt=""` decorative; seo/image-alt). */
|
|
651
|
+
hasAlt: boolean;
|
|
652
|
+
/** True when the <img> has a literal `loading="lazy"` (performance/lcp-image). Dynamic/spread → false. */
|
|
653
|
+
lazy: boolean;
|
|
654
|
+
/** True when the <img> has a `srcset` attribute (performance/responsive-image). */
|
|
655
|
+
hasSrcset: boolean;
|
|
656
|
+
/** 1-based source line, or 0 if unknown. */
|
|
657
|
+
line: number;
|
|
658
|
+
/** Source file the <img> came from. */
|
|
659
|
+
file: string;
|
|
660
|
+
}
|
|
661
|
+
/** Resolved <img> elements for a single route (page + layout chain). */
|
|
662
|
+
interface ResolvedImages {
|
|
663
|
+
route: string;
|
|
664
|
+
images: ImageInfo[];
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/headings.d.ts
|
|
668
|
+
/**
|
|
669
|
+
* A normalized page-body heading occurrence — the mode-independent boundary for
|
|
670
|
+
* the heading-hierarchy rule (mirrors images.ts). Both providers collect these
|
|
671
|
+
* so seo/single-h1 never needs to know which mode produced them.
|
|
672
|
+
*/
|
|
673
|
+
interface HeadingInfo {
|
|
674
|
+
/** Heading level 1–6 (the `n` in <hn>). */
|
|
675
|
+
level: number;
|
|
676
|
+
/** 1-based source line, or 0 if unknown (rendered mode does not track lines). */
|
|
677
|
+
line: number;
|
|
678
|
+
/** Source file the heading came from. */
|
|
679
|
+
file: string;
|
|
680
|
+
}
|
|
681
|
+
/** Resolved page-body headings for a single route (page + layout chain). */
|
|
682
|
+
interface ResolvedHeadings {
|
|
683
|
+
route: string;
|
|
684
|
+
headings: HeadingInfo[];
|
|
685
|
+
/**
|
|
686
|
+
* Headings found in child components rendered (transitively) by this route's
|
|
687
|
+
* chain files — source mode only; absent in rendered mode. Kept separate from
|
|
688
|
+
* `headings` because their position in document order is unknown: safe for
|
|
689
|
+
* counting (seo/single-h1), unusable for outline order (seo/heading-level-skip).
|
|
690
|
+
*/
|
|
691
|
+
componentHeadings?: HeadingInfo[];
|
|
692
|
+
}
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/a11y.d.ts
|
|
695
|
+
/** One step of a template branch address: which exclusive block, and which arm of it. */
|
|
696
|
+
interface BranchStep {
|
|
697
|
+
/** index of the {#if}/{#await} block among its file's blocks (document order) */
|
|
698
|
+
group: number;
|
|
699
|
+
/** branch index within the group (if: 0..n consequent→else; await: 0=pending,1=then,2=catch) */
|
|
700
|
+
branch: number;
|
|
701
|
+
}
|
|
702
|
+
/** Where a folded occurrence sits, for the finding location. */
|
|
703
|
+
interface A11yOccurrenceInfo {
|
|
704
|
+
file: string;
|
|
705
|
+
line: number;
|
|
706
|
+
}
|
|
707
|
+
/** One reason a route's closed world failed to hold, with the first offending location. */
|
|
708
|
+
interface A11ySkipCause {
|
|
709
|
+
kind: 'component' | 'spread' | 'html' | 'dynamic-id';
|
|
710
|
+
file: string;
|
|
711
|
+
line: number;
|
|
712
|
+
/** for kind 'component': the unresolvable component's name as written */
|
|
713
|
+
detail?: string;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Route-scoped a11y facts, the mode-independent boundary for the landmark/id rules
|
|
717
|
+
* (mirrors headings.ts). Source mode composes the layout chain plus its resolved
|
|
718
|
+
* components; rendered mode reads the prerendered document.
|
|
719
|
+
*/
|
|
720
|
+
interface ResolvedA11y {
|
|
721
|
+
route: string;
|
|
722
|
+
/** representatives per landmark kind after the branch-aware fold ('main' | 'banner' | 'contentinfo' | 'complementary') */
|
|
723
|
+
landmarks: Record<string, A11yOccurrenceInfo[]>;
|
|
724
|
+
/** landmark occurrences nested inside another landmark after composition */
|
|
725
|
+
nestedLandmarks: {
|
|
726
|
+
kind: string;
|
|
727
|
+
within: string;
|
|
728
|
+
file: string;
|
|
729
|
+
line: number;
|
|
730
|
+
}[];
|
|
731
|
+
/** representatives per literal id */
|
|
732
|
+
ids: Record<string, A11yOccurrenceInfo[]>;
|
|
733
|
+
/** literal id references */
|
|
734
|
+
idRefs: {
|
|
735
|
+
id: string;
|
|
736
|
+
attr: string;
|
|
737
|
+
file: string;
|
|
738
|
+
line: number;
|
|
739
|
+
}[];
|
|
740
|
+
/** optimistic candidates: every literal id anywhere (all branches, each/snippet bodies, components, app.html) */
|
|
741
|
+
idCandidates: string[];
|
|
742
|
+
/** closed world holds: every component resolved, no depth truncation, no {@html}/spread, no dynamic id */
|
|
743
|
+
fullyResolved: boolean;
|
|
744
|
+
/** Why `fullyResolved` is false — deduped by (kind, file, detail), first occurrence's line kept. Present exactly when `fullyResolved` is false. */
|
|
745
|
+
unresolvedCauses?: A11ySkipCause[];
|
|
746
|
+
/**
|
|
747
|
+
* Distinct tag names in the route's body subtree — layout chain, page, every resolved component,
|
|
748
|
+
* and `app.html`'s `<body>` (static), or the prerendered `<body>` (rendered); optimistic across
|
|
749
|
+
* `{#if}` arms and `{#each}`/snippet bodies. Never `<svelte:head>` content, `<template>` children,
|
|
750
|
+
* or `<svelte:element>`. Absent where a provider does not collect it (a11y/required-element).
|
|
751
|
+
*/
|
|
752
|
+
elementTags?: string[];
|
|
753
|
+
/**
|
|
754
|
+
* The closed world for elements: every component descended into (an unresolved, depth-truncated,
|
|
755
|
+
* or — conservatively — cycle-cut one clears it), no `{@html}`, no `<svelte:element>`. Incomparable with `fullyResolved` — a spread or `id={expr}` clears that flag
|
|
756
|
+
* and not this one, since neither can hide an element; a `<svelte:element>` clears this and not
|
|
757
|
+
* that. "Missing" is only reportable when this holds; presence is sound regardless.
|
|
758
|
+
*/
|
|
759
|
+
elementsClosed?: boolean;
|
|
760
|
+
/** The file a route-level finding is anchored to: the page file (static) or the prerendered HTML path (rendered). */
|
|
761
|
+
file?: string;
|
|
762
|
+
}
|
|
763
|
+
type Foldable = {
|
|
764
|
+
key: string;
|
|
765
|
+
path: BranchStep[];
|
|
766
|
+
repeatable: boolean;
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
|
|
770
|
+
* occurrences sum, across the arms of one exclusive block the arm with the most
|
|
771
|
+
* occurrences wins (tie → lowest branch index) and ITS occurrences are the group's
|
|
772
|
+
* representatives — so a caller's count is always `list.length`, with a location per
|
|
773
|
+
* representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
|
|
774
|
+
* The max is per key: there is no scalar total to maximize.
|
|
775
|
+
*/
|
|
776
|
+
declare function foldOccurrences<T extends Foldable>(nodes: T[]): Map<string, T[]>;
|
|
777
|
+
/**
|
|
778
|
+
* Decode a fragment identifier the way navigation does before matching an element id
|
|
779
|
+
* (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
|
|
780
|
+
* the browser would also fail to decode them, so the raw text is the comparable form.
|
|
781
|
+
*/
|
|
782
|
+
declare function decodeFragmentId(fragment: string): string;
|
|
783
|
+
/** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
|
|
784
|
+
declare function splitTokens(value: string | undefined): string[];
|
|
785
|
+
/** Explicit `role` values that map to the landmark kinds the route rules inspect. */
|
|
786
|
+
declare const LANDMARK_ROLES: ReadonlySet<string>;
|
|
787
|
+
/**
|
|
788
|
+
* Attributes whose (whitespace-tokenized) values reference element ids: the ARIA id-reference and
|
|
789
|
+
* id-reference-list properties, and HTML's own (`for`, `list`, `headers`, `form`, the popover and
|
|
790
|
+
* command targets). `href="#…"` is handled separately — its value is a URL, not a token list.
|
|
791
|
+
*/
|
|
792
|
+
declare const IDREF_ATTRS: readonly string[];
|
|
793
|
+
/**
|
|
794
|
+
* Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
|
|
795
|
+
* case-insensitive) scrolls to the top when no element has that id, so it is never a missing
|
|
796
|
+
* reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
|
|
797
|
+
*/
|
|
798
|
+
declare function isTopFragment(id: string): boolean;
|
|
799
|
+
/**
|
|
800
|
+
* A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
|
|
801
|
+
* instructions for finding text and names no element, while anything before it is still an
|
|
802
|
+
* ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
|
|
803
|
+
* nothing. Returns an empty string when the fragment is a directive and nothing else.
|
|
804
|
+
*/
|
|
805
|
+
declare function stripTextDirective(fragment: string): string;
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/kit-module.d.ts
|
|
808
|
+
/**
|
|
809
|
+
* Facts parsed from one SvelteKit route/hooks file for the SSR shared-state rules
|
|
810
|
+
* (the security kit-module rules). Collected by `collectKitModuleFacts` (static/CLI + vite build mode).
|
|
811
|
+
*/
|
|
812
|
+
interface KitModuleFacts {
|
|
813
|
+
/** Repo-relative source file. */
|
|
814
|
+
file: string;
|
|
815
|
+
/** 'server' = runs only on the server (+*.server, +server, hooks.server); 'universal' = +page.ts/+layout.ts (still runs on the server during SSR). */
|
|
816
|
+
kind: 'server' | 'universal';
|
|
817
|
+
/** Module-scope let/var reassigned from inside a function (security/server-module-state). */
|
|
818
|
+
moduleStateReassignments: {
|
|
819
|
+
name: string;
|
|
820
|
+
line: number;
|
|
821
|
+
inHandler: boolean;
|
|
822
|
+
}[];
|
|
823
|
+
/** Writes to an imported binding from inside an exported handler (security/handler-state-write). */
|
|
824
|
+
importedStateWrites: {
|
|
825
|
+
name: string;
|
|
826
|
+
line: number;
|
|
827
|
+
via: 'assignment' | 'set-call';
|
|
828
|
+
}[];
|
|
829
|
+
/** Writes to an imported binding outside handlers — top level or helper functions (security/shared-state-import's write flavour). */
|
|
830
|
+
importedStateWritesOutsideHandlers: {
|
|
831
|
+
name: string;
|
|
832
|
+
line: number;
|
|
833
|
+
}[];
|
|
834
|
+
/**
|
|
835
|
+
* `.set()`/`.update()` in a handler on an import resolving under the `$lib` server root.
|
|
836
|
+
* The call shape alone cannot tell a persistence client (`db.set(…)`) from a hand-rolled
|
|
837
|
+
* in-memory store, so the decision needs the target module — which this pure parse cannot
|
|
838
|
+
* read. `collectKitModuleFacts` resolves each one and promotes the in-memory ones into
|
|
839
|
+
* `importedStateWrites`; a consumer that ignores this field sees the pre-arbitration
|
|
840
|
+
* behaviour, i.e. every one of these exempt.
|
|
841
|
+
*/
|
|
842
|
+
pendingServerStoreWrites: {
|
|
843
|
+
name: string;
|
|
844
|
+
imported: string;
|
|
845
|
+
resolved: string;
|
|
846
|
+
line: number;
|
|
847
|
+
}[];
|
|
848
|
+
/** Value imports whose specifier resolves to a repo-local `.svelte.ts`/`.svelte.js` runes module (security/shared-state-import). */
|
|
849
|
+
runesModuleImports: {
|
|
850
|
+
source: string;
|
|
851
|
+
resolved: string;
|
|
852
|
+
names: string[];
|
|
853
|
+
line: number;
|
|
854
|
+
}[];
|
|
855
|
+
/** Svelte lifecycle/context calls that run outside component initialisation — top level, handler bodies, or the `init` hook (correctness/orphan-lifecycle). */
|
|
856
|
+
lifecycleCalls: {
|
|
857
|
+
name: string;
|
|
858
|
+
line: number;
|
|
859
|
+
inHandler: boolean;
|
|
860
|
+
}[];
|
|
861
|
+
/** Browser-global reads in server-executed positions — top level, handler bodies, the `init` hook (correctness/server-browser-global). Empty when the file itself exports `ssr = false`. */
|
|
862
|
+
browserGlobalRefs: {
|
|
863
|
+
name: string;
|
|
864
|
+
line: number;
|
|
865
|
+
inHandler: boolean;
|
|
866
|
+
}[];
|
|
867
|
+
/** Root-relative `redirect()` literals in this Kit module (correctness/base-path-navigation). */
|
|
868
|
+
basePathLinks: BasePathLinkFact[];
|
|
869
|
+
/** Set when this file disables SSR via `export const ssr = false` (inline or same-file alias export) — the declaration's line (seo/ssr-disabled). */
|
|
870
|
+
ssrDisabled?: {
|
|
871
|
+
line: number;
|
|
872
|
+
};
|
|
873
|
+
/** Set when this file disables client-side rendering via `export const csr = false` (inline or same-file alias export). With no client runtime, a universal load only runs during SSR — performance/load-waterfall's browser-waterfall premise doesn't hold. */
|
|
874
|
+
csrDisabled?: {
|
|
875
|
+
line: number;
|
|
876
|
+
};
|
|
877
|
+
/** Sequential-await analysis of the exported `load` function (performance/load-waterfall, performance/sequential-awaits): 1-based lines of await sites that depend on an earlier await's result, and of sites independent of all earlier awaits. Set only when at least one list is non-empty. */
|
|
878
|
+
loadWaterfalls?: {
|
|
879
|
+
dependentLines: number[];
|
|
880
|
+
independentLines: number[];
|
|
881
|
+
};
|
|
882
|
+
/** Inline `svelte-vitals-disable-next-line` directives in this file. */
|
|
883
|
+
suppressions: SuppressionDirective[];
|
|
884
|
+
/** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
|
|
885
|
+
parseFailed?: true;
|
|
886
|
+
/** Set when the file could not be READ — an environment problem, not a malformed module. */
|
|
887
|
+
readFailed?: true;
|
|
888
|
+
}
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/config-apply.d.ts
|
|
891
|
+
/** The severity a setting selects: `'off'`, an explicit severity, or undefined (leave the built-in). */
|
|
892
|
+
declare function settingSeverity(setting: RuleSetting | undefined): Severity | 'off' | undefined;
|
|
893
|
+
/** The options a setting carries, or undefined for the string forms. */
|
|
894
|
+
declare function settingOptions(setting: RuleSetting | undefined): RuleOptions | undefined;
|
|
895
|
+
/** Drop rules disabled via config (design §6), including a `defaultOff` rule with no entry. */
|
|
896
|
+
declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
897
|
+
/**
|
|
898
|
+
* `config` with `failedRuleIds` (from `runRules`' `failedRules`) forced `'off'`: a rule that threw
|
|
899
|
+
* examined nothing, so leaving it in the inventory would score it as if it had run clean, silently
|
|
900
|
+
* inflating Health. Reuses the exact mechanism a `rules: { id: 'off' }` config entry already gets —
|
|
901
|
+
* `selectRules`/`buildInventory` both drop an `'off'` id from the denominator — rather than adding a
|
|
902
|
+
* second, parallel notion of "not counted" for callers to keep in sync.
|
|
903
|
+
*/
|
|
904
|
+
declare function withFailedRulesOff(config: Config, failedRuleIds: readonly string[]): Config;
|
|
905
|
+
/** One-line "rule failed and was skipped" warning; capped to the message's first line so a stack trace can't flood a terminal. */
|
|
906
|
+
declare function formatFailedRuleWarning(f: {
|
|
907
|
+
id: string;
|
|
908
|
+
message: string;
|
|
909
|
+
}): string;
|
|
910
|
+
/** Apply per-rule severity overrides to results (design §6). */
|
|
911
|
+
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
912
|
+
/** An override entry with its globs compiled once. Build with `compileOverrides`. */
|
|
913
|
+
interface CompiledOverride {
|
|
914
|
+
routes: RegExp[];
|
|
915
|
+
files: RegExp[];
|
|
916
|
+
rules: Record<string, RuleSetting>;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Compile every override entry's globs to RegExp, once. Callers that match many
|
|
920
|
+
* targets (every component, every route) must hoist this out of their loop.
|
|
921
|
+
*/
|
|
922
|
+
declare function compileOverrides(config: Config): CompiledOverride[];
|
|
923
|
+
/**
|
|
924
|
+
* Whether an override entry applies to a target. THE single definition of that
|
|
925
|
+
* question — the result post-pass and in-run option resolution both call it.
|
|
926
|
+
* Sharing this matcher is necessary but not sufficient for a severity override
|
|
927
|
+
* and an option override to select the same files: each caller must also pass
|
|
928
|
+
* the same `target` (route and, critically, `file`) the other path effectively
|
|
929
|
+
* matches against. See Finding 1, docs/superpowers/specs/2026-07-26-rule-options-design.md.
|
|
930
|
+
*/
|
|
931
|
+
declare function overrideMatches(o: CompiledOverride, target: {
|
|
932
|
+
route?: string;
|
|
933
|
+
file?: string;
|
|
934
|
+
}): boolean;
|
|
935
|
+
/**
|
|
936
|
+
* Apply route-/file-scoped overrides to results (design 2026-07-18). An entry
|
|
937
|
+
* matches when any `route` glob matches the finding's route id or any `files`
|
|
938
|
+
* glob matches its location (OR). `'off'` removes a matched result entirely —
|
|
939
|
+
* passing seeds included, so scoring and "checks passed" counts behave as if
|
|
940
|
+
* the rule never ran there. A severity value rewrites the result's severity.
|
|
941
|
+
* Entries are evaluated in order (later entries win); within one entry, a
|
|
942
|
+
* rule-id key beats a category key only when it specifies a `severity` — an
|
|
943
|
+
* options-only rule-id key (no `severity`) contributes its options but leaves
|
|
944
|
+
* the category key's severity in force, rather than shadowing it (design
|
|
945
|
+
* 2026-07-26, Finding 2 / second review Finding E).
|
|
946
|
+
*/
|
|
947
|
+
declare function applyOverrides(results: Result[], config: Config): Result[];
|
|
948
|
+
//#endregion
|
|
949
|
+
//#region src/rule-options.d.ts
|
|
950
|
+
/**
|
|
951
|
+
* One configurable option. `kind` decides the merge semantics, so no rule
|
|
952
|
+
* writes merge code of its own: `integer` replaces, and the two collection
|
|
953
|
+
* kinds ADD to the built-in default (never replace — see the design doc).
|
|
954
|
+
*/
|
|
955
|
+
type RuleOptionSpec = {
|
|
956
|
+
kind: 'integer';
|
|
957
|
+
default: number;
|
|
958
|
+
min?: number;
|
|
959
|
+
max?: number;
|
|
960
|
+
} | {
|
|
961
|
+
kind: 'string-list';
|
|
962
|
+
default: readonly string[];
|
|
963
|
+
/**
|
|
964
|
+
* Grammar every entry must match, checked at config load. A declaration-driven rule reserves
|
|
965
|
+
* its grammar with this so a value the rule does not interpret today (`'input[type=file]'`
|
|
966
|
+
* for a tag-name list) is rejected rather than accepted-and-ignored — accepting it would make
|
|
967
|
+
* giving it meaning later a reinterpretation of a value the frozen schema already took.
|
|
968
|
+
*/
|
|
969
|
+
pattern?: {
|
|
970
|
+
regex: RegExp;
|
|
971
|
+
describe: string;
|
|
972
|
+
};
|
|
973
|
+
} | {
|
|
974
|
+
kind: 'string-map';
|
|
975
|
+
default: Readonly<Record<string, string>>;
|
|
976
|
+
};
|
|
977
|
+
/** A rule's configurable options, keyed by option name. */
|
|
978
|
+
type RuleOptionsSpec = Record<string, RuleOptionSpec>;
|
|
979
|
+
/**
|
|
980
|
+
* Typed reads of a resolved options object. `RuleOptions` values are `unknown`
|
|
981
|
+
* (the map is open-ended by design), so without these every rule would carry
|
|
982
|
+
* its own `o.max as number` cast and the "resolution guarantees the declared
|
|
983
|
+
* kind" invariant would live in a dozen places instead of one. `resolveRuleOptions`
|
|
984
|
+
* always seeds every declared key from the spec default and validation rejects a
|
|
985
|
+
* wrongly-typed value up front, so a mismatch here means a rule read a key it
|
|
986
|
+
* never declared — the `fallback` keeps that a wrong number rather than a crash.
|
|
987
|
+
*/
|
|
988
|
+
declare function intOption(options: RuleOptions, key: string, fallback?: number): number;
|
|
989
|
+
/** As `intOption`, for a `string-list` option. */
|
|
990
|
+
declare function listOption(options: RuleOptions, key: string): string[];
|
|
991
|
+
/** As `intOption`, for a `string-map` option. */
|
|
992
|
+
declare function mapOption(options: RuleOptions, key: string): Record<string, string>;
|
|
993
|
+
/**
|
|
994
|
+
* Whether any config layer so much as mentions `ruleId` — its `rules` entry, or any `overrides`
|
|
995
|
+
* entry's.
|
|
996
|
+
*
|
|
997
|
+
* A rule that is inert until declared can return early on `false` instead of resolving options once
|
|
998
|
+
* per target and discarding the result. That waste is not hypothetical: the three directory-shaped
|
|
999
|
+
* Architecture rules resolve per directory, so an unconfigured project pays it for every directory
|
|
1000
|
+
* under `src/` three times over, on every dev-server save. Measured 2026-07-30 over a synthetic tree
|
|
1001
|
+
* of 1,523 directories: 5.4 ms per analysis, for rules that are off by default and therefore produce
|
|
1002
|
+
* nothing.
|
|
1003
|
+
*
|
|
1004
|
+
* Deliberately conservative. It asks only whether the rule is *mentioned*, not whether the mention
|
|
1005
|
+
* resolves to a non-empty value, so a `'off'` severity with no options still answers `true` and the
|
|
1006
|
+
* caller does its normal work. A cheaper-but-wrong version of this would make a rule skip work it
|
|
1007
|
+
* owed; this one can only ever fail to save time.
|
|
1008
|
+
*/
|
|
1009
|
+
declare function isMentionedAnywhere(config: Config, ruleId: string): boolean;
|
|
1010
|
+
/**
|
|
1011
|
+
* Effective options for a rule at a target: built-in defaults, then
|
|
1012
|
+
* `config.rules[ruleId].options`, then every matching `config.overrides` entry
|
|
1013
|
+
* in order. Integers take the last value; lists and maps accumulate.
|
|
1014
|
+
*
|
|
1015
|
+
* `target` omitted skips overrides entirely (project-scoped rules). Callers
|
|
1016
|
+
* resolving many targets should hoist `compileOverrides(config)` and pass it as
|
|
1017
|
+
* `compiled` — otherwise every call recompiles the globs.
|
|
1018
|
+
*/
|
|
1019
|
+
declare function resolveRuleOptions(ruleId: string, spec: RuleOptionsSpec | undefined, config: Config, target?: {
|
|
1020
|
+
route?: string;
|
|
1021
|
+
file?: string;
|
|
1022
|
+
}, compiled?: CompiledOverride[]): RuleOptions;
|
|
1023
|
+
/**
|
|
1024
|
+
* Problems with a user-supplied options object, as human-readable sentences
|
|
1025
|
+
* (empty = valid). Callers treat any result as fatal: a typo that silently
|
|
1026
|
+
* leaves the config inert is the failure this exists to prevent.
|
|
1027
|
+
*
|
|
1028
|
+
* `baseline`, when given, is the already-resolved value this `options` layer
|
|
1029
|
+
* is being merged onto — built-in defaults merged with any earlier layer(s)
|
|
1030
|
+
* (e.g. the global `config.rules[id].options`, when `options` is an
|
|
1031
|
+
* `overrides[]` entry). The min/max cross-check below compares against it
|
|
1032
|
+
* instead of the spec's own default, so a layer that only sets one side of a
|
|
1033
|
+
* range is checked against what it actually inherits (design 2026-07-26
|
|
1034
|
+
* review, Finding A). Omit it to check `options` against the spec defaults
|
|
1035
|
+
* alone, as when validating the global layer itself. A `baseline` that is
|
|
1036
|
+
* only partially resolved (missing `min` or `max`) is treated as "can't
|
|
1037
|
+
* determine that side" rather than silently comparing against `undefined` —
|
|
1038
|
+
* see the `typeof` guard below.
|
|
1039
|
+
*
|
|
1040
|
+
* `skipRangeCheck`, when true, skips the min/max cross-check entirely
|
|
1041
|
+
* regardless of `baseline`. A caller sets this when it statically cannot
|
|
1042
|
+
* rule out that some *other* config layer narrows the opposite side of the
|
|
1043
|
+
* range at the same target — see the CLI's and the Vite plugin's
|
|
1044
|
+
* `overrides[]` validation (design 2026-07-26 review, Finding A, third
|
|
1045
|
+
* pass).
|
|
1046
|
+
*/
|
|
1047
|
+
declare function validateRuleOptions(ruleId: string, spec: RuleOptionsSpec | undefined, options: RuleOptions, baseline?: RuleOptions, skipRangeCheck?: boolean): string[];
|
|
1048
|
+
/**
|
|
1049
|
+
* Whether `validateRuleOptions` should skip the min/max cross-check for
|
|
1050
|
+
* `overrides[selfIndex].rules[key]` — the whole decision, so the CLI's
|
|
1051
|
+
* config-file loader and the Vite plugin can't drift apart on it (they held
|
|
1052
|
+
* line-for-line copies of it before).
|
|
1053
|
+
*
|
|
1054
|
+
* An entry that sets both sides, or neither, is judged against its baseline as
|
|
1055
|
+
* usual. An entry that sets only one side is skipped when some *other* entry
|
|
1056
|
+
* sets the opposite side, since the two may co-apply at a shared target and be
|
|
1057
|
+
* valid there — see `otherOverrideNarrowsOppositeSide` for why that is
|
|
1058
|
+
* conservative by necessity and what it lets through.
|
|
1059
|
+
*/
|
|
1060
|
+
declare function shouldSkipRangeCheck(overrides: readonly unknown[], selfIndex: number, key: string, setting: unknown): boolean;
|
|
1061
|
+
/**
|
|
1062
|
+
* Problems with one user-supplied rule setting — the bare severity string or the
|
|
1063
|
+
* object form — as human-readable sentences prefixed with `label` (empty = valid).
|
|
1064
|
+
* THE single definition of what a setting may look like: the CLI's config-file
|
|
1065
|
+
* loader and the Vite plugin both funnel through it, so a config file and the
|
|
1066
|
+
* equivalent plugin option are accepted or rejected identically. Callers treat any
|
|
1067
|
+
* result as fatal, on the same reasoning as an unknown rule id — a typo that
|
|
1068
|
+
* silently leaves the config inert is the failure being prevented.
|
|
1069
|
+
*
|
|
1070
|
+
* `label` names the setting in the message (e.g. `rules.seo/title-length`,
|
|
1071
|
+
* `overrides[0].rules.architecture`); `ruleId` is the key options messages quote.
|
|
1072
|
+
* `allowOptions` is false for a category key: a category may carry a severity, but
|
|
1073
|
+
* options are rule-specific and meaningless there. `baseline` and `skipRangeCheck`
|
|
1074
|
+
* are passed through to `validateRuleOptions`.
|
|
1075
|
+
*/
|
|
1076
|
+
declare function validateRuleSetting(label: string, ruleId: string, setting: unknown, spec: RuleOptionsSpec | undefined, opts: {
|
|
1077
|
+
allowOptions: boolean;
|
|
1078
|
+
baseline?: RuleOptions;
|
|
1079
|
+
skipRangeCheck?: boolean;
|
|
1080
|
+
}): string[];
|
|
1081
|
+
//#endregion
|
|
1082
|
+
//#region src/rule.d.ts
|
|
1083
|
+
/** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
|
|
1084
|
+
interface RuleContext {
|
|
1085
|
+
heads: ResolvedHead[];
|
|
1086
|
+
/** Per-route <img> elements for Performance rules (absent in modes that don't collect them). */
|
|
1087
|
+
images?: ResolvedImages[];
|
|
1088
|
+
/** Per-route page-body headings for seo/single-h1 (absent in modes that don't collect them). */
|
|
1089
|
+
headings?: ResolvedHeadings[];
|
|
1090
|
+
/** Per-route composed landmark/id occurrences for the route-scoped a11y rules (absent in modes that don't collect them). */
|
|
1091
|
+
a11y?: ResolvedA11y[];
|
|
1092
|
+
/** Per-file component-body facts for the component-scoped rules (absent in the dev handle's rendered pass). */
|
|
1093
|
+
components?: ComponentFacts[];
|
|
1094
|
+
/** Per-file SvelteKit route/hooks facts for the kit-module rules (absent in the dev handle's rendered pass). */
|
|
1095
|
+
kitModules?: KitModuleFacts[];
|
|
1096
|
+
/**
|
|
1097
|
+
* Every file under `src/`, as project-relative paths, for directory-shaped Architecture rules
|
|
1098
|
+
* (static/CLI + vite build mode only). Sorted — see `collectSourceFiles`, which is what both
|
|
1099
|
+
* adapters use to build it.
|
|
1100
|
+
*/
|
|
1101
|
+
sourceFiles?: string[];
|
|
1102
|
+
project: Project;
|
|
1103
|
+
config: Config;
|
|
1104
|
+
/**
|
|
1105
|
+
* Report per-declaration counts of places this rule examined. The engine supplies it and keys the
|
|
1106
|
+
* result by rule id; a rule that does not call it gets no entry, which is distinct from an entry of
|
|
1107
|
+
* zeros. Absent in contexts a caller builds directly. Silent last-write-wins: calling it more than
|
|
1108
|
+
* once keeps only the most recent map, with no merge and no error — call it once, with the complete
|
|
1109
|
+
* counts, at the end of `check()`.
|
|
1110
|
+
*/
|
|
1111
|
+
recordExamined?: (counts: Record<string, number>) => void;
|
|
1112
|
+
}
|
|
1113
|
+
interface Rule {
|
|
1114
|
+
id: string;
|
|
1115
|
+
title: string;
|
|
1116
|
+
category: Category;
|
|
1117
|
+
/** Default severity (overridable by config in later slices). */
|
|
1118
|
+
severity: Severity;
|
|
1119
|
+
/** 'route' = evaluated per route, 'project' = site-wide, 'component' = evaluated per source file (design §10, §12). */
|
|
1120
|
+
scope: Scope;
|
|
1121
|
+
/** Why this rule matters — one or two sentences, surfaced by `svelte-vitals explain` (issue #24). */
|
|
1122
|
+
rationale: string;
|
|
1123
|
+
/** Canonical remediation template, shared by findings and `svelte-vitals explain` (issue #24). */
|
|
1124
|
+
fix?: Fix;
|
|
1125
|
+
/** Configurable options for this rule; absent means the rule takes none. */
|
|
1126
|
+
options?: RuleOptionsSpec;
|
|
1127
|
+
/**
|
|
1128
|
+
* The message this rule puts on a PASS result. Declared so a PASS synthesised elsewhere — the
|
|
1129
|
+
* central inline-suppression pass, which turns a fully-suppressed rule+route into a pass — reads
|
|
1130
|
+
* the same as one the rule emitted itself. Rules built through `componentRule` and the a11y
|
|
1131
|
+
* route factory supply it; the rest fall back to `title`, which is a cosmetic difference visible
|
|
1132
|
+
* only in `--verbose`'s passed listing.
|
|
1133
|
+
*/
|
|
1134
|
+
passLabel?: string;
|
|
1135
|
+
/** Off unless config.rules names the rule explicitly — the opt-in class (design 2026-08-21). */
|
|
1136
|
+
defaultOff?: true;
|
|
1137
|
+
/**
|
|
1138
|
+
* The rule compares routes against each other (`seo/duplicate-title`), so it cannot be judged
|
|
1139
|
+
* from one route's rendered HTML — the dev dashboard's live layer leaves it to the static pass.
|
|
1140
|
+
*/
|
|
1141
|
+
crossRoute?: true;
|
|
1142
|
+
/**
|
|
1143
|
+
* Evaluate the resolved heads. A single rule may return one Result per route,
|
|
1144
|
+
* so it always returns an array. Project-scoped rules return a single element.
|
|
1145
|
+
*/
|
|
1146
|
+
check(ctx: RuleContext): Promise<Result[]>;
|
|
1147
|
+
}
|
|
1148
|
+
/** Documentation URL for a rule id. Single source so no per-rule URL can drift (issue #24). */
|
|
1149
|
+
declare function docsUrlFor(id: string): string;
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether a detection should be penalized by scoring (design §12). Shared by the
|
|
1152
|
+
* future Scorer and by the Slice 0 reporter so pass/fail is decided in one place.
|
|
1153
|
+
*
|
|
1154
|
+
* presence 'none' → penalized (nothing set anywhere)
|
|
1155
|
+
* value 'absent' → penalized (tag present but empty)
|
|
1156
|
+
* value 'dynamic' → penalized when treatDynamicAs is not 'pass' (warn or fail)
|
|
1157
|
+
* otherwise (static/inherited) → not penalized
|
|
1158
|
+
*/
|
|
1159
|
+
declare function isPenalized(detection: Detection, treatDynamicAs: TreatDynamicAs): boolean;
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/scoring/score.d.ts
|
|
1162
|
+
interface ScoreModel {
|
|
1163
|
+
routeAverage: number;
|
|
1164
|
+
sitePenalty: number;
|
|
1165
|
+
/** Headline cap value when it actually lowered the score, else null. */
|
|
1166
|
+
criticalCap: number | null;
|
|
1167
|
+
}
|
|
1168
|
+
interface ScoreResult {
|
|
1169
|
+
/** The score as displayed: `Math.floor(rawScore)`, so 100 means the deduction was exactly zero. */
|
|
1170
|
+
score: number;
|
|
1171
|
+
/**
|
|
1172
|
+
* The same score before flooring, after `sitePenalty` and the cap, clamped to `[0, 100]`. Exposed so
|
|
1173
|
+
* `computeHealth` can average unrounded values and floor once — averaging the displayed scores would
|
|
1174
|
+
* compose two roundings and move Health by up to two points.
|
|
1175
|
+
*/
|
|
1176
|
+
rawScore: number;
|
|
1177
|
+
scoreModel: ScoreModel;
|
|
1178
|
+
/** Keys this result set touched. */
|
|
1179
|
+
keys: number;
|
|
1180
|
+
/** Keys carrying at least one penalized finding. */
|
|
1181
|
+
affectedKeys: number;
|
|
1182
|
+
}
|
|
1183
|
+
interface ScoreOptions {
|
|
1184
|
+
applyCriticalCap?: boolean;
|
|
1185
|
+
/** The rules that ran. Defaults to the selected registry; supplied by tests and custom rule sets. */
|
|
1186
|
+
rules?: readonly Rule[];
|
|
1187
|
+
}
|
|
1188
|
+
/** Compute the headline score and its breakdown (design §12). */
|
|
1189
|
+
declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
|
|
1190
|
+
/** Compute an independent score per category present in `results` (issue #10). */
|
|
1191
|
+
declare function scoresByCategory(results: Result[], config: Config, options?: ScoreOptions): Partial<Record<Category, ScoreResult>>;
|
|
1192
|
+
interface HealthResult {
|
|
1193
|
+
/** Weighted overall score across present categories (0–100). */
|
|
1194
|
+
health: number;
|
|
1195
|
+
categories: Partial<Record<Category, ScoreResult>>;
|
|
1196
|
+
/** Effective weight used per present category. */
|
|
1197
|
+
weights: Partial<Record<Category, number>>;
|
|
1198
|
+
}
|
|
1199
|
+
/** Combined weighted Health score over the categories present in `results` (#10). */
|
|
1200
|
+
declare function computeHealth(results: Result[], config: Config): HealthResult;
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/reporter/json.d.ts
|
|
1203
|
+
declare function issueOf(result: Result): {
|
|
1204
|
+
fix?: Fix | undefined;
|
|
1205
|
+
docsUrl?: string | undefined;
|
|
1206
|
+
recommendation: string | undefined;
|
|
1207
|
+
line?: number | undefined;
|
|
1208
|
+
id: string;
|
|
1209
|
+
category: Category;
|
|
1210
|
+
title: string;
|
|
1211
|
+
detection: Detection;
|
|
1212
|
+
location: string | undefined;
|
|
1213
|
+
};
|
|
1214
|
+
type JsonIssue = ReturnType<typeof issueOf> & {
|
|
1215
|
+
severity: ReturnType<typeof effectiveSeverity>;
|
|
1216
|
+
};
|
|
1217
|
+
/**
|
|
1218
|
+
* Per-rule counts. A rule present with `findings: 0` ran and reported nothing, and an absent rule was not
|
|
1219
|
+
* selected — but only when the caller supplied `ruleIds`. Without it the map is seeded from results alone,
|
|
1220
|
+
* so absence means "produced nothing" rather than "not selected".
|
|
1221
|
+
*/
|
|
1222
|
+
interface RuleEvidence {
|
|
1223
|
+
findings: number;
|
|
1224
|
+
passed: number;
|
|
1225
|
+
}
|
|
1226
|
+
interface JsonReport {
|
|
1227
|
+
version: string;
|
|
1228
|
+
score: number;
|
|
1229
|
+
weights: Partial<Record<Category, number>>;
|
|
1230
|
+
categories: Record<string, {
|
|
1231
|
+
score: number;
|
|
1232
|
+
scoreModel: ScoreModel;
|
|
1233
|
+
keys: number;
|
|
1234
|
+
affectedKeys: number;
|
|
1235
|
+
}>;
|
|
1236
|
+
summary: Summary;
|
|
1237
|
+
rules: Record<string, RuleEvidence>;
|
|
1238
|
+
routes: Array<{
|
|
1239
|
+
route: string;
|
|
1240
|
+
score: number;
|
|
1241
|
+
categories: Record<string, number>;
|
|
1242
|
+
issues: JsonIssue[];
|
|
1243
|
+
}>;
|
|
1244
|
+
siteIssues: JsonIssue[];
|
|
1245
|
+
/**
|
|
1246
|
+
* Floored severity weight per `"<category>::<scope>"` pair. Reproduces a `routes[].categories` entry
|
|
1247
|
+
* (`100 - 100 * failed / inventories[pair]`): a key is either a route id or a source file path, and
|
|
1248
|
+
* those two key spaces never overlap, so a category's results on one key always draw on a single scope.
|
|
1249
|
+
* It does not reproduce `routes[].score`: a route spanning more than one pair sums their *raw* weights
|
|
1250
|
+
* and floors that sum once, so adding this map's already-floored entries and re-dividing can disagree.
|
|
1251
|
+
*/
|
|
1252
|
+
inventories: Record<string, number>;
|
|
1253
|
+
/**
|
|
1254
|
+
* Per-rule, per-declaration counts of places examined. Unlike `rules`, this describes the analysis
|
|
1255
|
+
* rather than the report: `--diff`, `--baseline` and suppressions do not narrow it. Three states: a
|
|
1256
|
+
* rule that reports no counts has no entry; a rule that counts but whose configuration declares
|
|
1257
|
+
* nothing has an empty entry; a declaration that judged nothing has an entry of `0`.
|
|
1258
|
+
*/
|
|
1259
|
+
examined?: Record<string, Record<string, number>>;
|
|
1260
|
+
/**
|
|
1261
|
+
* Routes a closed-world rule skipped, keyed by rule id. Like `examined`, this describes the
|
|
1262
|
+
* analysis rather than the report: `--diff`, `--baseline` and suppressions do not narrow it.
|
|
1263
|
+
* `refs` is the route's literal id-reference count — a skipped route with `refs: 0` would
|
|
1264
|
+
* produce nothing even if unlocked. Only source-mode analysis populates it; absent when no
|
|
1265
|
+
* analyzed route was skipped.
|
|
1266
|
+
*/
|
|
1267
|
+
skipped?: Record<string, Array<{
|
|
1268
|
+
route: string;
|
|
1269
|
+
refs: number;
|
|
1270
|
+
causes: Array<{
|
|
1271
|
+
kind: string;
|
|
1272
|
+
file: string;
|
|
1273
|
+
line: number;
|
|
1274
|
+
detail?: string;
|
|
1275
|
+
}>;
|
|
1276
|
+
}>>;
|
|
1277
|
+
}
|
|
1278
|
+
/** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
|
|
1279
|
+
declare function buildJsonReport(results: Result[], config: Config, meta: {
|
|
1280
|
+
version: string;
|
|
1281
|
+
}, ruleIds?: readonly string[], examined?: Record<string, Record<string, number>>, skipped?: JsonReport['skipped']): JsonReport;
|
|
1282
|
+
/** Render results as the documented JSON report string (design §7). */
|
|
1283
|
+
declare function formatJsonReport(results: Result[], config: Config, meta: {
|
|
1284
|
+
version: string;
|
|
1285
|
+
}, ruleIds?: readonly string[], examined?: Record<string, Record<string, number>>, skipped?: JsonReport['skipped']): string;
|
|
1286
|
+
//#endregion
|
|
1287
|
+
export { HeadTag as $, formatFailedRuleWarning as A, defaultProject as At, IDREF_ATTRS as B, shouldSkipRangeCheck as C, RuleSetting as Ct, applyOverrides as D, TreatDynamicAs as Dt, CompiledOverride as E, Severity as Et, withFailedRulesOff as F, OrphanEffectFact as Ft, isTopFragment as G, ResolvedA11y as H, KitModuleFacts as I, SourceSpan as It, HeadingInfo as J, splitTokens as K, A11yOccurrenceInfo as L, SuppressionDirective as Lt, selectRules as M, ComponentFacts as Mt, settingOptions as N, EachBlockFact as Nt, applyRuleSeverities as O, Value as Ot, settingSeverity as P, EffectFact as Pt, HeadProvider as Q, A11ySkipCause as R, skippedFileWarnings as Rt, resolveRuleOptions as S, RuleOverride as St, validateRuleSetting as T, Scope as Tt, decodeFragmentId as U, LANDMARK_ROLES as V, foldOccurrences as W, ImageInfo as X, ResolvedHeadings as Y, ResolvedImages as Z, RuleOptionsSpec as _, KitAlias as _t, HealthResult as a, formatGithubReport as at, listOption as b, Result as bt, ScoreResult as c, classify as ct, scoresByCategory as d, summarize as dt, ResolvedHead as et, Rule as f, CATEGORIES as ft, RuleOptionSpec as g, Fix as gt, isPenalized as h, Detection as ht, formatJsonReport as i, formatMarkdownReport as it, overrideMatches as j, defineConfig as jt, compileOverrides as k, defaultConfig as kt, computeHealth as l, effectiveSeverity as lt, docsUrlFor as m, Config as mt, RuleEvidence as n, Runtime as nt, ScoreModel as o, Classification as ot, RuleContext as p, Category as pt, stripTextDirective as q, buildJsonReport as r, withReadLimit as rt, ScoreOptions as s, Summary as st, JsonReport as t, READ_CONCURRENCY as tt, computeScore as u, hasFailureAtOrAbove as ut, intOption as v, Presence as vt, validateRuleOptions as w, RuleSettingObject as wt, mapOption as x, RuleOptions as xt, isMentionedAnywhere as y, Project as yt, BranchStep as z };
|