@svelte-vitals/core 0.44.0 → 0.46.0

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.
@@ -1,7 +1,313 @@
1
+ /**
2
+ * Component-body facts for the Correctness category — the source-analysis boundary
3
+ * (mirrors images.ts / headings.ts). Collected by the static (CLI) provider only;
4
+ * the rendered provider can't see reactivity, so correctness rules no-op there.
5
+ */
6
+ /** An `{#each}` block in a component template. */
7
+ interface EachBlockFact {
8
+ /** True when the block has a key, e.g. `{#each items as item (item.id)}`. */
9
+ hasKey: boolean;
10
+ /** 1-based source line, or 0 if unknown. */
11
+ line: number;
12
+ /** 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. */
13
+ indexKey?: boolean;
14
+ }
15
+ /** An `$effect(...)` / `$effect.pre(...)` call in a component's instance script. */
16
+ interface EffectFact {
17
+ /** 1-based source line, or 0 if unknown. */
18
+ line: number;
19
+ /** True when the effect body only assigns to `$state` variables (the "use $derived" smell). */
20
+ assignsOnlyState: boolean;
21
+ /** 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). */
22
+ mountOnly: boolean;
23
+ }
24
+ /** A `$effect` guaranteed to run outside component initialisation — it throws `effect_orphan` at runtime (correctness/orphan-effect). */
25
+ interface OrphanEffectFact {
26
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
27
+ line: number;
28
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor creates a bare effect. */
29
+ kind: 'top-level' | 'constructor-instantiated';
30
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
31
+ className?: string;
32
+ }
33
+ /** A svelte lifecycle/context call guaranteed to run outside component initialisation — it throws `lifecycle_outside_component` at runtime (correctness/orphan-lifecycle). */
34
+ interface OrphanLifecycleCallFact {
35
+ /** Canonical svelte export name (alias-resolved), e.g. 'onMount'. */
36
+ name: string;
37
+ /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
38
+ line: number;
39
+ /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor calls a tracked function. */
40
+ kind: 'top-level' | 'constructor-instantiated';
41
+ /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
42
+ className?: string;
43
+ }
44
+ /** 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). */
45
+ interface BrowserGlobalRefFact {
46
+ /** The global's name, e.g. 'window'. */
47
+ name: string;
48
+ /** 1-based source line, or 0 if unknown. */
49
+ line: number;
50
+ /** '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). */
51
+ context: 'module' | 'instance';
52
+ }
53
+ /** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
54
+ interface SourceSpan {
55
+ /** 1-based source line, or 0 if unknown. */
56
+ line: number;
57
+ }
58
+ /** An inline `svelte-vitals-disable-next-line` directive found in the component's source (issue #92). */
59
+ interface SuppressionDirective {
60
+ /** 1-based line the directive suppresses (the line immediately after the comment). */
61
+ line: number;
62
+ /** Rule ids suppressed on that line; undefined = suppress every rule on that line. */
63
+ ruleIds?: string[];
64
+ }
65
+ /** An `<input type="checkbox">` / `<input type="radio">` element carrying a `bind:value`
66
+ * directive — `bind:value` observes the DOM `value` property, which checkbox/radio
67
+ * interaction never changes, so the bound state silently never updates
68
+ * (correctness/checkable-bind-value). */
69
+ interface CheckableBindValueFact {
70
+ /** Which checkable input type was flagged — selects the message wording. */
71
+ kind: 'checkbox' | 'radio';
72
+ /** 1-based source line, or 0 if unknown. */
73
+ line: number;
74
+ }
75
+ /** A root-relative navigation literal — broken when the app is served under `kit.paths.base`
76
+ * (correctness/base-path-navigation). Shared by the component and Kit-module channels. */
77
+ interface BasePathLinkFact {
78
+ /** Which navigation surface it was written on — selects the message wording. */
79
+ kind: 'href' | 'goto' | 'redirect';
80
+ /** The literal path as written, e.g. '/about'. */
81
+ path: string;
82
+ /** 1-based source line, or 0 if unknown. */
83
+ line: number;
84
+ }
85
+ /** An interactive element (e.g. `<button>`) found nested inside another interactive
86
+ * container (e.g. `<a href>`) (a11y/interactive-nesting). */
87
+ interface InteractiveNestingFact {
88
+ containerTag: string;
89
+ /** The container's literal `role`, when that is what made it a container rather than its tag. */
90
+ containerRole?: string;
91
+ descendantTag: string;
92
+ /** 1-based source line of the descendant, or 0 if unknown. */
93
+ line: number;
94
+ }
95
+ /** A `button`/`a href`/`input type="image"` with no computable accessible name (a11y/accessible-name). */
96
+ interface UnnamedInteractiveFact {
97
+ tag: string;
98
+ /** 1-based source line, or 0 if unknown. */
99
+ line: number;
100
+ }
101
+ /** An element carrying a `role` and/or `aria-*` attribute(s) (a11y ARIA rules). */
102
+ interface AriaElementFact {
103
+ tag: string;
104
+ /** 1-based source line, or 0 if unknown. */
105
+ line: number;
106
+ /** literal role value; undefined = no role attr; { expression: true } = dynamic */
107
+ role?: {
108
+ literal?: string;
109
+ expression?: boolean;
110
+ };
111
+ /** every aria-* attribute on the element */
112
+ aria: {
113
+ name: string;
114
+ literal?: string;
115
+ expression?: boolean;
116
+ line: number;
117
+ }[];
118
+ /** literal `type` of an `<input>`, lowercased; undefined for non-inputs or a dynamic type */
119
+ inputType?: string;
120
+ /** an `<input>` carrying a `list` attribute — its implicit role is `combobox` and the host supplies `aria-expanded` */
121
+ hasList?: true;
122
+ /**
123
+ * A `<select>`'s native role: `combobox` with no `multiple` and no `size` above 1, `listbox`
124
+ * otherwise; absent for a non-select or when a dynamic `size` leaves it unknowable.
125
+ */
126
+ selectKind?: 'combobox' | 'listbox';
127
+ /** Set when the element also carries a spread attribute — its full attribute set is
128
+ * unknowable, so required-prop presence checks must treat it as satisfied (a11y/required-aria-props). */
129
+ hasSpread?: true;
130
+ }
131
+ /**
132
+ * Every element in a component with its literal attribute names — the input for the rules that
133
+ * judge against the HTML spec data (a11y/deprecated-element, a11y/deprecated-attr, and the rest of
134
+ * that family). Tag and attribute names are lowercased, matching how HTML parses them.
135
+ */
136
+ interface ElementFact {
137
+ tag: string;
138
+ /** 1-based source line, or 0 if unknown. */
139
+ line: number;
140
+ /**
141
+ * Literal attribute names on the element (directives, spreads and expression-only names excluded).
142
+ * The per-attribute line is not what the deprecation rules anchor to — they anchor at the start
143
+ * tag so a `disable-next-line` directive can reach a multi-line element — but a value-level rule
144
+ * (`invalid-attr`) may want it for its message.
145
+ */
146
+ attrs: {
147
+ name: string;
148
+ line: number;
149
+ value?: string;
150
+ }[];
151
+ /**
152
+ * Inside an `<svg>` subtree, or in a component declaring `<svelte:options namespace="svg" />`.
153
+ * `<foreignObject>` returns to HTML. Names collide across the two namespaces (`a`, `script`,
154
+ * `style`, `title`), so HTML-only rules must skip these.
155
+ */
156
+ inSvg?: true;
157
+ /**
158
+ * Index of the nearest literal ancestor element in the same array (push-before-children DFS
159
+ * keeps it sound), looking through `{#if}`/`{#each}`/`{#await}`/`{#key}`. Absent at template
160
+ * root and after every construct whose rendering position is not lexical — a component,
161
+ * `<svelte:element>`, `<slot>`, `{@render}`, `{@html}`, a custom element or unknown tag,
162
+ * a `{#snippet}` body root, `<svelte:head>` children — so `a11y/permitted-contents` never
163
+ * judges across one.
164
+ */
165
+ parent?: number;
166
+ /** A spread attribute is present — every attribute test on this element is unknowable. */
167
+ hasSpread?: true;
168
+ /**
169
+ * A direct child the static walk cannot see through (component, `{@html}`, `{@render}`,
170
+ * `<slot />`, `<svelte:element>`, a custom element or unknown tag) — `:has(...)` over this
171
+ * element's subtree is unknowable.
172
+ */
173
+ unknownContent?: true;
174
+ }
175
+ /** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
176
+ interface ComponentFacts {
177
+ /** Source file the component came from. */
178
+ file: string;
179
+ eachBlocks: EachBlockFact[];
180
+ effects: EffectFact[];
181
+ /** `{@html …}` occurrences — raw-HTML render surfaces (security/raw-html). */
182
+ htmlTags: SourceSpan[];
183
+ /** Element attributes with a literal `javascript:` URL (security/javascript-url). */
184
+ javascriptUrls: SourceSpan[];
185
+ /** Source line count of the component file (architecture/component-size). */
186
+ loc: number;
187
+ /** Named props destructured from `$props()`; 0 when unknowable (rest / non-destructured) (architecture/prop-count). */
188
+ propCount: number;
189
+ /** Module specifiers of every `import` in the instance + module scripts (performance/heavy-import). */
190
+ imports: string[];
191
+ /**
192
+ * Module specifiers of every `import`, each with its source line (performance/heavy-import,
193
+ * architecture/route-component-import). `type` marks a declaration that contributes **no runtime
194
+ * value binding** — either `import type …`, or one whose every specifier is inline-typed
195
+ * (`import { type A } from …`). A specifier-less side-effect import is not marked: it still loads
196
+ * the module. Optional, so existing external constructors of `ComponentFacts` are unaffected.
197
+ */
198
+ importSpans: {
199
+ source: string;
200
+ line: number;
201
+ type?: true;
202
+ }[];
203
+ /** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — performance/namespace-import. */
204
+ namespaceImports: {
205
+ source: string;
206
+ line: number;
207
+ }[];
208
+ /** `$state` declarations never written or escaped anywhere in the component — candidates for const (correctness/unmutated-state). */
209
+ constableStates: {
210
+ name: string;
211
+ line: number;
212
+ }[];
213
+ /** 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. */
214
+ mutatedProps: {
215
+ name: string;
216
+ line: number;
217
+ legacy?: boolean;
218
+ }[];
219
+ /** 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. */
220
+ stalePropDerivations: {
221
+ name: string;
222
+ line: number;
223
+ legacy?: boolean;
224
+ }[];
225
+ /** Object/array-literal $state bindings reassigned at least once but never mutated, escaped, aliased, or item-edited — $state.raw candidates (performance/state-raw). */
226
+ rawableStates: {
227
+ name: string;
228
+ line: number;
229
+ }[];
230
+ /** 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). */
231
+ nonreactiveBuiltinStates: {
232
+ name: string;
233
+ type: string;
234
+ line: number;
235
+ }[];
236
+ /** `<input type="checkbox">` / `<input type="radio">` elements bound with `bind:value`
237
+ * instead of `bind:checked`/`bind:group` (correctness/checkable-bind-value). */
238
+ checkableBindValues: CheckableBindValueFact[];
239
+ /** Root-relative `<a href>` and `goto()` literals in this component (correctness/base-path-navigation). */
240
+ basePathLinks: BasePathLinkFact[];
241
+ /** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-effect). */
242
+ orphanEffects: OrphanEffectFact[];
243
+ /** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-lifecycle). */
244
+ orphanLifecycleCalls: OrphanLifecycleCallFact[];
245
+ /** Browser-global reads in server-executed positions of this file (correctness/server-browser-global, correctness/instance-browser-global). */
246
+ browserGlobalRefs: BrowserGlobalRefFact[];
247
+ /** 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. */
248
+ moduleStateDecls: {
249
+ name: string;
250
+ line: number;
251
+ }[];
252
+ /** 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. */
253
+ suppressions?: SuppressionDirective[];
254
+ /** Markdown links `[label](url)` appearing inside a comment (architecture/doc-link-target). */
255
+ commentLinks: {
256
+ url: string;
257
+ line: number;
258
+ }[];
259
+ /** Elements carrying a role or any aria-* attribute (a11y ARIA rules). */
260
+ ariaElements?: AriaElementFact[];
261
+ /** Every element with its attribute names and SVG-namespace flag (the HTML spec-data rules). */
262
+ elements?: ElementFact[];
263
+ /** Interactive elements nested inside another interactive container (a11y/interactive-nesting). */
264
+ interactiveNestings?: InteractiveNestingFact[];
265
+ /** `button`/`a href`/`input type="image"` elements with no computable accessible name (a11y/accessible-name). */
266
+ unnamedInteractive?: UnnamedInteractiveFact[];
267
+ /** `<label>` elements with neither a `for` attribute nor a wrapped labelable descendant (a11y/label-has-control). */
268
+ unassociatedLabels?: {
269
+ line: number;
270
+ }[];
271
+ /** Text nodes whose trimmed content opens with a bullet character followed by whitespace, outside any `li` (a11y/use-list). */
272
+ bulletTexts?: {
273
+ line: number;
274
+ char: string;
275
+ }[];
276
+ /** `<select required>` (no `multiple`, display size absent or ≤ 1) whose first `option` element
277
+ * child is not a placeholder label option (a11y/placeholder-label-option). */
278
+ selectsMissingPlaceholder?: {
279
+ line: number;
280
+ }[];
281
+ /** `<time>` with no `datetime` attribute whose literal text content is not machine-readable (a11y/require-datetime). */
282
+ timesMissingDatetime?: {
283
+ line: number;
284
+ text: string;
285
+ }[];
286
+ /** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
287
+ parseFailed?: true;
288
+ /** Set when the file could not be READ — an environment problem (permissions, a descriptor
289
+ * limit), not a malformed component. Reported separately so one does not masquerade as the other. */
290
+ readFailed?: true;
291
+ }
292
+ /**
293
+ * Warnings for files a collector could not read or parse: the file contributes empty facts, so any
294
+ * findings it would have produced are simply missing rather than reported as fixed. The two causes
295
+ * are reported separately — an unreadable file is an environment problem (permissions, a descriptor
296
+ * limit) and a malformed one is the author's, and sharing a message is how a descriptor limit once
297
+ * read as hundreds of broken components. Capped at 10 inline paths so one badly-broken directory
298
+ * cannot flood the terminal.
299
+ */
300
+ declare function skippedFileWarnings(facts: readonly {
301
+ file: string;
302
+ parseFailed?: true;
303
+ readFailed?: true;
304
+ }[]): string[];
305
+
1
306
  /**
2
307
  * Core type definitions shared across modes. This module is pure: no I/O, no
3
308
  * `node:` imports, no runtime-specific globals (design §8).
4
309
  */
