@svelte-vitals/core 0.39.0 → 0.40.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 CHANGED
@@ -309,6 +309,13 @@ interface HeadingInfo {
309
309
  interface ResolvedHeadings {
310
310
  route: string;
311
311
  headings: HeadingInfo[];
312
+ /**
313
+ * Headings found in child components rendered (transitively) by this route's
314
+ * chain files — source mode only; absent in rendered mode. Kept separate from
315
+ * `headings` because their position in document order is unknown: safe for
316
+ * counting (seo/single-h1), unusable for outline order (seo/heading-level-skip).
317
+ */
318
+ componentHeadings?: HeadingInfo[];
312
319
  }
313
320
 
314
321
  /**
@@ -479,6 +486,8 @@ interface ComponentFacts {
479
486
  url: string;
480
487
  line: number;
481
488
  }[];
489
+ /** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
490
+ parseFailed?: true;
482
491
  }
483
492
 
484
493
  /** What the per-file parsers produce — `ComponentFacts` minus `file`, with `suppressions` always present. */
@@ -599,6 +608,8 @@ interface KitModuleFacts {
599
608
  };
600
609
  /** Inline `svelte-vitals-disable-next-line` directives in this file. */
601
610
  suppressions: SuppressionDirective[];
611
+ /** Set when the file failed to read or parse and these facts are the empty fallback — the file was NOT analyzed. */
612
+ parseFailed?: true;
602
613
  }
603
614
 
604
615
  /**
@@ -1119,9 +1130,15 @@ declare const seoHreflang: Rule;
1119
1130
 
1120
1131
  /**
1121
1132
  * seo/single-h1 — Heading hierarchy (single H1). Reads the per-route page-body headings
1122
- * channel (collected by both providers). Zero <h1> (no primary heading) and two
1123
- * or more (diluted topic) are both flagged; exactly one passes. A route whose
1124
- * headings were not collected (channel unset) emits nothing.
1133
+ * channel (collected by both providers), counting `headings` plus `componentHeadings`
1134
+ * (static mode only headings found transitively in rendered child components) as one
1135
+ * combined list. Zero <h1> (no primary heading) is a `warning`: defensible, a page needs
1136
+ * a primary heading. Two or more is only `info`: a single <h1> is the conventional
1137
+ * signal, but no official source documents a ranking penalty for several (2026-08-09 v1
1138
+ * rule-validity review, P2 #11) — so it's flagged as a style nit, not a defect. Exactly
1139
+ * one passes. A route whose headings were not collected (channel unset) emits nothing. A
1140
+ * global `rules: { 'seo/single-h1': <severity> }` override flattens both arms to one
1141
+ * severity (design, `applyRuleSeverities`).
1125
1142
  */
1126
1143
  declare const seoSingleH1: Rule;
1127
1144
 
@@ -1179,8 +1196,9 @@ declare const correctnessNonreactiveBuiltinState: Rule;
1179
1196
 
1180
1197
  /**
1181
1198
  * correctness/checkable-bind-value — bind:value binds the DOM value property. A
1182
- * checkbox/radio's user interaction toggles checkedness, which bind:value never observes, so
1183
- * the bound state is frozen at its initial value and silently never updates in production.
1199
+ * checkbox/radio's user interaction toggles checkedness, which bind:value never observes. A
1200
+ * checkbox throws bind_invalid_checkbox_value in dev (silently tracks value instead of
1201
+ * checkedness in prod); a radio throws nothing and its bound state silently never updates.
1184
1202
  * bind:checked (single checkbox) / bind:group (checkbox list, radio group) are the correct
1185
1203
  * bindings.
1186
1204
  */
package/dist/index.js CHANGED
@@ -94,7 +94,7 @@ function attrTextOf(attr) {
94
94
  // src/component-parse.ts
95
95
  function unwrapTs(expr) {
96
96
  let cur = expr;
97
- while (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression")
97
+ while (cur !== void 0 && (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression"))
98
98
  cur = cur.expression;
99
99
  return cur;
100
100
  }
@@ -623,6 +623,21 @@ function collectDirectiveEscapes(node, names, acc) {
623
623
  }
624
624
  }
625
625
  var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
626
+ function collectImportedLocalNames(program, acc) {
627
+ for (const stmt of program?.body ?? []) {
628
+ if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
629
+ for (const s of stmt.specifiers ?? []) {
630
+ if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
631
+ acc.add(s.local.name);
632
+ }
633
+ }
634
+ }
635
+ function collectNewExprLocalNames(program, acc) {
636
+ walkEstree(program, (n) => {
637
+ if (n.type !== "VariableDeclarator" || n.id?.type !== "Identifier" || !n.init) return;
638
+ if (unwrapTs(n.init).type === "NewExpression") acc.add(n.id.name);
639
+ });
640
+ }
626
641
  function bodyReadsReactive(fn, reactiveNames) {
627
642
  let reads = false;
628
643
  const visit = (n) => {
@@ -1398,6 +1413,12 @@ function parseComponentFacts(source, filename) {
1398
1413
  }
1399
1414
  const stateNames = /* @__PURE__ */ new Set();
1400
1415
  const reactiveNames = /* @__PURE__ */ new Set();
1416
+ collectImportedLocalNames(program, reactiveNames);
1417
+ collectNewExprLocalNames(program, reactiveNames);
1418
+ if (moduleProgram) {
1419
+ collectImportedLocalNames(moduleProgram, reactiveNames);
1420
+ collectNewExprLocalNames(moduleProgram, reactiveNames);
1421
+ }
1401
1422
  const stateDecls = [];
1402
1423
  walkEstree(program, (n) => {
1403
1424
  if (n.type !== "VariableDeclarator" || !n.init) return;
@@ -1566,7 +1587,7 @@ async function collectComponentFacts(rt, cwd) {
1566
1587
  const source = await rt.readFile(rt.join(cwd, rel));
1567
1588
  return { file: rel, ...parseComponentFacts(source, rel) };
1568
1589
  } catch {
1569
- return emptyComponentFacts(rel);
1590
+ return { ...emptyComponentFacts(rel), parseFailed: true };
1570
1591
  }
1571
1592
  })
1572
1593
  );
@@ -2146,7 +2167,7 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
2146
2167
  const source = await rt.readFile(rt.join(cwd, rel));
2147
2168
  return { file: rel, kind, ...parseKitModuleFacts(source, rel, aliases) };
2148
2169
  } catch {
2149
- return emptyKitModuleFacts(rel, kind);
2170
+ return { ...emptyKitModuleFacts(rel, kind), parseFailed: true };
2150
2171
  }
2151
2172
  })
2152
2173
  );