310
+
5
311
  type Severity = 'critical' | 'warning' | 'info';
6
312
  /** Where a head tag is set, relative to the route being evaluated (design §4). */
7
313
  type Presence = 'own' | 'inherited' | 'none';
@@ -55,11 +361,13 @@ interface Project {
55
361
  * `file` is the config path relative to the analyzed root (posix, may start with `../`
56
362
  * in monorepos); unset for inline programmatic configs. `line` is 1-based and set only
57
363
  * when the literal `minify: false` was located in that file; unset when the value was
58
- * resolved at build time (plugin/conditional config).
364
+ * resolved at build time (plugin/conditional config). `suppressions` carries the config file's
365
+ * own inline directives, so a line-anchored finding in it can be silenced like any other.
59
366
  */
60
367
  viteMinifyDisabled?: {
61
368
  file?: string;
62
369
  line?: number;
370
+ suppressions?: SuppressionDirective[];
63
371
  };
64
372
  /**
65
373
  * Set when the project configures a non-empty `kit.paths.base` — read from the `sveltekit()`
@@ -91,6 +399,8 @@ interface Project {
91
399
  * absent when the file wasn't read.
92
400
  */
93
401
  appHtmlIds?: string[];
402
+ /** Distinct lowercased tag names inside `app.html`'s `<body>` (a11y/required-element's presence set; static mode). */
403
+ appHtmlBodyTags?: string[];
94
404
  }