@@ -2536,7 +2557,7 @@ function headTagRule(opts) {
2536
2557
  var seoDescriptionPresence = headTagRule({
2537
2558
  id: "seo/description-presence",
2538
2559
  title: "Description presence",
2539
- severity: "critical",
2560
+ severity: "warning",
2540
2561
  match: (t) => t.kind === "meta" && t.name === "description",
2541
2562
  label: '<meta name="description">',
2542
2563
  recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.',
@@ -2556,7 +2577,7 @@ var seoCanonicalUrl = headTagRule({
2556
2577
  match: (t) => t.kind === "link" && t.rel === "canonical",
2557
2578
  label: '<link rel="canonical">',
2558
2579
  recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.',
2559
- rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query strings and trailing-slash variants.",
2580
+ rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query-string variants of the same page.",
2560
2581
  fix: {
2561
2582
  description: 'Add <link rel="canonical"> inside <svelte:head>, or set the canonical prop on your meta component.',
2562
2583
  snippet: '<svelte:head>\n <link rel="canonical" href="https://example.com/this-page" />\n</svelte:head>',
@@ -2693,7 +2714,7 @@ var seoHtmlLang = {
2693
2714
  category: "seo",
2694
2715
  severity: "warning",
2695
2716
  scope: "project",
2696
- rationale: "The <html lang> attribute declares the page language for search engines, screen readers, and translation tools.",
2717
+ rationale: "The <html lang> attribute tells screen readers how to pronounce the page, browsers whether to offer translation, and other assistive tools how to handle the content \u2014 Google has said it does not use lang for ranking.",
2697
2718
  fix: FIX4,
2698
2719
  async check(ctx) {
2699
2720
  const detection = ctx.project.htmlLang;
@@ -2778,7 +2799,7 @@ var performanceImageDimensions = imageRule({
2778
2799
  severity: "warning",
2779
2800
  label: "<img> width/height",
2780
2801
  recommendation: "Set explicit width and height on <img> to reserve space and avoid layout shift (CLS).",
2781
- rationale: "An <img> without explicit width and height triggers layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability.",
2802
+ rationale: "An <img> without explicit width and height can trigger layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability \u2014 unless the box is reserved another way, e.g. CSS aspect-ratio.",
2782
2803
  fix: {
2783
2804
  description: "Add explicit width and height attributes to the <img>.",
2784
2805
  snippet: '<img src="/hero.jpg" width="1200" height="630" alt="\u2026" />',
@@ -3233,7 +3254,7 @@ var performancePreconnect = {
3233
3254
  rationale: "Connecting to a third-party origin (DNS + TCP + TLS) is costly; a preconnect/dns-prefetch hint starts it early so the resource arrives sooner.",
3234
3255
  fix: {
3235
3256
  description: "Add a preconnect hint for the third-party origin.",
3236
- snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
3257
+ snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
3237
3258
  lang: "html"
3238
3259
  },
3239
3260
  options: OPTIONS,
@@ -3357,11 +3378,11 @@ var seoTwitterCard = headTagRule({
3357
3378
  var seoOgDescription = headTagRule({
3358
3379
  id: "seo/og-description",
3359
3380
  title: "Open Graph description",
3360
- severity: "warning",
3381
+ severity: "info",
3361
3382
  match: (t) => t.kind === "meta" && t.property === "og:description",
3362
3383
  label: '<meta property="og:description">',
3363
3384
  recommendation: 'Add <meta property="og:description">, or set openGraph.description on your meta component.',
3364
- rationale: "og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through.",
3385
+ rationale: "og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through. The Open Graph protocol lists it as an optional property.",
3365
3386
  fix: {
3366
3387
  description: "Add an og:description meta tag in <svelte:head>.",
3367
3388
  snippet: '<svelte:head>\n <meta property="og:description" content="A concise page summary." />\n</svelte:head>',
@@ -3373,11 +3394,11 @@ var seoOgDescription = headTagRule({
3373
3394
  var seoOgUrl = headTagRule({
3374
3395
  id: "seo/og-url",
3375
3396
  title: "Open Graph URL",
3376
- severity: "info",
3397
+ severity: "warning",
3377
3398
  match: (t) => t.kind === "meta" && t.property === "og:url",
3378
3399
  label: '<meta property="og:url">',
3379
3400
  recommendation: 'Add <meta property="og:url"> with the canonical URL, or set openGraph.url on your meta component.',
3380
- rationale: "og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL.",
3401
+ rationale: "og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL. The Open Graph protocol lists it as a required property.",
3381
3402
  fix: {
3382
3403
  description: "Add an og:url meta tag in <svelte:head>.",
3383
3404
  snippet: '<svelte:head>\n <meta property="og:url" content="https://example.com/this-page" />\n</svelte:head>',
@@ -3397,7 +3418,7 @@ var seoViewport = headTagRule({
3397
3418
  // silent there instead of false-flagging "missing" on every route.
3398
3419
  appliesTo: (head) => head.source === "rendered",
3399
3420
  recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> (usually in app.html).',
3400
- rationale: "Without a viewport meta tag the page is not mobile-responsive, which Google penalizes under mobile-first indexing.",
3421
+ rationale: "Without a viewport meta tag mobile browsers render the page at a fixed ~980px layout viewport and scale it to fit, so text and controls end up too small to read or tap without pinch-zooming.",
3401
3422
  fix: {
3402
3423
  description: "Add the viewport meta tag (typically in src/app.html <head>).",
3403
3424
  snippet: '<meta name="viewport" content="width=device-width, initial-scale=1" />',
@@ -3562,19 +3583,25 @@ function hasNonEmpty(node, key) {
3562
3583
  return true;
3563
3584
  }
3564
3585
  var REQUIRED_PROPS = {
3565
- Article: ["headline"],
3566
- BlogPosting: ["headline"],
3567
- NewsArticle: ["headline"],
3568
- Product: ["name", "offers"],
3586
+ Product: { all: ["name"], oneOf: ["review", "aggregateRating", "offers"] },
3569
3587
  BreadcrumbList: ["itemListElement"],
3570
- Organization: ["name", "url"],
3571
3588
  WebSite: ["name", "url"],
3572
3589
  Event: ["name", "startDate", "location"],
3573
- Recipe: ["name", "image", "recipeIngredient", "recipeInstructions"],
3574
- Person: ["name"],
3575
- VideoObject: ["name", "description", "thumbnailUrl", "uploadDate"],
3590
+ Recipe: ["name", "image"],
3591
+ VideoObject: ["name", "thumbnailUrl", "uploadDate"],
3576
3592
  LocalBusiness: ["name", "address"]
3577
3593
  };
3594
+ function oneOfLabel(props) {
3595
+ const last = props.at(-1) ?? "";
3596
+ return props.length <= 1 ? last : `one of ${props.slice(0, -1).join(", ")} or ${last}`;
3597
+ }
3598
+ function missingRequiredProps(node, row) {
3599
+ const all = Array.isArray(row) ? row : row.all;
3600
+ const missing = all.filter((p) => !hasNonEmpty(node, p));
3601
+ const oneOf = Array.isArray(row) ? void 0 : row.oneOf;
3602
+ if (oneOf && !oneOf.some((p) => hasNonEmpty(node, p))) missing.push(oneOfLabel(oneOf));
3603
+ return missing;
3604
+ }
3578
3605
  function jsonldTags(head) {
3579
3606
  return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
3580
3607
  }
@@ -3766,7 +3793,7 @@ var seoJsonLdRequiredProps = jsonldRule({
3766
3793
  const required = REQUIRED_PROPS[t];
3767
3794
  if (!required) continue;
3768
3795
  hasKnownType = true;
3769
- const missing = required.filter((p) => !hasNonEmpty(node, p));
3796
+ const missing = missingRequiredProps(node, required);
3770
3797
  if (missing.length > 0) return `${t} JSON-LD is missing required ${missing.join(", ")}`;
3771
3798
  }
3772
3799
  }
@@ -3808,7 +3835,7 @@ function lengthRule(opts) {
3808
3835
  const o = resolveRuleOptions(opts.id, spec, ctx.config, { route: head.route, file: location }, compiled);
3809
3836
  const min = intOption(o, "min", opts.min);
3810
3837
  const max = intOption(o, "max", opts.max);
3811
- const recommendation12 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3838
+ const recommendation10 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
3812
3839
  const len = visibleLength(tag.text);
3813
3840
  let problem;
3814
3841
  if (len < min) problem = `${opts.noun} is too short (${len} chars; aim for ${min}\u2013${max})`;
@@ -3822,7 +3849,7 @@ function lengthRule(opts) {
3822
3849
  route: head.route,
3823
3850
  location,
3824
3851
  message: problem,
3825
- recommendation: recommendation12,
3852
+ recommendation: recommendation10,
3826
3853
  docsUrl: docsUrl12
3827
3854
  } : {
3828
3855
  id: opts.id,
@@ -3836,7 +3863,7 @@ function lengthRule(opts) {
3836
3863
  // it to also apply `severity: 'off'`.
3837
3864
  location,
3838
3865
  message: opts.label,
3839
- recommendation: recommendation12,
3866
+ recommendation: recommendation10,
3840
3867
  docsUrl: docsUrl12
3841
3868
  }
3842
3869
  );
@@ -3904,7 +3931,7 @@ var seoImageAlt = imageRule({
3904
3931
  rationale: "An <img> with no alt attribute is invisible to image search and assistive technology; a descriptive alt is an image-SEO signal.",
3905
3932
  fix: {
3906
3933
  description: 'Add a descriptive alt attribute to the <img> (or alt="" if purely decorative).',
3907
- snippet: '<img src="/photo.jpg" width="800" height="600" alt="Description of the image" />',
3934
+ snippet: '<img src="/photo.jpg" width="800" height="600" alt="Golden retriever catching a frisbee in a park" />',
3908
3935
  lang: "svelte"
3909
3936
  },
3910
3937
  ok: (img) => img.hasAlt
@@ -3912,7 +3939,9 @@ var seoImageAlt = imageRule({
3912
3939
 
3913
3940
  // src/rules/seo/hreflang.ts
3914
3941
  var docsUrl4 = docsUrlFor("seo/hreflang");
3915
- var recommendation4 = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") and include an x-default when you have multiple language alternates.';
3942
+ var malformedRecommendation = 'Use valid hreflang codes, e.g. "en", "en-US", or the literal "x-default".';
3943
+ var noDefaultRecommendation = "x-default is a Google recommendation, not a requirement \u2014 most useful for a language-selector or auto-redirecting page. Add one if this page behaves that way; otherwise search engines fall back to matching the individual alternates.";
3944
+ var passRecommendation = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") for your language alternates.';
3916
3945
  var HREFLANG_RE = /^[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?$/i;
3917
3946
  function isValidHreflang(v) {
3918
3947
  return v.toLowerCase() === "x-default" || HREFLANG_RE.test(v);
@@ -3923,7 +3952,7 @@ var seoHreflang = {
3923
3952
  category: "seo",
3924
3953
  severity: "warning",
3925
3954
  scope: "route",
3926
- rationale: "A malformed hreflang code or a missing x-default breaks international targeting, so search engines may serve the wrong language version.",
3955
+ rationale: "A malformed hreflang code breaks international targeting outright. A missing x-default is a Google recommendation for language-selector or auto-redirecting pages, not a defect on every multilingual site.",
3927
3956
  async check(ctx) {
3928
3957
  const out = [];
3929
3958
  for (const head of ctx.heads) {
@@ -3936,10 +3965,13 @@ var seoHreflang = {
3936
3965
  let problem;
3937
3966
  let location = head.file;
3938
3967
  if (badTag) {
3939
- problem = `Invalid hreflang value "${badTag.hreflang}"`;
3968
+ problem = { message: `Invalid hreflang value "${badTag.hreflang}"`, recommendation: malformedRecommendation };
3940
3969
  location = badTag.file ?? head.file;
3941
3970
  } else if (values.length >= 2 && !values.some((v) => v.toLowerCase() === "x-default")) {
3942
- problem = "Multiple hreflang alternates without an x-default";
3971
+ problem = {
3972
+ message: "Multiple hreflang alternates with no x-default declared",
3973
+ recommendation: noDefaultRecommendation
3974
+ };
3943
3975
  }
3944
3976
  out.push(
3945
3977
  problem ? {
@@ -3949,8 +3981,8 @@ var seoHreflang = {
3949
3981
  detection: PENALIZED,
3950
3982
  route: head.route,
3951
3983
  location,
3952
- message: problem,
3953
- recommendation: recommendation4,
3984
+ message: problem.message,
3985
+ recommendation: problem.recommendation,
3954
3986
  docsUrl: docsUrl4
3955
3987
  } : {
3956
3988
  id: "seo/hreflang",
@@ -3962,7 +3994,7 @@ var seoHreflang = {
3962
3994
  // 2026-08-08-pass-result-location-design.md).
3963
3995
  location,
3964
3996
  message: "hreflang",
3965
- recommendation: recommendation4,
3997
+ recommendation: passRecommendation,
3966
3998
  docsUrl: docsUrl4
3967
3999
  }
3968
4000
  );
@@ -3973,26 +4005,33 @@ var seoHreflang = {
3973
4005
 
3974
4006
  // src/rules/seo/single-h1.ts
3975
4007
  var docsUrl5 = docsUrlFor("seo/single-h1");
3976
- var recommendation5 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
4008
+ var passRecommendation2 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
4009
+ var missingRecommendation = "Add a single, descriptive <h1> naming the page's main topic.";
4010
+ var multipleRecommendation = "A single, clear <h1> is the conventional signal for a page \u2014 consider demoting extra top-level headings to <h2>+.";
3977
4011
  var seoSingleH1 = {
3978
4012
  id: "seo/single-h1",
3979
4013
  title: "Heading hierarchy",
3980
4014
  category: "seo",
3981
4015
  severity: "warning",
3982
4016
  scope: "route",
3983
- rationale: "Each page should have exactly one <h1> naming its main topic; none leaves the page without a primary heading, and several dilute the topic signal.",
4017
+ rationale: "A page should have a primary heading naming its main topic. Zero <h1> leaves the page without one; a single, clear <h1> is the conventional signal, though multiple <h1>s are tolerated by modern heading algorithms.",
3984
4018
  async check(ctx) {
3985
4019
  const out = [];
3986
4020
  for (const route of ctx.headings ?? []) {
3987
- const h1 = route.headings.filter((h) => h.level === 1);
4021
+ const combined = [...route.headings, ...route.componentHeadings ?? []];
4022
+ const h1 = combined.filter((h) => h.level === 1);
3988
4023
  let problem;
3989
4024
  let where = {};
3990
4025
  if (h1.length === 0) {
3991
- problem = "Missing <h1>";
4026
+ problem = { message: "Missing <h1>", severity: "warning", recommendation: missingRecommendation };
3992
4027
  const first = route.headings[0];
3993
4028
  if (first) where = { location: first.file, ...first.line > 0 ? { line: first.line } : {} };
3994
4029
  } else if (h1.length > 1) {
3995
- problem = `Multiple <h1> (${h1.length}); use exactly one`;
4030
+ problem = {
4031
+ message: `Multiple <h1> (${h1.length}); a single <h1> is the conventional signal`,
4032
+ severity: "info",
4033
+ recommendation: multipleRecommendation
4034
+ };
3996
4035
  const extra = h1[1];
3997
4036
  where = { location: extra.file, ...extra.line > 0 ? { line: extra.line } : {} };
3998
4037
  }
@@ -4000,12 +4039,12 @@ var seoSingleH1 = {
4000
4039
  problem ? {
4001
4040
  id: "seo/single-h1",
4002
4041
  category: "seo",
4003
- severity: "warning",
4042
+ severity: problem.severity,
4004
4043
  detection: PENALIZED,
4005
4044
  route: route.route,
4006
4045
  ...where,
4007
- message: problem,
4008
- recommendation: recommendation5,
4046
+ message: problem.message,
4047
+ recommendation: problem.recommendation,
4009
4048
  docsUrl: docsUrl5
4010
4049
  } : {
4011
4050
  id: "seo/single-h1",
@@ -4018,7 +4057,7 @@ var seoSingleH1 = {
4018
4057
  // 2026-08-08-pass-result-location-design.md). Only reached when h1.length === 1.
4019
4058
  location: h1[0].file,
4020
4059
  message: "Heading hierarchy",
4021
- recommendation: recommendation5,
4060
+ recommendation: passRecommendation2,
4022
4061
  docsUrl: docsUrl5
4023
4062
  }
4024
4063
  );
@@ -4102,14 +4141,14 @@ var seoDuplicateDescription = uniquenessRule({
4102
4141
 
4103
4142
  // src/rules/seo/heading-level-skip.ts
4104
4143
  var docsUrl6 = docsUrlFor("seo/heading-level-skip");
4105
- var recommendation6 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
4144
+ var recommendation4 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
4106
4145
  var seoHeadingLevelSkip = {
4107
4146
  id: "seo/heading-level-skip",
4108
4147
  title: "Heading order",
4109
4148
  category: "seo",
4110
4149
  severity: "info",
4111
4150
  scope: "route",
4112
- rationale: "Skipping a heading level breaks the document outline that search engines and assistive tech rely on to understand page structure.",
4151
+ rationale: "Skipping a heading level breaks the document outline that assistive tech relies on to navigate page structure, and that search engines use as a structural signal.",
4113
4152
  async check(ctx) {
4114
4153
  const out = [];
4115
4154
  for (const route of ctx.headings ?? []) {
@@ -4134,7 +4173,7 @@ var seoHeadingLevelSkip = {
4134
4173
  location: skip.file,
4135
4174
  ...skip.line > 0 ? { line: skip.line } : {},
4136
4175
  message: `Heading level skipped (<h${skip.prev}> to <h${skip.level}>)`,
4137
- recommendation: recommendation6,
4176
+ recommendation: recommendation4,
4138
4177
  docsUrl: docsUrl6
4139
4178
  } : {
4140
4179
  id: "seo/heading-level-skip",
@@ -4148,7 +4187,7 @@ var seoHeadingLevelSkip = {
4148
4187
  // already continued above, so `[0]` is always defined here.
4149
4188
  location: route.headings[0].file,
4150
4189
  message: "Heading order",
4151
- recommendation: recommendation6,
4190
+ recommendation: recommendation4,
4152
4191
  docsUrl: docsUrl6
4153
4192
  }
4154
4193
  );
@@ -4258,7 +4297,7 @@ function componentRule(opts) {
4258
4297
  const compiled = compileOverrides(ctx.config);
4259
4298
  for (const c of ctx.components ?? []) {
4260
4299
  const o = resolveRuleOptions(opts.id, opts.options, ctx.config, { route: c.file, file: c.file }, compiled);
4261
- const recommendation12 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
4300
+ const recommendation10 = typeof opts.recommendation === "function" ? opts.recommendation(o) : opts.recommendation;
4262
4301
  if (!opts.applies(c, o, ctx)) continue;
4263
4302
  const bad = opts.bad(c, o, ctx).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
4264
4303
  if (bad.length === 0) {
@@ -4272,7 +4311,7 @@ function componentRule(opts) {
4272
4311
  // same location a penalized result for this file would carry.
4273
4312
  location: c.file,
4274
4313
  message: opts.label,
4275
- recommendation: recommendation12,
4314
+ recommendation: recommendation10,
4276
4315
  docsUrl: docsUrl12
4277
4316
  });
4278
4317
  continue;
@@ -4287,7 +4326,7 @@ function componentRule(opts) {
4287
4326
  location: c.file,
4288
4327
  ...b.line > 0 ? { line: b.line } : {},
4289
4328
  message: b.message,
4290
- recommendation: recommendation12,
4329
+ recommendation: recommendation10,
4291
4330
  docsUrl: docsUrl12,
4292
4331
  ...opts.fix ? { fix: { ...opts.fix } } : {}
4293
4332
  });
@@ -4343,10 +4382,13 @@ var correctnessEffectAsOnMount = componentRule({
4343
4382
  title: "Effect used as onMount",
4344
4383
  category: "correctness",
4345
4384
  label: "$effect usage",
4346
- recommendation: "Move mount-time side effects to onMount (import { onMount } from 'svelte'); reserve $effect for logic that reacts to $state/$derived/$props.",
4347
- rationale: "An $effect that reads no reactive value runs once after mount and never re-runs \u2014 it is an onMount in disguise, which obscures intent and misuses the reactivity system.",
4385
+ recommendation: "If this runs in response to a user interaction, use an event handler; to sync an element with an external library, use {@attach} instead. For genuine one-time mount work, use onMount (import { onMount } from 'svelte'). Reserve $effect for logic that reacts to $state/$derived/$props.",
4386
+ rationale: "An $effect whose body reads no reactive value visible to this analysis runs once after mount and never re-runs on the paths it can see \u2014 usually a sign the code belongs in an event handler, {@attach}, or onMount instead of $effect. This can't see a reactive value reached only through a plain function's return value, so a genuinely reactive effect built that way can still be flagged.",
4348
4387
  applies: (c) => c.effects.length > 0,
4349
- bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({ line: e.line, message: "$effect reads no reactive value \u2014 use onMount instead" }))
4388
+ bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({
4389
+ line: e.line,
4390
+ message: "$effect reads no reactive value this analysis can see \u2014 consider an event handler, {@attach}, or onMount instead"
4391
+ }))
4350
4392
  });
4351
4393
 
4352
4394
  // src/rules/correctness/unmutated-state.ts
@@ -4426,14 +4468,14 @@ var correctnessCheckableBindValue = componentRule({
4426
4468
  severity: "warning",
4427
4469
  label: "bind:checked / bind:group on checkable inputs",
4428
4470
  recommendation: "Replace bind:value with bind:checked (single checkbox) or bind:group (checkbox list / radio group).",
4429
- rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes \u2014 the bound state is frozen at its initial value. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
4471
+ rationale: "bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes. On a checkbox this throws bind_invalid_checkbox_value in a development build; in production the check is skipped and the binding silently tracks the value attribute instead of checkedness. On a radio it throws nothing in either build \u2014 it renders once with the initial value, then silently never updates. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this.",
4430
4472
  fix: {
4431
4473
  description: "For a single checkbox, replace bind:value={x} with bind:checked={x} (x becomes a boolean). For a checkbox list or radio group, replace bind:value={x} with bind:group={x} on every input sharing the group, keeping each input's static value attribute to identify the option."
4432
4474
  },
4433
4475
  applies: (c) => c.checkableBindValues.length > 0,
4434
4476
  bad: (c) => c.checkableBindValues.map((v) => ({
4435
4477
  line: v.line,
4436
- message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 the bound value silently never updates when the user toggles it. Use bind:checked (single checkbox) or bind:group (checkbox list) instead." : "bind:value on a radio input does not track which option is selected \u2014 the bound value silently never updates when the user picks one. Use bind:group with a shared group variable across the radio inputs instead."
4478
+ message: v.kind === "checkbox" ? "bind:value on a checkbox does not track its checked state \u2014 it throws bind_invalid_checkbox_value in development; in a production build it silently tracks the value attribute instead of checkedness. Use bind:checked (single checkbox) or bind:group (checkbox list) instead." : "bind:value on a radio input does not track which option is selected \u2014 the bound value silently never updates when the user picks one. Use bind:group with a shared group variable across the radio inputs instead."
4437
4479
  }))
4438
4480
  });
4439
4481
 
@@ -4445,7 +4487,7 @@ var correctnessOrphanEffect = componentRule({
4445
4487
  severity: "critical",
4446
4488
  label: "$effect context",
4447
4489
  recommendation: "Wrap the effect in $effect.root (and own the returned cleanup), or restructure so the effect is created during component initialisation (e.g. call a setup method from a component).",
4448
- rationale: "An $effect created outside component initialisation throws effect_orphan at runtime \u2014 the compiler does not catch it, and it typically surfaces as a production 500.",
4490
+ rationale: "An $effect created outside component initialisation throws effect_orphan at runtime. The compiler does not catch it \u2014 the server compiler deletes $effect calls entirely, so SSR renders without error \u2014 and the crash happens client-side, when the module evaluates in the browser, breaking hydration rather than producing a server error.",
4449
4491
  // `orphanEffects` is typed required, but a facts object built by an older/external
4450
4492
  // constructor may omit it — default to empty rather than let `applies` throw and
4451
4493
  // take the whole `runRules` Promise.all down with it.
@@ -4464,6 +4506,14 @@ var DOCS_URL = docsUrlFor(ID);
4464
4506
  var LABEL = "Lifecycle-call context";
4465
4507
  var RECOMMENDATION = "Call lifecycle/context functions during component initialisation (the top level of a component's <script>). In load, return the data and call setContext in a layout/page component; in shared modules, expose a setup function that components call during init.";
4466
4508
  var topLevelMessage = (name) => `${name}() runs at module evaluation, outside component initialisation \u2014 it throws lifecycle_outside_component at runtime`;
4509
+ var ALWAYS_THROWS = /* @__PURE__ */ new Set(["getContext", "setContext", "hasContext", "getAllContexts"]);
4510
+ function kitLifecycleMessage(name, kind, inHandler) {
4511
+ if (kind === "server" && !ALWAYS_THROWS.has(name)) {
4512
+ const where = inHandler ? `${name}() is called in a load/handler, outside component initialisation` : `${name}() runs outside component initialisation (module evaluation or the init hook)`;
4513
+ return name === "onDestroy" ? `${where} \u2014 on the server it still crashes, but with a plain TypeError, not lifecycle_outside_component (onDestroy has no component-context guard there)` : `${where} \u2014 on the server this is a silent no-op (it throws lifecycle_outside_component only if this module also runs in the browser)`;
4514
+ }
4515
+ return inHandler ? `${name}() is called in a load/handler \u2014 it runs on every request, outside component initialisation, and throws lifecycle_outside_component at runtime` : `${name}() runs outside component initialisation (module evaluation or the init hook) \u2014 it throws lifecycle_outside_component at runtime`;
4516
+ }
4467
4517
  function isSuppressed3(suppressions, line) {
4468
4518
  return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID)));
4469
4519
  }
@@ -4506,7 +4556,7 @@ var correctnessOrphanLifecycle = {
4506
4556
  category: "correctness",
4507
4557
  severity: "critical",
4508
4558
  scope: "component",
4509
- rationale: "Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime \u2014 the compiler does not catch it, and it surfaces as a production crash.",
4559
+ rationale: "Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime \u2014 the compiler does not catch it, and it surfaces as a production crash. Exception: in a Kit module that only ever runs on the server (+page.server.ts, +server.ts, hooks.server.ts), onMount/beforeUpdate/afterUpdate/createEventDispatcher are silent no-ops there instead, and onDestroy throws a plain TypeError rather than lifecycle_outside_component \u2014 only getContext/setContext/hasContext/getAllContexts still throw in that channel.",
4510
4560
  async check(ctx) {
4511
4561
  const out = [];
4512
4562
  for (const c of ctx.components ?? []) {
@@ -4530,7 +4580,7 @@ var correctnessOrphanLifecycle = {
4530
4580
  m.file,
4531
4581
  calls.map((l) => ({
4532
4582
  line: l.line,
4533
- message: l.inHandler ? `${l.name}() is called in a load/handler \u2014 it runs on every request, outside component initialisation, and throws lifecycle_outside_component at runtime` : `${l.name}() runs outside component initialisation (module evaluation or the init hook) \u2014 it throws lifecycle_outside_component at runtime`
4583
+ message: kitLifecycleMessage(l.name, m.kind, l.inHandler)
4534
4584
  })),
4535
4585
  m.suppressions
4536
4586
  );
@@ -4721,7 +4771,7 @@ var securityRawHtml = componentRule({
4721
4771
  title: "Raw HTML render",
4722
4772
  category: "security",
4723
4773
  label: "{@html} usage",
4724
- recommendation: "Sanitize the value before {@html} (e.g. DOMPurify), or render it as text/markup instead.",
4774
+ recommendation: "Sanitize the value before {@html} (e.g. DOMPurify), or render it as text/markup instead. A sanitizer keeps {@html} in the source, so the finding persists by design \u2014 once reviewed, suppress it with the inline directive.",
4725
4775
  rationale: "{@html} renders its value as unescaped HTML; if the value can contain user input and is not sanitized, it is a cross-site-scripting (XSS) vector.",
4726
4776
  applies: (c) => c.htmlTags.length > 0,
4727
4777
  bad: (c) => c.htmlTags.map((h) => ({ line: h.line, message: "{@html} renders unescaped HTML \u2014 ensure it is sanitized" }))
@@ -4734,7 +4784,7 @@ var securityJavascriptUrl = componentRule({
4734
4784
  category: "security",
4735
4785
  label: "No javascript: URLs",
4736
4786
  recommendation: "Use an event handler or a real URL instead of a javascript: URL.",
4737
- rationale: "A javascript: URL in href/src/action executes arbitrary script on activation \u2014 an XSS / unsafe-navigation vector that also breaks under a strict Content-Security-Policy.",
4787
+ rationale: "A javascript: URL in href/src/action/formaction breaks under a strict Content-Security-Policy and turns what should be a real navigation into inline script execution on activation \u2014 use an event handler on a <button> instead (the same shape is also a classic XSS vector, though detection here is literal-only, so every flagged URL is author-written, not injected).",
4738
4788
  applies: (c) => c.javascriptUrls.length > 0,
4739
4789
  bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
4740
4790
  });
@@ -4746,12 +4796,12 @@ var securityHandlerStateWrite = kitModuleRule({
4746
4796
  category: "security",
4747
4797
  severity: "critical",
4748
4798
  label: "Load/handler purity",
4749
- recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database.",
4750
- rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests \u2014 one user's data can be served to another.",
4751
- applies: (m) => m.importedStateWrites.length > 0,
4799
+ recommendation: "Return the data from load (or the action) and pass it via page data instead of writing it to module state; per-user data belongs in cookies/locals plus a database. Caches and rate limiters keyed by non-personal data are the benign shape \u2014 if that describes this write, add an inline suppression.",
4800
+ rationale: "SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests.",
4801
+ applies: (m) => m.importedStateWrites.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
4752
4802
  bad: (m) => m.importedStateWrites.map((w) => ({
4753
4803
  line: w.line,
4754
- message: `a server-executed handler writes imported module state "${w.name}" \u2014 shared across all requests on the server, one user's data can leak to another`
4804
+ message: `a server-executed handler writes imported module state "${w.name}" \u2014 module state is shared across all requests on the server; if this holds per-request or per-user data, one user's data can leak to another. Caches and rate limiters keyed by non-personal data are the benign shape \u2014 verify which this is`
4755
4805
  }))
4756
4806
  });
4757
4807
 
@@ -4781,7 +4831,7 @@ var securitySharedStateImport = kitModuleRule({
4781
4831
  label: "Server state imports",
4782
4832
  recommendation: "Keep module-scope $state out of server-executed code: return data from load and share it via page data or the context API. If the module is genuinely client-only, restructure so server files do not import it, or add an inline suppression.",
4783
4833
  rationale: "A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data.",
4784
- applies: (m) => m.runesModuleImports.length > 0,
4834
+ applies: (m) => m.runesModuleImports.length > 0 && !(m.kind === "universal" && m.ssrDisabled),
4785
4835
  bad: (m, ctx) => {
4786
4836
  const stateFiles = new Set((ctx.components ?? []).filter((c) => c.moduleStateDecls.length > 0).map((c) => c.file));
4787
4837
  const writtenOutside = new Set(m.importedStateWritesOutsideHandlers.map((w) => w.name));
@@ -4841,7 +4891,7 @@ var architecturePropCount = componentRule({
4841
4891
 
4842
4892
  // src/rules/architecture/private-scope-import.ts
4843
4893
  var docsUrl7 = docsUrlFor("architecture/private-scope-import");
4844
- var recommendation7 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
4894
+ var recommendation5 = "Move the unit to the directory shared by all of its importers, or import it only from inside its own scope.";
4845
4895
  var OPTIONS2 = { scopes: { kind: "string-list", default: [] } };
4846
4896
  function ancestorDirs(file) {
4847
4897
  const segments = file.split("/");
@@ -4927,7 +4977,7 @@ var architecturePrivateScopeImport = {
4927
4977
  // afterward (maintainer ruling, same date).
4928
4978
  location: c.file,
4929
4979
  message: "No private-scope imports",
4930
- recommendation: recommendation7,
4980
+ recommendation: recommendation5,
4931
4981
  docsUrl: docsUrl7
4932
4982
  });
4933
4983
  continue;
@@ -4942,7 +4992,7 @@ var architecturePrivateScopeImport = {
4942
4992
  location: c.file,
4943
4993
  ...v.line > 0 ? { line: v.line } : {},
4944
4994
  message: v.message,
4945
- recommendation: recommendation7,
4995
+ recommendation: recommendation5,
4946
4996
  docsUrl: docsUrl7,
4947
4997
  fix: { ...architecturePrivateScopeImport.fix }
4948
4998
  });
@@ -5067,7 +5117,7 @@ function classifyUnusedKeys(unused, excludedDirs, compile) {
5067
5117
  // src/rules/architecture/unit-entry-file.ts
5068
5118
  var ID4 = "architecture/unit-entry-file";
5069
5119
  var docsUrl8 = docsUrlFor(ID4);
5070
- var recommendation8 = "Give every declared unit directory a file named after it, or stop declaring that directory a unit.";
5120
+ var recommendation6 = "Give every declared unit directory a file named after it, or stop declaring that directory a unit.";
5071
5121
  var OPTIONS3 = {
5072
5122
  units: { kind: "string-map", default: {} },
5073
5123
  pascalCaseUnits: { kind: "string-map", default: {} },
@@ -5146,7 +5196,7 @@ var architectureUnitEntryFile = {
5146
5196
  detection: { presence: "own", value: "static" },
5147
5197
  location: expected,
5148
5198
  message: "Unit entry file",
5149
- recommendation: recommendation8,
5199
+ recommendation: recommendation6,
5150
5200
  docsUrl: docsUrl8
5151
5201
  });
5152
5202
  continue;
@@ -5161,7 +5211,7 @@ var architectureUnitEntryFile = {
5161
5211
  route: dir,
5162
5212
  location: at,
5163
5213
  message: `${dir} declares a unit but has no ${expected}`,
5164
- recommendation: recommendation8,
5214
+ recommendation: recommendation6,
5165
5215
  docsUrl: docsUrl8,
5166
5216
  // Which declaration matched decides the wording: a `units` match like functions/getFoo/
5167
5217
  // is already camelCase, so telling its author to rename it would be nonsense.
@@ -5226,7 +5276,7 @@ function satisfiesCasing(name, allowed) {
5226
5276
  // src/rules/architecture/directory-naming.ts
5227
5277
  var ID5 = "architecture/directory-naming";
5228
5278
  var docsUrl9 = docsUrlFor(ID5);
5229
- var recommendation9 = "Name each directory in the casing its location declares, or narrow the declaration.";
5279
+ var recommendation7 = "Name each directory in the casing its location declares, or narrow the declaration.";
5230
5280
  var OPTIONS4 = {
5231
5281
  directories: { kind: "string-map", default: {} },
5232
5282
  exclude: { kind: "string-list", default: [] }
@@ -5292,7 +5342,7 @@ var architectureDirectoryNaming = {
5292
5342
  route: dir,
5293
5343
  location: at,
5294
5344
  message: `${dir} must be ${allowed.join(" or ")}.`,
5295
- recommendation: recommendation9,
5345
+ recommendation: recommendation7,
5296
5346
  docsUrl: docsUrl9,
5297
5347
  fix: { description: "Rename the directory, or narrow the declaration that governs it." }
5298
5348
  });
@@ -5335,7 +5385,7 @@ var architectureDirectoryNaming = {
5335
5385
  // src/rules/architecture/reserved-directory-names.ts
5336
5386
  var ID6 = "architecture/reserved-directory-names";
5337
5387
  var docsUrl10 = docsUrlFor(ID6);
5338
- var recommendation10 = "Use one of the names this location declares, or add the new name to the declaration.";
5388
+ var recommendation8 = "Use one of the names this location declares, or add the new name to the declaration.";
5339
5389
  var OPTIONS5 = {
5340
5390
  scopes: { kind: "string-map", default: {} },
5341
5391
  unitScopes: { kind: "string-map", default: {} },
@@ -5494,7 +5544,7 @@ var architectureReservedDirectoryNames = {
5494
5544
  route: child,
5495
5545
  location: at,
5496
5546
  message: `${child} is not one of the names declared here: ${winner.names.join(", ")}.`,
5497
- recommendation: recommendation10,
5547
+ recommendation: recommendation8,
5498
5548
  docsUrl: docsUrl10,
5499
5549
  fix: {
5500
5550
  description: "Rename it to a declared name, move it under one of them, or add its name to the declaration."
@@ -5552,7 +5602,7 @@ var architectureReservedDirectoryNames = {
5552
5602
  // src/rules/architecture/reserved-name-placement.ts
5553
5603
  var ID7 = "architecture/reserved-name-placement";
5554
5604
  var docsUrl11 = docsUrlFor(ID7);
5555
- var recommendation11 = "Move it to one of the places declared for this name, or declare this place for it.";
5605
+ var recommendation9 = "Move it to one of the places declared for this name, or declare this place for it.";
5556
5606
  var fixDescription = "Move the directory to one of the places declared for its name, rename it, or declare this place for the name.";
5557
5607
  var OPTIONS6 = {
5558
5608
  placements: { kind: "string-map", default: {} },
@@ -5670,7 +5720,7 @@ var architectureReservedNamePlacement = {
5670
5720
  route: dir,
5671
5721
  location: at,
5672
5722
  message: `${dir} is not one of the places declared for '${name}'.`,
5673
- recommendation: recommendation11,
5723
+ recommendation: recommendation9,
5674
5724
  docsUrl: docsUrl11,
5675
5725
  fix: {
5676
5726
  description: fixDescription
@@ -5884,8 +5934,8 @@ var performanceNamespaceImport = componentRule({
5884
5934
  // src/rules/perf/minify-disabled.ts
5885
5935
  var PENALIZED7 = { presence: "none", value: "absent" };
5886
5936
  var MINIFY_DISABLED_FIX = {
5887
- description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
5888
- snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
5937
+ description: "Remove the minify: false override from vite.config (Vite minifies by default), or scope it to non-production builds.",
5938
+ snippet: "export default defineConfig({\n build: {\n // minify: false \u2014 removed; Vite minifies production builds by default\n }\n});",
5889
5939
  lang: "ts"
5890
5940
  };
5891
5941
  var RECOMMENDATION4 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
@@ -6465,7 +6515,7 @@ function formatAgentReport(results, config) {
6465
6515
  return lines.join("\n").replace(/\n+$/, "\n");
6466
6516
  }
6467
6517
  lines.push(
6468
- `${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes.`,
6518
+ `${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes. Run \`svelte-vitals explain <rule-id>\` for any rule's rationale and options (works offline).`,
6469
6519
  ""
6470
6520
  );
6471
6521
  const groups = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",