95
405
  declare const defaultProject: Project;
96
406
  /** A concrete, agent-actionable remediation for a finding (design §10, issue #18). */
@@ -202,6 +512,22 @@ declare function summarize(results: Result[], config: Config): Summary;
202
512
  /** Whether the run should fail the build/CI per the minimum failing severity. */
203
513
  declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
204
514
 
515
+ /**
516
+ * Render penalized findings as GitHub Actions workflow commands (issue #18, design slice 5).
517
+ * GitHub turns these into inline PR annotations and run-annotation entries. Returns '' when clean.
518
+ */
519
+ declare function formatGithubReport(results: Result[], config: Config): string;
520
+
521
+ /**
522
+ * Render a compact Markdown summary — Health score, per-category table, severity counts, and
523
+ * a findings table — suitable for a GitHub Actions job summary or a sticky PR comment
524
+ * (`svelte-vitals ci install`). Delegates all aggregation to `buildJsonReport` so the numbers
525
+ * never drift from the JSON/console reporters.
526
+ */
527
+ declare function formatMarkdownReport(results: Result[], config: Config, meta: {
528
+ version: string;
529
+ }): string;
530
+
205
531
  /**
206
532
  * Runtime abstraction (design §8). Core defines only the interface; concrete
207
533
  * adapters (Node / Deno / Bun) live in the CLI package and are the only place
@@ -286,7 +612,7 @@ interface HeadTag {
286
612
  presence: Exclude<Presence, 'none'>;
287
613
  /** Whether the tag's value is static/dynamic/absent (design §4). */
288
614
  value: Value;
289
- /** Source file the tag came from (static mode only). */
615
+ /** Source file the tag came from (static mode); unset on a rendered head. */
290
616
  file?: string;
291
617
  }
292
618
  /** Resolved effective head for a single route (design §8). */
@@ -322,379 +648,147 @@ interface ImageInfo {
322
648
  /** True when the <img> has an `alt` attribute at all (incl. empty `alt=""` decorative; seo/image-alt). */
323
649
  hasAlt: boolean;
324
650
  /** True when the <img> has a literal `loading="lazy"` (performance/lcp-image). Dynamic/spread → false. */
325
- lazy: boolean;
326
- /** True when the <img> has a `srcset` attribute (performance/responsive-image). */
327
- hasSrcset: boolean;
328
- /** 1-based source line, or 0 if unknown. */
329
- line: number;
330
- /** Source file the <img> came from. */
331
- file: string;
332
- }
333
- /** Resolved <img> elements for a single route (page + layout chain). */
334
- interface ResolvedImages {
335
- route: string;
336
- images: ImageInfo[];
337
- }
338
-
339
- /**
340
- * A normalized page-body heading occurrence — the mode-independent boundary for
341
- * the heading-hierarchy rule (mirrors images.ts). Both providers collect these
342
- * so seo/single-h1 never needs to know which mode produced them.
343
- */
344
- interface HeadingInfo {
345
- /** Heading level 1–6 (the `n` in <hn>). */
346
- level: number;
347
- /** 1-based source line, or 0 if unknown (rendered mode does not track lines). */
348
- line: number;
349
- /** Source file the heading came from. */
350
- file: string;
351
- }
352
- /** Resolved page-body headings for a single route (page + layout chain). */
353
- interface ResolvedHeadings {
354
- route: string;
355
- headings: HeadingInfo[];
356
- /**
357
- * Headings found in child components rendered (transitively) by this route's
358
- * chain files — source mode only; absent in rendered mode. Kept separate from
359
- * `headings` because their position in document order is unknown: safe for
360
- * counting (seo/single-h1), unusable for outline order (seo/heading-level-skip).
361
- */
362
- componentHeadings?: HeadingInfo[];
363
- }
364
-
365
- /** One step of a template branch address: which exclusive block, and which arm of it. */
366
- interface BranchStep {
367
- /** index of the {#if}/{#await} block among its file's blocks (document order) */
368
- group: number;
369
- /** branch index within the group (if: 0..n consequent→else; await: 0=pending,1=then,2=catch) */
370
- branch: number;
371
- }
372
- /** Where a folded occurrence sits, for the finding location. */
373
- interface A11yOccurrenceInfo {
374
- file: string;
375
- line: number;
376
- }
377
- /**
378
- * Route-scoped a11y facts, the mode-independent boundary for the landmark/id rules
379
- * (mirrors headings.ts). Source mode composes the layout chain plus its resolved
380
- * components; rendered mode reads the prerendered document.
381
- */
382
- interface ResolvedA11y {
383
- route: string;
384
- /** representatives per landmark kind after the branch-aware fold ('main' | 'banner' | 'contentinfo' | 'complementary') */
385
- landmarks: Record<string, A11yOccurrenceInfo[]>;
386
- /** landmark occurrences nested inside another landmark after composition */
387
- nestedLandmarks: {
388
- kind: string;
389
- within: string;
390
- file: string;
391
- line: number;
392
- }[];
393
- /** representatives per literal id */
394
- ids: Record<string, A11yOccurrenceInfo[]>;
395
- /** literal id references */
396
- idRefs: {
397
- id: string;
398
- attr: string;
399
- file: string;
400
- line: number;
401
- }[];
402
- /** optimistic candidates: every literal id anywhere (all branches, each/snippet bodies, components, app.html) */
403
- idCandidates: string[];
404
- /** closed world holds: every component resolved, no depth truncation, no {@html}/spread, no dynamic id */
405
- fullyResolved: boolean;
406
- }
407
- type Foldable = {
408
- key: string;
409
- path: BranchStep[];
410
- repeatable: boolean;
411
- };
412
- /**
413
- * Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
414
- * occurrences sum, across the arms of one exclusive block the arm with the most
415
- * occurrences wins (tie → lowest branch index) and ITS occurrences are the group's
416
- * representatives — so a caller's count is always `list.length`, with a location per
417
- * representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
418
- * The max is per key: there is no scalar total to maximize.
419
- */
420
- declare function foldOccurrences<T extends Foldable>(nodes: T[]): Map<string, T[]>;
421
- /**
422
- * Decode a fragment identifier the way navigation does before matching an element id
423
- * (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
424
- * the browser would also fail to decode them, so the raw text is the comparable form.
425
- */
426
- declare function decodeFragmentId(fragment: string): string;
427
- /** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
428
- declare function splitTokens(value: string | undefined): string[];
429
- /** Explicit `role` values that map to the landmark kinds the route rules inspect. */
430
- declare const LANDMARK_ROLES: ReadonlySet<string>;
431
- /** Attributes whose (whitespace-tokenized) values reference element ids. */
432
- declare const IDREF_ATTRS: readonly string[];
433
- /**
434
- * Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
435
- * case-insensitive) scrolls to the top when no element has that id, so it is never a missing
436
- * reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
437
- */
438
- declare function isTopFragment(id: string): boolean;
439
- /**
440
- * A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
441
- * instructions for finding text and names no element, while anything before it is still an
442
- * ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
443
- * nothing. Returns an empty string when the fragment is a directive and nothing else.
444
- */
445
- declare function stripTextDirective(fragment: string): string;
446
-
447
- /**
448
- * Component-body facts for the Correctness category — the source-analysis boundary
449
- * (mirrors images.ts / headings.ts). Collected by the static (CLI) provider only;
450
- * the rendered provider can't see reactivity, so correctness rules no-op there.
451
- */
452
- /** An `{#each}` block in a component template. */
453
- interface EachBlockFact {
454
- /** True when the block has a key, e.g. `{#each items as item (item.id)}`. */
455
- hasKey: boolean;
456
- /** 1-based source line, or 0 if unknown. */
457
- line: number;
458
- /** 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. */
459
- indexKey?: boolean;
460
- }
461
- /** An `$effect(...)` / `$effect.pre(...)` call in a component's instance script. */
462
- interface EffectFact {
463
- /** 1-based source line, or 0 if unknown. */
464
- line: number;
465
- /** True when the effect body only assigns to `$state` variables (the "use $derived" smell). */
466
- assignsOnlyState: boolean;
467
- /** 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). */
468
- mountOnly: boolean;
469
- }
470
- /** A `$effect` guaranteed to run outside component initialisation — it throws `effect_orphan` at runtime (correctness/orphan-effect). */
471
- interface OrphanEffectFact {
472
- /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
473
- line: number;
474
- /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor creates a bare effect. */
475
- kind: 'top-level' | 'constructor-instantiated';
476
- /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
477
- className?: string;
478
- }
479
- /** A svelte lifecycle/context call guaranteed to run outside component initialisation — it throws `lifecycle_outside_component` at runtime (correctness/orphan-lifecycle). */
480
- interface OrphanLifecycleCallFact {
481
- /** Canonical svelte export name (alias-resolved), e.g. 'onMount'. */
482
- name: string;
483
- /** 1-based source line, or 0 if unknown. For 'constructor-instantiated', the module-scope `new` site. */
484
- line: number;
485
- /** 'top-level' = runs at module evaluation; 'constructor-instantiated' = module-scope `new` of a same-file class whose constructor calls a tracked function. */
486
- kind: 'top-level' | 'constructor-instantiated';
487
- /** Class name when kind is 'constructor-instantiated' (used in the finding message). */
488
- className?: string;
489
- }
490
- /** 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). */
491
- interface BrowserGlobalRefFact {
492
- /** The global's name, e.g. 'window'. */
493
- name: string;
494
- /** 1-based source line, or 0 if unknown. */
495
- line: number;
496
- /** '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). */
497
- context: 'module' | 'instance';
498
- }
499
- /** A flagged source position in a component (e.g. an `{@html}` tag or a `javascript:` URL). */
500
- interface SourceSpan {
501
- /** 1-based source line, or 0 if unknown. */
502
- line: number;
503
- }
504
- /** An inline `svelte-vitals-disable-next-line` directive found in the component's source (issue #92). */
505
- interface SuppressionDirective {
506
- /** 1-based line the directive suppresses (the line immediately after the comment). */
507
- line: number;
508
- /** Rule ids suppressed on that line; undefined = suppress every rule on that line. */
509
- ruleIds?: string[];
510
- }
511
- /** An `<input type="checkbox">` / `<input type="radio">` element carrying a `bind:value`
512
- * directive — `bind:value` observes the DOM `value` property, which checkbox/radio
513
- * interaction never changes, so the bound state silently never updates
514
- * (correctness/checkable-bind-value). */
515
- interface CheckableBindValueFact {
516
- /** Which checkable input type was flagged — selects the message wording. */
517
- kind: 'checkbox' | 'radio';
518
- /** 1-based source line, or 0 if unknown. */
519
- line: number;
520
- }
521
- /** A root-relative navigation literal — broken when the app is served under `kit.paths.base`
522
- * (correctness/base-path-navigation). Shared by the component and Kit-module channels. */
523
- interface BasePathLinkFact {
524
- /** Which navigation surface it was written on — selects the message wording. */
525
- kind: 'href' | 'goto' | 'redirect';
526
- /** The literal path as written, e.g. '/about'. */
527
- path: string;
528
- /** 1-based source line, or 0 if unknown. */
529
- line: number;
530
- }
531
- /** An interactive element (e.g. `<button>`) found nested inside another interactive
532
- * container (e.g. `<a href>`) (a11y/interactive-nesting). */
533
- interface InteractiveNestingFact {
534
- containerTag: string;
535
- /** The container's literal `role`, when that is what made it a container rather than its tag. */
536
- containerRole?: string;
537
- descendantTag: string;
538
- /** 1-based source line of the descendant, or 0 if unknown. */
539
- line: number;
540
- }
541
- /** A `button`/`a href`/`input type="image"` with no computable accessible name (a11y/accessible-name). */
542
- interface UnnamedInteractiveFact {
543
- tag: string;
544
- /** 1-based source line, or 0 if unknown. */
545
- line: number;
546
- }
547
- /** An element carrying a `role` and/or `aria-*` attribute(s) (a11y ARIA rules). */
548
- interface AriaElementFact {
549
- tag: string;
651
+ lazy: boolean;
652
+ /** True when the <img> has a `srcset` attribute (performance/responsive-image). */
653
+ hasSrcset: boolean;
550
654
  /** 1-based source line, or 0 if unknown. */
551
655
  line: number;
552
- /** literal role value; undefined = no role attr; { expression: true } = dynamic */
553
- role?: {
554
- literal?: string;
555
- expression?: boolean;
556
- };
557
- /** every aria-* attribute on the element */
558
- aria: {
559
- name: string;
560
- literal?: string;
561
- expression?: boolean;
562
- line: number;
563
- }[];
564
- /** literal `type` of an `<input>`, lowercased; undefined for non-inputs or a dynamic type */
565
- inputType?: string;
566
- /** Set when the element also carries a spread attribute — its full attribute set is
567
- * unknowable, so required-prop presence checks must treat it as satisfied (a11y/required-aria-props). */
568
- hasSpread?: true;
656
+ /** Source file the <img> came from. */
657
+ file: string;
569
658
  }
570
- /** Reactivity/correctness + security + architecture facts parsed from one `.svelte` component. */
571
- interface ComponentFacts {
572
- /** Source file the component came from. */
659
+ /** Resolved <img> elements for a single route (page + layout chain). */
660
+ interface ResolvedImages {
661
+ route: string;
662
+ images: ImageInfo[];
663
+ }
664
+
665
+ /**
666
+ * A normalized page-body heading occurrence — the mode-independent boundary for
667
+ * the heading-hierarchy rule (mirrors images.ts). Both providers collect these
668
+ * so seo/single-h1 never needs to know which mode produced them.
669
+ */
670
+ interface HeadingInfo {
671
+ /** Heading level 1–6 (the `n` in <hn>). */
672
+ level: number;
673
+ /** 1-based source line, or 0 if unknown (rendered mode does not track lines). */
674
+ line: number;
675
+ /** Source file the heading came from. */
573
676
  file: string;
574
- eachBlocks: EachBlockFact[];
575
- effects: EffectFact[];
576
- /** `{@html …}` occurrences — raw-HTML render surfaces (security/raw-html). */
577
- htmlTags: SourceSpan[];
578
- /** Element attributes with a literal `javascript:` URL (security/javascript-url). */
579
- javascriptUrls: SourceSpan[];
580
- /** Source line count of the component file (architecture/component-size). */
581
- loc: number;
582
- /** Named props destructured from `$props()`; 0 when unknowable (rest / non-destructured) (architecture/prop-count). */
583
- propCount: number;
584
- /** Module specifiers of every `import` in the instance + module scripts (performance/heavy-import). */
585
- imports: string[];
677
+ }
678
+ /** Resolved page-body headings for a single route (page + layout chain). */
679
+ interface ResolvedHeadings {
680
+ route: string;
681
+ headings: HeadingInfo[];
586
682
  /**
587
- * Module specifiers of every `import`, each with its source line (performance/heavy-import,
588
- * architecture/route-component-import). `type` marks a declaration that contributes **no runtime
589
- * value binding** either `import type …`, or one whose every specifier is inline-typed
590
- * (`import { type A } from …`). A specifier-less side-effect import is not marked: it still loads
591
- * the module. Optional, so existing external constructors of `ComponentFacts` are unaffected.
683
+ * Headings found in child components rendered (transitively) by this route's
684
+ * chain files source mode only; absent in rendered mode. Kept separate from
685
+ * `headings` because their position in document order is unknown: safe for
686
+ * counting (seo/single-h1), unusable for outline order (seo/heading-level-skip).
592
687
  */
593
- importSpans: {
594
- source: string;
595
- line: number;
596
- type?: true;
597
- }[];
598
- /** Value `import * as X from '<bare pkg>'` namespace imports (type-only excluded) — performance/namespace-import. */
599
- namespaceImports: {
600
- source: string;
601
- line: number;
602
- }[];
603
- /** `$state` declarations never written or escaped anywhere in the component — candidates for const (correctness/unmutated-state). */
604
- constableStates: {
605
- name: string;
606
- line: number;
607
- }[];
608
- /** 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. */
609
- mutatedProps: {
610
- name: string;
611
- line: number;
612
- legacy?: boolean;
613
- }[];
614
- /** 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. */
615
- stalePropDerivations: {
616
- name: string;
617
- line: number;
618
- legacy?: boolean;
619
- }[];
620
- /** Object/array-literal $state bindings reassigned at least once but never mutated, escaped, aliased, or item-edited — $state.raw candidates (performance/state-raw). */
621
- rawableStates: {
622
- name: string;
623
- line: number;
624
- }[];
625
- /** 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). */
626
- nonreactiveBuiltinStates: {
627
- name: string;
628
- type: string;
629
- line: number;
630
- }[];
631
- /** `<input type="checkbox">` / `<input type="radio">` elements bound with `bind:value`
632
- * instead of `bind:checked`/`bind:group` (correctness/checkable-bind-value). */
633
- checkableBindValues: CheckableBindValueFact[];
634
- /** Root-relative `<a href>` and `goto()` literals in this component (correctness/base-path-navigation). */
635
- basePathLinks: BasePathLinkFact[];
636
- /** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-effect). */
637
- orphanEffects: OrphanEffectFact[];
638
- /** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-lifecycle). */
639
- orphanLifecycleCalls: OrphanLifecycleCallFact[];
640
- /** Browser-global reads in server-executed positions of this file (correctness/server-browser-global, correctness/instance-browser-global). */
641
- browserGlobalRefs: BrowserGlobalRefFact[];
642
- /** 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. */
643
- moduleStateDecls: {
644
- name: string;
645
- line: number;
646
- }[];
647
- /** 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. */
648
- suppressions?: SuppressionDirective[];
649
- /** Markdown links `[label](url)` appearing inside a comment (architecture/doc-link-target). */
650
- commentLinks: {
651
- url: string;
652
- line: number;
653
- }[];
654
- /** Elements carrying a role or any aria-* attribute (a11y ARIA rules). */
655
- ariaElements?: AriaElementFact[];
656
- /** Interactive elements nested inside another interactive container (a11y/interactive-nesting). */
657
- interactiveNestings?: InteractiveNestingFact[];
658
- /** `button`/`a href`/`input type="image"` elements with no computable accessible name (a11y/accessible-name). */
659
- unnamedInteractive?: UnnamedInteractiveFact[];
660
- /** `<label>` elements with neither a `for` attribute nor a wrapped labelable descendant (a11y/label-has-control). */
661
- unassociatedLabels?: {
662
- line: number;
663
- }[];
664
- /** Text nodes whose trimmed content opens with a bullet character followed by whitespace, outside any `li` (a11y/use-list). */
665
- bulletTexts?: {
666
- line: number;
667
- char: string;
668
- }[];
669
- /** `<select required>` (no `multiple`, display size absent or ≤ 1) whose first `option` element
670
- * child is not a placeholder label option (a11y/placeholder-label-option). */
671
- selectsMissingPlaceholder?: {
688
+ componentHeadings?: HeadingInfo[];
689
+ }
690
+
691
+ /** One step of a template branch address: which exclusive block, and which arm of it. */
692
+ interface BranchStep {
693
+ /** index of the {#if}/{#await} block among its file's blocks (document order) */
694
+ group: number;
695
+ /** branch index within the group (if: 0..n consequent→else; await: 0=pending,1=then,2=catch) */
696
+ branch: number;
697
+ }
698
+ /** Where a folded occurrence sits, for the finding location. */
699
+ interface A11yOccurrenceInfo {
700
+ file: string;
701
+ line: number;
702
+ }
703
+ /**
704
+ * Route-scoped a11y facts, the mode-independent boundary for the landmark/id rules
705
+ * (mirrors headings.ts). Source mode composes the layout chain plus its resolved
706
+ * components; rendered mode reads the prerendered document.
707
+ */
708
+ interface ResolvedA11y {
709
+ route: string;
710
+ /** representatives per landmark kind after the branch-aware fold ('main' | 'banner' | 'contentinfo' | 'complementary') */
711
+ landmarks: Record<string, A11yOccurrenceInfo[]>;
712
+ /** landmark occurrences nested inside another landmark after composition */
713
+ nestedLandmarks: {
714
+ kind: string;
715
+ within: string;
716
+ file: string;
672
717
  line: number;
673
718
  }[];
674
- /** `<time>` with no `datetime` attribute whose literal text content is not machine-readable (a11y/require-datetime). */
675
- timesMissingDatetime?: {
719
+ /** representatives per literal id */
720
+ ids: Record<string, A11yOccurrenceInfo[]>;
721
+ /** literal id references */
722
+ idRefs: {
723
+ id: string;
724
+ attr: string;
725
+ file: string;
676
726
  line: number;
677
- text: string;
678
727
  }[];
679
- /** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
680
- parseFailed?: true;
681
- /** Set when the file could not be READ an environment problem (permissions, a descriptor
682
- * limit), not a malformed component. Reported separately so one does not masquerade as the other. */
683
- readFailed?: true;
728
+ /** optimistic candidates: every literal id anywhere (all branches, each/snippet bodies, components, app.html) */
729
+ idCandidates: string[];
730
+ /** closed world holds: every component resolved, no depth truncation, no {@html}/spread, no dynamic id */
731
+ fullyResolved: boolean;
732
+ /**
733
+ * Distinct tag names in the route's body subtree — layout chain, page, every resolved component,
734
+ * and `app.html`'s `<body>` (static), or the prerendered `<body>` (rendered); optimistic across
735
+ * `{#if}` arms and `{#each}`/snippet bodies. Never `<svelte:head>` content, `<template>` children,
736
+ * or `<svelte:element>`. Absent where a provider does not collect it (a11y/required-element).
737
+ */
738
+ elementTags?: string[];
739
+ /**
740
+ * The closed world for elements: every component descended into (an unresolved, depth-truncated,
741
+ * or — conservatively — cycle-cut one clears it), no `{@html}`, no `<svelte:element>`. Incomparable with `fullyResolved` — a spread or `id={expr}` clears that flag
742
+ * and not this one, since neither can hide an element; a `<svelte:element>` clears this and not
743
+ * that. "Missing" is only reportable when this holds; presence is sound regardless.
744
+ */
745
+ elementsClosed?: boolean;
746
+ /** The file a route-level finding is anchored to: the page file (static) or the prerendered HTML path (rendered). */
747
+ file?: string;
684
748
  }
749
+ type Foldable = {
750
+ key: string;
751
+ path: BranchStep[];
752
+ repeatable: boolean;
753
+ };
685
754
  /**
686
- * Warnings for files a collector could not read or parse: the file contributes empty facts, so any
687
- * findings it would have produced are simply missing rather than reported as fixed. The two causes
688
- * are reported separately an unreadable file is an environment problem (permissions, a descriptor
689
- * limit) and a malformed one is the author's, and sharing a message is how a descriptor limit once
690
- * read as hundreds of broken components. Capped at 10 inline paths so one badly-broken directory
691
- * cannot flood the terminal.
755
+ * Branch-aware occurrence fold (design "Control-flow semantics"): within a branch
756
+ * occurrences sum, across the arms of one exclusive block the arm with the most
757
+ * occurrences wins (tie lowest branch index) and ITS occurrences are the group's
758
+ * representatives so a caller's count is always `list.length`, with a location per
759
+ * representative. `{#each}`/`{#snippet}` occurrences render 0..N times and drop out.
760
+ * The max is per key: there is no scalar total to maximize.
692
761
  */
693
- declare function skippedFileWarnings(facts: readonly {
694
- file: string;
695
- parseFailed?: true;
696
- readFailed?: true;
697
- }[]): string[];
762
+ declare function foldOccurrences<T extends Foldable>(nodes: T[]): Map<string, T[]>;
763
+ /**
764
+ * Decode a fragment identifier the way navigation does before matching an element id
765
+ * (`href="#caf%C3%A9"` targets `id="café"`). Malformed escapes are kept verbatim —
766
+ * the browser would also fail to decode them, so the raw text is the comparable form.
767
+ */
768
+ declare function decodeFragmentId(fragment: string): string;
769
+ /** Whitespace-split tokens of a (possibly undefined) literal attribute value. */
770
+ declare function splitTokens(value: string | undefined): string[];
771
+ /** Explicit `role` values that map to the landmark kinds the route rules inspect. */
772
+ declare const LANDMARK_ROLES: ReadonlySet<string>;
773
+ /**
774
+ * Attributes whose (whitespace-tokenized) values reference element ids: the ARIA id-reference and
775
+ * id-reference-list properties, and HTML's own (`for`, `list`, `headers`, `form`, the popover and
776
+ * command targets). `href="#…"` is handled separately — its value is a URL, not a token list.
777
+ */
778
+ declare const IDREF_ATTRS: readonly string[];
779
+ /**
780
+ * Whether a decoded URL fragment is HTML's "top of the document" indicator: `#top` (ASCII
781
+ * case-insensitive) scrolls to the top when no element has that id, so it is never a missing
782
+ * reference. Compare AFTER percent-decoding — `#%74op` navigates identically to `#top`.
783
+ */
784
+ declare function isTopFragment(id: string): boolean;
785
+ /**
786
+ * A fragment with its text directive removed. Everything from the first `:~:` on is user-agent
787
+ * instructions for finding text and names no element, while anything before it is still an
788
+ * ordinary element fragment — `#section:~:text=hi` targets `id="section"`, `#:~:text=hi` targets
789
+ * nothing. Returns an empty string when the fragment is a directive and nothing else.
790
+ */
791
+ declare function stripTextDirective(fragment: string): string;
698
792
 
699
793
  /**
700
794
  * Facts parsed from one SvelteKit route/hooks file for the SSR shared-state rules
@@ -856,6 +950,16 @@ type RuleOptionSpec = {
856
950
  } | {
857
951
  kind: 'string-list';
858
952
  default: readonly string[];
953
+ /**
954
+ * Grammar every entry must match, checked at config load. A declaration-driven rule reserves
955
+ * its grammar with this so a value the rule does not interpret today (`'input[type=file]'`
956
+ * for a tag-name list) is rejected rather than accepted-and-ignored — accepting it would make
957
+ * giving it meaning later a reinterpretation of a value the frozen schema already took.
958
+ */
959
+ pattern?: {
960
+ regex: RegExp;
961
+ describe: string;
962
+ };
859
963
  } | {
860
964
  kind: 'string-map';
861
965
  default: Readonly<Record<string, string>>;
@@ -974,9 +1078,9 @@ interface RuleContext {
974
1078
  headings?: ResolvedHeadings[];
975
1079
  /** Per-route composed landmark/id occurrences for the route-scoped a11y rules (absent in modes that don't collect them). */
976
1080
  a11y?: ResolvedA11y[];
977
- /** Per-file component-body facts for Correctness rules (static/CLI mode only). */
1081
+ /** Per-file component-body facts for the component-scoped rules (absent in the dev handle's rendered pass). */
978
1082
  components?: ComponentFacts[];
979
- /** Per-file SvelteKit route/hooks facts for the SSR shared-state rules (static/CLI + vite build mode only). */
1083
+ /** Per-file SvelteKit route/hooks facts for the kit-module rules (absent in the dev handle's rendered pass). */
980
1084
  kitModules?: KitModuleFacts[];
981
1085
  /**
982
1086
  * Every file under `src/`, as project-relative paths, for directory-shaped Architecture rules
@@ -1009,6 +1113,19 @@ interface Rule {
1009
1113
  fix?: Fix;
1010
1114
  /** Configurable options for this rule; absent means the rule takes none. */
1011
1115
  options?: RuleOptionsSpec;
1116
+ /**
1117
+ * The message this rule puts on a PASS result. Declared so a PASS synthesised elsewhere — the
1118
+ * central inline-suppression pass, which turns a fully-suppressed rule+route into a pass — reads
1119
+ * the same as one the rule emitted itself. Rules built through `componentRule` and the a11y
1120
+ * route factory supply it; the rest fall back to `title`, which is a cosmetic difference visible
1121
+ * only in `--verbose`'s passed listing.
1122
+ */
1123
+ passLabel?: string;
1124
+ /**
1125
+ * The rule compares routes against each other (`seo/duplicate-title`), so it cannot be judged
1126
+ * from one route's rendered HTML — the dev dashboard's live layer leaves it to the static pass.
1127
+ */
1128
+ crossRoute?: true;
1012
1129
  /**
1013
1130
  * Evaluate the resolved heads. A single rule may return one Result per route,
1014
1131
  * so it always returns an array. Project-scoped rules return a single element.
@@ -1135,4 +1252,4 @@ declare function formatJsonReport(results: Result[], config: Config, meta: {
1135
1252
  version: string;
1136
1253
  }, ruleIds?: readonly string[], examined?: Record<string, Record<string, number>>): string;
1137
1254
 
1138
- export { formatFailedRuleWarning as $, type A11yOccurrenceInfo as A, type BranchStep as B, type ComponentFacts as C, applyRuleSeverities as D, type EachBlockFact as E, type Fix as F, buildJsonReport as G, type HeadTag as H, type ImageInfo as I, type JsonReport as J, type KitAlias as K, LANDMARK_ROLES as L, classify as M, compileOverrides as N, type OrphanEffectFact as O, type Project as P, computeHealth as Q, type Runtime as R, type SuppressionDirective as S, computeScore as T, decodeFragmentId as U, type Value as V, defaultConfig as W, defaultProject as X, docsUrlFor as Y, effectiveSeverity as Z, foldOccurrences as _, type KitModuleFacts as a, formatJsonReport as a0, hasFailureAtOrAbove as a1, intOption as a2, isMentionedAnywhere as a3, isPenalized as a4, isTopFragment as a5, listOption as a6, mapOption as a7, overrideMatches as a8, resolveRuleOptions as a9, scoresByCategory as aa, selectRules as ab, settingOptions as ac, settingSeverity as ad, shouldSkipRangeCheck as ae, skippedFileWarnings as af, splitTokens as ag, stripTextDirective as ah, summarize as ai, validateRuleOptions as aj, validateRuleSetting as ak, withFailedRulesOff as al, withReadLimit as am, CATEGORIES as an, type Detection as ao, type Presence as ap, type RuleEvidence as aq, type RuleOptions as ar, type RuleOverride as as, type RuleSetting as at, type RuleSettingObject as au, type ScoreModel as av, type Summary as aw, type TreatDynamicAs as ax, defineConfig as ay, type Rule as b, type RuleContext as c, type Result as d, type Category as e, type Severity as f, type RuleOptionSpec as g, type ResolvedHead as h, type Config as i, type Classification as j, type CompiledOverride as k, type EffectFact as l, type HeadProvider as m, type HeadingInfo as n, type HealthResult as o, IDREF_ATTRS as p, READ_CONCURRENCY as q, type ResolvedA11y as r, type ResolvedHeadings as s, type ResolvedImages as t, type RuleOptionsSpec as u, type Scope as v, type ScoreOptions as w, type ScoreResult as x, type SourceSpan as y, applyOverrides as z };
1255
+ export { formatFailedRuleWarning as $, type A11yOccurrenceInfo as A, type BranchStep as B, type ComponentFacts as C, applyRuleSeverities as D, type EachBlockFact as E, type Fix as F, buildJsonReport as G, type HeadTag as H, type ImageInfo as I, type JsonReport as J, type KitAlias as K, LANDMARK_ROLES as L, classify as M, compileOverrides as N, type OrphanEffectFact as O, type Project as P, computeHealth as Q, type Result as R, type SuppressionDirective as S, computeScore as T, decodeFragmentId as U, type Value as V, defaultConfig as W, defaultProject as X, docsUrlFor as Y, effectiveSeverity as Z, foldOccurrences as _, type Rule as a, formatGithubReport as a0, formatJsonReport as a1, formatMarkdownReport as a2, hasFailureAtOrAbove as a3, intOption as a4, isMentionedAnywhere as a5, isPenalized as a6, isTopFragment as a7, listOption as a8, mapOption as a9, defineConfig as aA, overrideMatches as aa, resolveRuleOptions as ab, scoresByCategory as ac, selectRules as ad, settingOptions as ae, settingSeverity as af, shouldSkipRangeCheck as ag, skippedFileWarnings as ah, splitTokens as ai, stripTextDirective as aj, summarize as ak, validateRuleOptions as al, validateRuleSetting as am, withFailedRulesOff as an, withReadLimit as ao, CATEGORIES as ap, type Detection as aq, type Presence as ar, type RuleEvidence as as, type RuleOptions as at, type RuleOverride as au, type RuleSetting as av, type RuleSettingObject as aw, type ScoreModel as ax, type Summary as ay, type TreatDynamicAs as az, type Config as b, type Runtime as c, type KitModuleFacts as d, type RuleContext as e, type Category as f, type Severity as g, type RuleOptionSpec as h, type ResolvedHead as i, type Classification as j, type CompiledOverride as k, type EffectFact as l, type HeadProvider as m, type HeadingInfo as n, type HealthResult as o, IDREF_ATTRS as p, READ_CONCURRENCY as q, type ResolvedA11y as r, type ResolvedHeadings as s, type ResolvedImages as t, type RuleOptionsSpec as u, type Scope as v, type ScoreOptions as w, type ScoreResult as x, type SourceSpan as y, applyOverrides as z };