svelte-vitals 0.45.1 → 0.47.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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![MIT](https://img.shields.io/npm/l/svelte-vitals)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  > **A static SvelteKit code-health scanner — not a runtime Web Vitals reporter.**
7
- > Diagnose your project's SEO, Performance, Correctness, Security, and Architecture health by statically analyzing your source code, before it ships. No browser, no build server, no headless Chrome.
7
+ > Diagnose your project's SEO, Performance, Correctness, Security, Architecture, and Accessibility health by statically analyzing your source code, before it ships. No browser, no build server, no headless Chrome.
8
8
  >
9
9
  > **ESM-only** (Node 22.13+). Ships ES modules only; `require()` is unsupported by design.
10
10
 
package/dist/bin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runAnalyzeCliGunshi
4
- } from "./chunk-P4YNVJUO.js";
5
- import "./chunk-VNVRHWO3.js";
6
- import "./chunk-MUIOPL5F.js";
4
+ } from "./chunk-MZO3HVTN.js";
5
+ import "./chunk-5UQ6P2M2.js";
6
+ import "./chunk-43FGGQQZ.js";
7
7
  import "./chunk-M5KM5SV7.js";
8
8
  import {
9
9
  consoleIO
@@ -18,29 +18,34 @@ import {
18
18
 
19
19
  // src/cli.ts
20
20
  async function runCli(argv, io = consoleIO, env = process.env) {
21
- const locale = resolveLocale(env);
22
- if (argv[0] === "complete") {
23
- const { runCompleteCliGunshi } = await import("./complete-HGLO72TI.js");
24
- return { code: await runCompleteCliGunshi(argv, io), exit: "natural" };
21
+ try {
22
+ const locale = resolveLocale(env);
23
+ if (argv[0] === "complete") {
24
+ const { runCompleteCliGunshi } = await import("./complete-BDKGAUQX.js");
25
+ return { code: await runCompleteCliGunshi(argv, io), exit: "natural" };
26
+ }
27
+ if (argv[0] === "docs") {
28
+ const { runDocsCliGunshi } = await import("./docs-XNKZQBBZ.js");
29
+ return { code: await runDocsCliGunshi(argv.slice(1), io, locale), exit: "natural" };
30
+ }
31
+ if (argv[0] === "explain") {
32
+ const { runExplainCliGunshi } = await import("./explain-YPWGQE5N.js");
33
+ return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
34
+ }
35
+ if (argv[0] === "install") {
36
+ const { runInstallCliGunshi } = await import("./install-HE76S4ID.js");
37
+ return { code: await runInstallCliGunshi(argv.slice(1), io, locale), exit: "immediate" };
38
+ }
39
+ if (argv[0] === "ci") {
40
+ const { runCiCliGunshi } = await import("./ci-J7PGFHK3.js");
41
+ const code = await runCiCliGunshi(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog }, locale);
42
+ return { code, exit: "immediate" };
43
+ }
44
+ return await runAnalyzeCliGunshi(argv, io, locale);
45
+ } catch (err) {
46
+ io.errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
47
+ return { code: 2, exit: "natural" };
25
48
  }
26
- if (argv[0] === "docs") {
27
- const { runDocsCliGunshi } = await import("./docs-CRZC47P7.js");
28
- return { code: await runDocsCliGunshi(argv.slice(1), io, locale), exit: "natural" };
29
- }
30
- if (argv[0] === "explain") {
31
- const { runExplainCliGunshi } = await import("./explain-52QDQUOY.js");
32
- return { code: await runExplainCliGunshi(argv.slice(1), io, locale), exit: "natural" };
33
- }
34
- if (argv[0] === "install") {
35
- const { runInstallCliGunshi } = await import("./install-3FI7M5BM.js");
36
- return { code: await runInstallCliGunshi(argv.slice(1), io, locale), exit: "immediate" };
37
- }
38
- if (argv[0] === "ci") {
39
- const { runCiCliGunshi } = await import("./ci-CLKU3TR6.js");
40
- const code = await runCiCliGunshi(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog }, locale);
41
- return { code, exit: "immediate" };
42
- }
43
- return runAnalyzeCliGunshi(argv, io, locale);
44
49
  }
45
50
 
46
51
  // src/bin.ts
@@ -52,4 +57,7 @@ async function main() {
52
57
  process.exitCode = code;
53
58
  }
54
59
  }
55
- void main();
60
+ main().catch((err) => {
61
+ console.error(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
62
+ process.exit(2);
63
+ });
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/resolve-args.ts
10
10
  import { parseArgs } from "util";
11
- var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
11
+ var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture", "a11y"];
12
12
  var FAIL_ON_VALUES = ["critical", "warning", "info"];
13
13
  function isFailOnValue(value) {
14
14
  return typeof value === "string" && FAIL_ON_VALUES.includes(value);
@@ -32,7 +32,9 @@ import {
32
32
  applyRuleSeverities,
33
33
  applyOverrides,
34
34
  settingSeverity,
35
- withFailedRulesOff
35
+ withFailedRulesOff,
36
+ formatFailedRuleWarning,
37
+ terminalSafe
36
38
  } from "@svelte-vitals/core";
37
39
 
38
40
  // src/runtime/node.ts
@@ -144,10 +146,28 @@ function detectHtmlLang(html) {
144
146
  const value = match[1] ?? match[2] ?? match[3] ?? "";
145
147
  return { presence: "own", value: value.trim().length > 0 ? "static" : "absent" };
146
148
  }
147
- async function detectAppHtmlLang(rt, cwd) {
149
+ function detectAppHtmlIds(html) {
150
+ const markup = html.replace(/<!--[\s\S]*?-->/g, "").replace(/<script[\s\S]*?<\/script\s*>/gi, "").replace(/<style[\s\S]*?<\/style\s*>/gi, "");
151
+ const found = markup.matchAll(/(?<![\w-])id\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>{][^\s"'>]*))/gi);
152
+ return [...new Set([...found].map((m) => m[1] ?? m[2] ?? m[3] ?? "").filter(Boolean))];
153
+ }
154
+ async function detectAppHtmlFacts(rt, cwd) {
148
155
  const appHtmlPath = rt.join(cwd, "src/app.html");
149
- if (!await rt.exists(appHtmlPath)) return { presence: "none", value: "absent" };
150
- return detectHtmlLang(await rt.readFile(appHtmlPath));
156
+ if (!await rt.exists(appHtmlPath)) return { htmlLang: { presence: "none", value: "absent" } };
157
+ let content;
158
+ try {
159
+ content = await rt.readFile(appHtmlPath);
160
+ } catch {
161
+ return { htmlLang: { presence: "none", value: "absent" } };
162
+ }
163
+ return {
164
+ htmlLang: detectHtmlLang(content),
165
+ // Comments are stripped first, then a simple anchored match — a starred group over a lazy
166
+ // [\s\S]*? is ambiguous across iterations and backtracks exponentially on a comment run
167
+ // with no doctype (measured: ~45 leading comments hang the process).
168
+ appHtmlDoctype: /^\s*<!doctype\s+html/i.test(content.replace(/<!--[\s\S]*?-->/g, "")),
169
+ appHtmlIds: detectAppHtmlIds(content)
170
+ };
151
171
  }
152
172
  async function robotsRefsSitemap(rt, cwd) {
153
173
  const p = rt.join(cwd, "static/robots.txt");
@@ -194,10 +214,10 @@ async function detectKitConfigFacts(rt, cwd) {
194
214
  };
195
215
  }
196
216
  async function collectProjectFacts(rt, cwd) {
197
- const [hasRobotsTxt, hasSitemap, htmlLang, viteMinifyDisabled, kitConfig] = await Promise.all([
217
+ const [hasRobotsTxt, hasSitemap, appHtmlFacts, viteMinifyDisabled, kitConfig] = await Promise.all([
198
218
  existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
199
219
  existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
200
- detectAppHtmlLang(rt, cwd),
220
+ detectAppHtmlFacts(rt, cwd),
201
221
  detectViteMinifyDisabled(rt, cwd),
202
222
  detectKitConfigFacts(rt, cwd)
203
223
  ]);
@@ -205,7 +225,7 @@ async function collectProjectFacts(rt, cwd) {
205
225
  return {
206
226
  hasRobotsTxt,
207
227
  hasSitemap,
208
- htmlLang,
228
+ ...appHtmlFacts,
209
229
  ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
210
230
  ...viteMinifyDisabled ? { viteMinifyDisabled } : {},
211
231
  ...kitConfig
@@ -220,7 +240,7 @@ import {
220
240
  } from "@svelte-vitals/core";
221
241
 
222
242
  // src/providers/source/routes.ts
223
- import { defaultConfig } from "@svelte-vitals/core";
243
+ import { defaultConfig, foldOccurrences, isTopFragment } from "@svelte-vitals/core";
224
244
 
225
245
  // src/providers/source/resolve.ts
226
246
  import { resolveRepoLocalPath } from "@svelte-vitals/core";
@@ -367,7 +387,13 @@ import {
367
387
  valueFromNodes,
368
388
  textFromNodes,
369
389
  attrText,
370
- attrValue
390
+ attrTextOf as attrTextOf3,
391
+ attrValue,
392
+ attrValueOf as attrValueOf3,
393
+ decodeFragmentId,
394
+ splitTokens,
395
+ LANDMARK_ROLES,
396
+ IDREF_ATTRS
371
397
  } from "@svelte-vitals/core";
372
398
 
373
399
  // src/providers/source/imports.ts
@@ -555,6 +581,137 @@ function collectHeadings(node, source, acc) {
555
581
  if (key in node) collectHeadings(childOf(node, key), source, acc);
556
582
  }
557
583
  }
584
+ var LANDMARK_TAGS = { main: "main", header: "banner", footer: "contentinfo" };
585
+ var IDREF_ATTR_SET = new Set(IDREF_ATTRS);
586
+ function collectA11y(fragment, source) {
587
+ const nodes = [];
588
+ let groups = 0;
589
+ let slotInLandmark;
590
+ let unknowableContent = false;
591
+ const emit = (ctx, node) => {
592
+ const inLandmark = ctx.landmarks.at(-1);
593
+ nodes.push({ ...node, repeatable: ctx.repeatable, path: ctx.path, ...inLandmark ? { inLandmark } : {} });
594
+ };
595
+ const noteSpread = (node) => {
596
+ const attributes = node.attributes;
597
+ if (Array.isArray(attributes) && attributes.some((a) => a.type === "SpreadAttribute")) {
598
+ unknowableContent = true;
599
+ }
600
+ };
601
+ const walk = (node, ctx) => {
602
+ if (Array.isArray(node)) {
603
+ for (const child of node) walk(child, ctx);
604
+ return;
605
+ }
606
+ if (!node || typeof node !== "object") return;
607
+ switch (node.type) {
608
+ case "SvelteHead":
609
+ return;
610
+ // head content never renders into the body
611
+ case "HtmlTag":
612
+ unknowableContent = true;
613
+ return;
614
+ case "IfBlock":
615
+ walkIfChain(node, ctx, groups++, 0);
616
+ return;
617
+ case "AwaitBlock": {
618
+ const group = groups++;
619
+ walk(node.pending, { ...ctx, path: [...ctx.path, { group, branch: 0 }] });
620
+ walk(node.then, { ...ctx, path: [...ctx.path, { group, branch: 1 }] });
621
+ walk(node.catch, { ...ctx, path: [...ctx.path, { group, branch: 2 }] });
622
+ return;
623
+ }
624
+ case "EachBlock":
625
+ walk(node.body, { ...ctx, repeatable: true });
626
+ walk(node.fallback, ctx);
627
+ return;
628
+ case "SnippetBlock":
629
+ walk(node.body, { ...ctx, repeatable: true });
630
+ return;
631
+ // <svelte:element> has a dynamic tag (so no tag-derived landmark) but its literal id/idref
632
+ // attributes are real — dropping them would make no-missing-id-ref report phantom misses.
633
+ case "RegularElement":
634
+ case "SvelteElement":
635
+ walkElement(node, ctx);
636
+ return;
637
+ case "Component":
638
+ case "SvelteComponent":
639
+ case "SvelteSelf":
640
+ noteSpread(node);
641
+ emit(ctx, { kind: "component", key: node.name, line: lineOf(source, node.start) });
642
+ walk(node.fragment, { ...ctx, elementDepth: ctx.elementDepth + 1 });
643
+ return;
644
+ case "SlotElement":
645
+ noteSpread(node);
646
+ slotInLandmark ??= ctx.landmarks.at(-1);
647
+ walk(node.fragment, ctx);
648
+ return;
649
+ case "RenderTag":
650
+ if (isChildrenRender(node)) slotInLandmark ??= ctx.landmarks.at(-1);
651
+ return;
652
+ default:
653
+ noteSpread(node);
654
+ for (const key of CHILD_NODE_KEYS) {
655
+ if (key in node) walk(childOf(node, key), ctx);
656
+ }
657
+ }
658
+ };
659
+ const walkIfChain = (node, ctx, group, branch) => {
660
+ walk(node.consequent, { ...ctx, path: [...ctx.path, { group, branch }] });
661
+ if (!node.alternate) return;
662
+ const rest = node.alternate.nodes.filter((n) => n.type !== "Text" || n.data.trim() !== "");
663
+ const chained = rest.length === 1 && rest[0].type === "IfBlock" && rest[0].elseif ? rest[0] : void 0;
664
+ if (chained) walkIfChain(chained, ctx, group, branch + 1);
665
+ else walk(node.alternate, { ...ctx, path: [...ctx.path, { group, branch: branch + 1 }] });
666
+ };
667
+ const walkElement = (node, ctx) => {
668
+ noteSpread(node);
669
+ const line = lineOf(source, node.start);
670
+ const attrs = node.attributes;
671
+ const roleAttr = findAttr3(attrs, "role");
672
+ const role = roleAttr ? splitTokens(attrTextOf3(roleAttr))[0] : void 0;
673
+ const landmark = roleAttr ? role && LANDMARK_ROLES.has(role) ? role : void 0 : LANDMARK_TAGS[node.name];
674
+ if (landmark) {
675
+ const headerFooter = !roleAttr && node.name !== "main";
676
+ emit(ctx, {
677
+ kind: "landmark",
678
+ key: landmark,
679
+ line,
680
+ ...headerFooter ? { topLevel: ctx.elementDepth === 0 } : {}
681
+ });
682
+ }
683
+ for (const attr of node.attributes) {
684
+ if (attr.type !== "Attribute") continue;
685
+ if (attr.name === "id") {
686
+ const v = attrValueOf3(attr);
687
+ if (v === "dynamic") emit(ctx, { kind: "id", key: "", line });
688
+ else if (v === "static") emit(ctx, { kind: "id", key: attrTextOf3(attr), line });
689
+ } else if (attr.name === "href") {
690
+ const href = attrTextOf3(attr);
691
+ if (href?.startsWith("#") && href.length > 1) {
692
+ emit(ctx, { kind: "idref", key: decodeFragmentId(href.slice(1)), line, attr: "href" });
693
+ }
694
+ } else if (IDREF_ATTR_SET.has(attr.name)) {
695
+ for (const token of splitTokens(attrTextOf3(attr))) {
696
+ emit(ctx, { kind: "idref", key: token, line, attr: attr.name });
697
+ }
698
+ }
699
+ }
700
+ const literalTag = node.type === "SvelteElement" ? node.tag.type === "Literal" && typeof node.tag.value === "string" ? node.tag.value : void 0 : node.name;
701
+ if (literalTag === "template") return;
702
+ walk(node.fragment, {
703
+ ...ctx,
704
+ elementDepth: ctx.elementDepth + 1,
705
+ landmarks: landmark ? [...ctx.landmarks, landmark] : ctx.landmarks
706
+ });
707
+ };
708
+ walk(fragment, { path: [], repeatable: false, landmarks: [], elementDepth: 0 });
709
+ return { nodes, ...slotInLandmark ? { slotInLandmark } : {}, unknowableContent };
710
+ }
711
+ function isChildrenRender(node) {
712
+ const call = node.expression.type === "ChainExpression" ? node.expression.expression : node.expression;
713
+ return call.callee.type === "Identifier" && call.callee.name === "children";
714
+ }
558
715
  function parseFile(source, filename) {
559
716
  const ast = parse(source, { modern: true, filename });
560
717
  const heads = [];
@@ -570,7 +727,8 @@ function parseFile(source, filename) {
570
727
  components,
571
728
  imports: collectImports(ast),
572
729
  images,
573
- headings
730
+ headings,
731
+ a11y: collectA11y(ast.fragment, source)
574
732
  };
575
733
  }
576
734
 
@@ -728,8 +886,64 @@ function chainFiles(pageRel, layouts) {
728
886
  }
729
887
  return [...chain.map((rel) => ({ rel, isPage: false })), { rel: pageRel, isPage: true }];
730
888
  }
731
- async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
889
+ function groupSpan(nodes) {
890
+ let max = -1;
891
+ for (const node of nodes) {
892
+ for (const step of node.path) if (step.group > max) max = step.group;
893
+ }
894
+ return max + 1;
895
+ }
896
+ function offsetPath(path, base) {
897
+ return base === 0 ? path : path.map((step) => ({ group: step.group + base, branch: step.branch }));
898
+ }
899
+ async function composeA11y(ctx, fileRel, parsed, depth, visited, chain) {
900
+ const { rt, cwd, state } = ctx;
901
+ if (parsed.a11y.unknowableContent) state.fullyResolved = false;
902
+ const base = state.nextGroup;
903
+ state.nextGroup += groupSpan(parsed.a11y.nodes);
904
+ const composed = [];
905
+ for (const node of parsed.a11y.nodes) {
906
+ const path = offsetPath(node.path, base);
907
+ if (node.kind !== "component") {
908
+ composed.push({ ...node, path, file: fileRel, chain });
909
+ continue;
910
+ }
911
+ const info = ctx.config.metaComponents.includes(node.key) ? void 0 : parsed.imports.get(node.key);
912
+ const childRel = info ? resolveComponentPath(info.source, fileRel, ctx.aliases) : void 0;
913
+ if (!childRel || depth <= 0 || visited.has(childRel) || !await rt.exists(rt.join(cwd, childRel))) {
914
+ state.fullyResolved = false;
915
+ continue;
916
+ }
917
+ const childParsed = await readAndParse(rt, cwd, childRel, ctx.cache);
918
+ const child = await composeA11y(ctx, childRel, childParsed, depth - 1, new Set(visited).add(childRel), false);
919
+ for (const inner of child) {
920
+ composed.push({ ...inner, path: [...path, ...inner.path], repeatable: node.repeatable || inner.repeatable });
921
+ }
922
+ }
923
+ return composed;
924
+ }
925
+ function countsAsLandmark(node) {
926
+ return node.topLevel === void 0 || node.chain && node.topLevel === true;
927
+ }
928
+ function representativeOrder(chainOrder) {
929
+ return (a, b) => {
930
+ const rankA = a.chain ? chainOrder.get(a.file) ?? 0 : chainOrder.size;
931
+ const rankB = b.chain ? chainOrder.get(b.file) ?? 0 : chainOrder.size;
932
+ if (rankA !== rankB) return rankA - rankB;
933
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
934
+ return a.line - b.line;
935
+ };
936
+ }
937
+ function representatives(nodes, chainOrder) {
938
+ const folded = foldOccurrences(nodes);
939
+ const order = representativeOrder(chainOrder);
940
+ return Object.fromEntries(
941
+ [...folded].map(([key, list]) => [key, list.sort(order).map(({ file, line }) => ({ file, line }))])
942
+ );
943
+ }
944
+ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases, appHtmlIds) {
732
945
  const files = chainFiles(pageRel, layouts);
946
+ const chainOrder = new Map(files.map((f, i) => [f.rel, i]));
733
947
  const composed = /* @__PURE__ */ new Map();
734
948
  const jsonldTags = [];
735
949
  let broadOwn = false;
@@ -737,8 +951,20 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
737
951
  const images = [];
738
952
  const headings = [];
739
953
  const componentHeadings = [];
954
+ const a11yCtx = { rt, cwd, config, cache, aliases, state: { nextGroup: 0, fullyResolved: true } };
955
+ const a11yNodes = [];
956
+ const nestedLandmarks = [];
957
+ let slotLandmark;
740
958
  for (const { rel, isPage } of files) {
741
959
  const parsed = await readAndParse(rt, cwd, rel, cache);
960
+ const contributed = await composeA11y(a11yCtx, rel, parsed, MAX_DEPTH, /* @__PURE__ */ new Set([rel]), true);
961
+ for (const node of contributed) {
962
+ if (!node.chain || node.kind !== "landmark" || !countsAsLandmark(node) || node.repeatable) continue;
963
+ const within = node.inLandmark ?? slotLandmark;
964
+ if (within) nestedLandmarks.push({ kind: node.key, within, file: node.file, line: node.line });
965
+ }
966
+ slotLandmark = parsed.a11y.slotInLandmark ?? slotLandmark;
967
+ a11yNodes.push(...contributed);
742
968
  for (const img of parsed.images) {
743
969
  images.push({ ...img, file: rel });
744
970
  }
@@ -764,20 +990,40 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache, aliases) {
764
990
  if (!composed.has(key)) composed.set(key, { ...tag, presence });
765
991
  }
766
992
  }
993
+ const idNodes = a11yNodes.filter((n) => n.kind === "id");
994
+ if (idNodes.some((n) => n.key === "")) a11yCtx.state.fullyResolved = false;
995
+ const literalIds = idNodes.filter((n) => n.key !== "");
767
996
  const route = deriveRoute(pageRel);
768
997
  return {
769
998
  head: { route, source: "static", tags: [...composed.values(), ...jsonldTags], file: pageRel },
770
999
  images: { route, images },
771
- headings: { route, headings, componentHeadings }
1000
+ headings: { route, headings, componentHeadings },
1001
+ a11y: {
1002
+ route,
1003
+ landmarks: representatives(
1004
+ a11yNodes.filter((n) => n.kind === "landmark" && countsAsLandmark(n)),
1005
+ chainOrder
1006
+ ),
1007
+ nestedLandmarks,
1008
+ ids: representatives(literalIds, chainOrder),
1009
+ // `href="#top"` scrolls to the document top with no element of that id, so it is
1010
+ // never a missing reference (HTML's "top of the document" fragment).
1011
+ idRefs: a11yNodes.filter((n) => n.kind === "idref" && !(n.attr === "href" && isTopFragment(n.key))).map((n) => ({ id: n.key, attr: n.attr ?? "", file: n.file, line: n.line })),
1012
+ idCandidates: [.../* @__PURE__ */ new Set([...literalIds.map((n) => n.key), ...appHtmlIds ?? []])],
1013
+ fullyResolved: a11yCtx.state.fullyResolved
1014
+ }
772
1015
  };
773
1016
  }
774
- async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases) {
1017
+ async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map(), aliases, appHtmlIds) {
775
1018
  const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
776
- const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache, aliases)));
1019
+ const facts = await Promise.all(
1020
+ pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache, aliases, appHtmlIds))
1021
+ );
777
1022
  return {
778
1023
  heads: facts.map((f) => f.head),
779
1024
  images: facts.map((f) => f.images),
780
- headings: facts.map((f) => f.headings)
1025
+ headings: facts.map((f) => f.headings),
1026
+ a11y: facts.map((f) => f.a11y)
781
1027
  };
782
1028
  }
783
1029
 
@@ -794,7 +1040,7 @@ async function collectAll(rt, cwd, config, opts = {}) {
794
1040
  const matches = routeMatcher(opts.route);
795
1041
  const project = await collectProjectFacts(rt, cwd);
796
1042
  const [collected, components, kitModules, sourceFiles] = await Promise.all([
797
- collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases),
1043
+ collectRoutes(rt, cwd, config, opts.parseCache, project.kitAliases, project.appHtmlIds),
798
1044
  // Component (Correctness) facts are file-scoped with no route attribution yet, so a
799
1045
  // route-filtered run skips them rather than reporting unrelated components (#68 review);
800
1046
  // kitModules is skipped for the same reason.
@@ -809,7 +1055,8 @@ async function collectAll(rt, cwd, config, opts = {}) {
809
1055
  const heads = collected.heads.filter((h) => matches(h.route));
810
1056
  const images = collected.images.filter((i) => matches(i.route));
811
1057
  const headings = collected.headings.filter((h) => matches(h.route));
812
- return { heads, images, headings, project, components, kitModules, sourceFiles };
1058
+ const a11y = collected.a11y.filter((a) => matches(a.route));
1059
+ return { heads, images, headings, a11y, project, components, kitModules, sourceFiles };
813
1060
  }
814
1061
 
815
1062
  // src/changed-files.ts
@@ -1002,15 +1249,16 @@ function applySuppressions(results, entries, config, allResults) {
1002
1249
  }
1003
1250
 
1004
1251
  // src/color.ts
1252
+ import { styleText } from "util";
1005
1253
  import { noColorPalette } from "@svelte-vitals/core";
1006
- var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
1254
+ var wrap = (format) => (s) => styleText(format, s, { validateStream: false });
1007
1255
  var ansiPalette = {
1008
- bold: wrap(1, 22),
1009
- dim: wrap(2, 22),
1010
- red: wrap(31, 39),
1011
- yellow: wrap(33, 39),
1012
- green: wrap(32, 39),
1013
- cyan: wrap(36, 39)
1256
+ bold: wrap("bold"),
1257
+ dim: wrap("dim"),
1258
+ red: wrap("red"),
1259
+ yellow: wrap("yellow"),
1260
+ green: wrap("green"),
1261
+ cyan: wrap("cyan")
1014
1262
  };
1015
1263
  function colorEnabled(opts) {
1016
1264
  if (opts.noColorFlag) return false;
@@ -1021,28 +1269,6 @@ function colorEnabled(opts) {
1021
1269
  }
1022
1270
  var paletteFor = (enabled) => enabled ? ansiPalette : noColorPalette;
1023
1271
 
1024
- // src/spinner.ts
1025
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1026
- function startSpinner(text, opts) {
1027
- const stream = opts.stream ?? process.stderr;
1028
- if (!opts.enabled) return { stop() {
1029
- } };
1030
- let i = 0;
1031
- const tick = () => {
1032
- stream.write(`\r${FRAMES[i % FRAMES.length]} ${text}`);
1033
- i++;
1034
- };
1035
- tick();
1036
- const timer = setInterval(tick, 80);
1037
- if (typeof timer.unref === "function") timer.unref();
1038
- return {
1039
- stop() {
1040
- clearInterval(timer);
1041
- stream.write("\r\x1B[K");
1042
- }
1043
- };
1044
- }
1045
-
1046
1272
  // src/mascot.ts
1047
1273
  import { createLogUpdate } from "log-update";
1048
1274
  function mascotStateFor(score) {
@@ -1119,24 +1345,28 @@ function renderConfettiFrame(offset, mascotBlock) {
1119
1345
  return [confettiRow(offset), mascotBlock, confettiRow(offset + 2)].join("\n");
1120
1346
  }
1121
1347
  var IDLE_TICK_MS = 160;
1348
+ var PLAIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1349
+ var PLAIN_TICK_MS = 80;
1122
1350
  function startMascotSpinner(text, opts) {
1123
1351
  if (!opts.enabled) return { stop() {
1124
1352
  } };
1125
1353
  const stream = opts.stream ?? process.stderr;
1354
+ const mascot = opts.mascot ?? true;
1126
1355
  const render = createLogUpdate(stream);
1127
1356
  let i = 0;
1128
1357
  const tick = () => {
1129
- render(`${renderMascotIdleFrame(i)}
1130
- ${text}`);
1358
+ render(mascot ? `${renderMascotIdleFrame(i)}
1359
+ ${text}` : `${PLAIN_FRAMES[i % PLAIN_FRAMES.length]} ${text}`);
1131
1360
  i++;
1132
1361
  };
1133
1362
  tick();
1134
- const timer = setInterval(tick, IDLE_TICK_MS);
1363
+ const timer = setInterval(tick, mascot ? IDLE_TICK_MS : PLAIN_TICK_MS);
1135
1364
  if (typeof timer.unref === "function") timer.unref();
1136
1365
  return {
1137
1366
  stop() {
1138
1367
  clearInterval(timer);
1139
1368
  render.clear();
1369
+ render.done();
1140
1370
  }
1141
1371
  };
1142
1372
  }
@@ -1307,7 +1537,7 @@ function skippedFileWarnings(facts) {
1307
1537
  ];
1308
1538
  }
1309
1539
  function failedRuleWarnings(failedRules) {
1310
- return failedRules.map((f) => `rule ${f.id} failed and was skipped: ${f.message.split("\n")[0]}`);
1540
+ return failedRules.map(formatFailedRuleWarning);
1311
1541
  }
1312
1542
  async function analyzeProject(opts = {}) {
1313
1543
  const cwd = opts.cwd ?? process.cwd();
@@ -1334,10 +1564,15 @@ async function analyzeProject(opts = {}) {
1334
1564
  ...await checkVersionFloor(rt, cwd),
1335
1565
  ...overridesOffWarnings(opts.allowRules, config.overrides)
1336
1566
  ];
1337
- const { heads, images, headings, project, components, kitModules, sourceFiles } = await collectAll(rt, cwd, config, {
1338
- route: opts.route,
1339
- parseCache: opts.parseCache
1340
- });
1567
+ const { heads, images, headings, a11y, project, components, kitModules, sourceFiles } = await collectAll(
1568
+ rt,
1569
+ cwd,
1570
+ config,
1571
+ {
1572
+ route: opts.route,
1573
+ parseCache: opts.parseCache
1574
+ }
1575
+ );
1341
1576
  const selected = selectRules(allRules2, config);
1342
1577
  const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
1343
1578
  const {
@@ -1348,6 +1583,7 @@ async function analyzeProject(opts = {}) {
1348
1583
  heads,
1349
1584
  images,
1350
1585
  headings,
1586
+ a11y,
1351
1587
  components,
1352
1588
  project,
1353
1589
  config,
@@ -1355,22 +1591,22 @@ async function analyzeProject(opts = {}) {
1355
1591
  sourceFiles
1356
1592
  });
1357
1593
  const results = applyOverrides(applyRuleSeverities(rawResults, config), config);
1358
- const scoringConfig = withFailedRulesOff(
1359
- config,
1360
- failedRules.map((f) => f.id)
1361
- );
1594
+ const failedRuleIds = failedRules.map((f) => f.id);
1595
+ const scoringConfig = withFailedRulesOff(config, failedRuleIds);
1362
1596
  return {
1363
1597
  results,
1364
1598
  config: scoringConfig,
1365
1599
  version: readPackageVersion(),
1366
1600
  ruleIds: rules.map((r) => r.id),
1367
1601
  examined,
1602
+ failedRuleIds,
1368
1603
  warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules]), ...failedRuleWarnings(failedRules)],
1369
1604
  loadedConfig: loaded
1370
1605
  };
1371
1606
  }
1372
1607
  async function applyScope(results, opts) {
1373
- const errorLog = opts.errorLog ?? ((line) => console.error(line));
1608
+ const rawErrorLog = opts.errorLog ?? ((line) => console.error(line));
1609
+ const errorLog = (line) => rawErrorLog(terminalSafe(line));
1374
1610
  let scoped = results;
1375
1611
  if (opts.staged || opts.diffBase !== void 0) {
1376
1612
  const changed = opts.staged ? getChangedFiles(opts.cwd, { staged: true }) : getChangedFiles(opts.cwd, { base: opts.diffBase });
@@ -1433,7 +1669,8 @@ function runAnalyzeOptions(opts) {
1433
1669
  }
1434
1670
  async function run(opts = {}) {
1435
1671
  const log = opts.log ?? ((line) => console.log(line));
1436
- const errorLog = opts.errorLog ?? ((line) => console.error(line));
1672
+ const rawErrorLog = opts.errorLog ?? ((line) => console.error(line));
1673
+ const errorLog = (line) => rawErrorLog(terminalSafe(line));
1437
1674
  if (opts.minHealth != null && (!Number.isFinite(opts.minHealth) || opts.minHealth < 0 || opts.minHealth > 100)) {
1438
1675
  errorLog(`svelte-vitals: invalid minHealth '${opts.minHealth}'; expected a number 0-100.`);
1439
1676
  return 2;
@@ -1452,7 +1689,11 @@ async function run(opts = {}) {
1452
1689
  if (useMascotSpinner && bubbleFitsWidth(stderrStream.columns)) {
1453
1690
  await playMascotGreeting({ enabled: true, stream: stderrStream, holdMs: opts.animationFrameDelayMs });
1454
1691
  }
1455
- const spinner = useMascotSpinner ? startMascotSpinner("Analyzing\u2026", { enabled: true, stream: stderrStream }) : startSpinner("Analyzing\u2026", { enabled: spinnerBaseEnabled, stream: stderrStream });
1692
+ const spinner = startMascotSpinner("Analyzing\u2026", {
1693
+ enabled: spinnerBaseEnabled,
1694
+ stream: stderrStream,
1695
+ mascot: useMascotSpinner
1696
+ });
1456
1697
  let cwd = opts.cwd ?? process.cwd();
1457
1698
  let analysis;
1458
1699
  try {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toList
3
- } from "./chunk-MUIOPL5F.js";
3
+ } from "./chunk-43FGGQQZ.js";
4
4
  import {
5
5
  CONFIG_FILENAMES,
6
6
  discoverApps,
@@ -17,7 +17,7 @@ import {
17
17
  WORKFLOW_PATH,
18
18
  buildWorkflowYaml,
19
19
  planWorkflowWrite
20
- } from "./chunk-F5P3REI7.js";
20
+ } from "./chunk-CTMKTFST.js";
21
21
  import {
22
22
  clackPrompts,
23
23
  realIO
@@ -110,13 +110,14 @@ function isKind(id, kind) {
110
110
 
111
111
  // src/install/skill-content.ts
112
112
  import { allRules, docsUrlFor } from "@svelte-vitals/core";
113
- var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
113
+ var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture", "a11y"];
114
114
  var CATEGORY_LABELS = {
115
115
  seo: "SEO",
116
116
  performance: "Performance",
117
117
  correctness: "Correctness",
118
118
  security: "Security",
119
- architecture: "Architecture"
119
+ architecture: "Architecture",
120
+ a11y: "Accessibility"
120
121
  };
121
122
  function oneLine(text) {
122
123
  return text.replace(/\r?\n+/g, " ").trim();
@@ -151,7 +152,7 @@ function sharedBody(version) {
151
152
 
152
153
  ## When to use
153
154
 
154
- Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, and architecture patterns.
155
+ Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, architecture, and accessibility patterns.
155
156
 
156
157
  ## Playbook
157
158
 
@@ -168,7 +169,7 @@ ${ruleDigest()}
168
169
  function buildSkillMarkdown(version) {
169
170
  const frontmatter = `---
170
171
  name: svelte-vitals
171
- description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture) and how to run the scanner.
172
+ description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture, accessibility) and how to run the scanner.
172
173
  ---`;
173
174
  return `${frontmatter}
174
175
 
@@ -176,7 +177,7 @@ ${sharedBody(version)}`;
176
177
  }
177
178
  function buildCursorRules(version) {
178
179
  const frontmatter = `---
179
- description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture)
180
+ description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture, accessibility)
180
181
  globs: ["**/*.svelte", "src/routes/**"]
181
182
  alwaysApply: false
182
183
  ---`;
@@ -281,7 +282,8 @@ Get the machine map before applying judgment:
281
282
  \`<title>\`/meta (SEO), image-heavy routes (Performance), forms and
282
283
  \`{@html}\` usage (Security), large or unkeyed list-rendering routes
283
284
  (Correctness), route/component files that have grown large or deeply
284
- nested (Architecture).
285
+ nested (Architecture), interactive controls and forms with unclear
286
+ labeling or ARIA usage (Accessibility).
285
287
  - **Leverage map** (the judgment the scan lacks): which routes are
286
288
  high-traffic/public/indexed (a marketing page, a product listing) versus
287
289
  low-traffic or gated (an internal admin tool, a rarely visited settings
@@ -290,10 +292,10 @@ Get the machine map before applying judgment:
290
292
 
291
293
  ### Phase 2 \u2014 Audit (parallel)
292
294
 
293
- Audit against svelte-vitals' five categories: SEO, Performance, Correctness,
294
- Security, Architecture (see the rule catalog below for the full "hunt for"
295
- list per category, generated from svelte-vitals' own rule metadata \u2014 always
296
- in sync, never invented).
295
+ Audit against svelte-vitals' six categories: SEO, Performance, Correctness,
296
+ Security, Architecture, Accessibility (see the rule catalog below for the
297
+ full "hunt for" list per category, generated from svelte-vitals' own rule
298
+ metadata \u2014 always in sync, never invented).
297
299
 
298
300
  For anything beyond a small project, fan out read-only subagents \u2014 one per
299
301
  category. Each subagent prompt must include: the recon facts (stack,
@@ -311,8 +313,8 @@ Depth follows effort level (default \`standard\`):
311
313
  | Effort | Coverage | Subagents | Findings |
312
314
  | ---------- | -------------------------------------- | --------- | ----------------------------- |
313
315
  | \`quick\` | Highest-traffic/public routes only | 0\u20131 | ~5, HIGH severity only |
314
- | \`standard\` | All routes and components | \u22645 | Full table |
315
- | \`deep\` | Whole project incl. rarely-hit routes | 5 | Full table + LOW polish items |
316
+ | \`standard\` | All routes and components | \u22646 | Full table |
317
+ | \`deep\` | Whole project incl. rarely-hit routes | 6 | Full table + LOW polish items |
316
318
 
317
319
  ### Phase 3 \u2014 Vet, prioritize, confirm
318
320
 
@@ -408,6 +410,11 @@ category the rule catalog above can't cover:
408
410
  complex, well-organized page (leave it \u2014 don't split just to satisfy a
409
411
  metric). Look for duplicated \`<svelte:head>\` boilerplate that a shared
410
412
  layout or meta component would remove.
413
+ - **Accessibility** \u2014 svelte-vitals checks static markup (ARIA validity,
414
+ landmarks, ids, labels); it can't drive a keyboard or screen reader. Hunt
415
+ for illogical tab order, missing visible focus styles, color contrast
416
+ below WCAG thresholds, and modal/menu components that don't trap or
417
+ restore focus \u2014 all invisible to a static scan.
411
418
 
412
419
  ## Plan template
413
420
 
@@ -421,7 +428,7 @@ target state.
421
428
  - **Status**: TODO
422
429
  - **Commit**: <output of \`git rev-parse --short HEAD\` when written>
423
430
  - **Severity**: HIGH | MEDIUM | LOW
424
- - **Category**: SEO | Performance | Correctness | Security | Architecture
431
+ - **Category**: SEO | Performance | Correctness | Security | Architecture | Accessibility
425
432
  - **Rule**: <RULEID> | Beyond the scan
426
433
  - **Estimated scope**: <n files, rough size>
427
434
 
@@ -491,7 +498,7 @@ adapted to this file \u2014 never approximated from memory.
491
498
  | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
492
499
  | bare | Full workflow: recon \u2192 audit all categories \u2192 vet \u2192 confirm \u2192 plans |
493
500
  | \`quick\` / \`deep\` | Adjust audit effort (see table); composes with a category focus |
494
- | a category focus (\`seo\`, \`performance\`, \`correctness\`, \`security\`, \`architecture\`) | Recon + audit that category only |
501
+ | a category focus (\`seo\`, \`performance\`, \`correctness\`, \`security\`, \`architecture\`, \`accessibility\`) | Recon + audit that category only |
495
502
  | \`plan <description>\` | Skip the audit; recon just enough to specify, then write a single plan for the described improvement |
496
503
  | \`execute <plan>\` | Dispatch an executor subagent to implement the plan in an isolated worktree, then review its diff against svelte-vitals (\`--diff --reporter agent\`) and render a verdict |
497
504
  | \`reconcile\` | Re-check \`plans/\` against the current code: mark done plans DONE, refresh stale \`file:line\`/route references, retire fixed findings |
@@ -1143,7 +1150,7 @@ var INSTALL_ARGS = {
1143
1150
  help: { type: "boolean", short: "h", description: "Show this help" }
1144
1151
  };
1145
1152
  async function buildInstallHelpText(installCommand2, locale) {
1146
- const ja = locale === "ja" ? await import("./ja-BCE5T3H2.js") : void 0;
1153
+ const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
1147
1154
  const optionsSection = stripAutoVersionLine(
1148
1155
  await localizedOptionsSection(
1149
1156
  installCommand2,
@@ -16,7 +16,7 @@ var JA_ARG_DESCRIPTIONS = {
16
16
  "min-health": "\u7D44\u307F\u5408\u308F\u305B\u305F Health \u30B9\u30B3\u30A2\u304C\u3053\u306E\u5024\u3092\u4E0B\u56DE\u308C\u3070\u5931\u6557\uFF08\u7D42\u4E86\u30B3\u30FC\u30C9 1\u30010\u301C100\uFF09",
17
17
  rules: "\u6709\u52B9\u306B\u3059\u308B\u30EB\u30FC\u30EB ID\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3001\u4ED6\u306F\u3059\u3079\u3066\u7121\u52B9\uFF09",
18
18
  ignore: "\u7121\u52B9\u306B\u3059\u308B\u30EB\u30FC\u30EB ID\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09",
19
- category: "\u89E3\u6790\u5BFE\u8C61\u30AB\u30C6\u30B4\u30EA\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09: seo | performance | correctness | security | architecture",
19
+ category: "\u89E3\u6790\u5BFE\u8C61\u30AB\u30C6\u30B4\u30EA\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09: seo | performance | correctness | security | architecture | a11y",
20
20
  weights: "\u30AB\u30C6\u30B4\u30EA\u3054\u3068\u306E Health \u91CD\u307F\u4E0A\u66F8\u304D\u3002\u4F8B: seo=2,performance=1\uFF08\u6307\u5B9A\u306E\u306A\u3044\u30AB\u30C6\u30B4\u30EA\u306F\u30C7\u30D5\u30A9\u30EB\u30C8\u5024 1\uFF09",
21
21
  score: "\u7D44\u307F\u5408\u308F\u305B\u305F Health \u30B9\u30B3\u30A2\u306E\u307F\u3092\u51FA\u529B\uFF08--min-health \u3068\u4F75\u7528\u3057\u3066\u30B2\u30FC\u30C8\u306B\u5229\u7528\u53EF\u80FD\uFF09",
22
22
  noColor: "\u30B3\u30F3\u30BD\u30FC\u30EB\u51FA\u529B\u306E ANSI \u30AB\u30E9\u30FC\u3092\u7121\u52B9\u5316",
@@ -50,7 +50,7 @@ var JA_ARG_DESCRIPTIONS = {
50
50
  }
51
51
  };
52
52
  function rootHelpJa(optionsSection) {
53
- return `svelte-vitals \u2014 \u6C7A\u5B9A\u8AD6\u7684\u306A SvelteKit \u30B3\u30FC\u30C9\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AB\u30FC\uFF08SEO\u30FB\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u30FB\u6B63\u78BA\u6027\u30FB\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u30FB\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\uFF09
53
+ return `svelte-vitals \u2014 \u6C7A\u5B9A\u8AD6\u7684\u306A SvelteKit \u30B3\u30FC\u30C9\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AB\u30FC\uFF08SEO\u30FB\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u30FB\u6B63\u78BA\u6027\u30FB\u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u30FB\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30FB\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\uFF09
54
54
 
55
55
  \u4F7F\u7528\u65B9\u6CD5:
56
56
  svelte-vitals [path] [options]
@@ -1,7 +1,7 @@
1
1
  // src/ci/workflow.ts
2
2
  var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
3
- var CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
4
- var CHECKOUT_VERSION = "v7.0.0";
3
+ var CHECKOUT_SHA = "3d3c42e5aac5ba805825da76410c181273ba90b1";
4
+ var CHECKOUT_VERSION = "v7.0.1";
5
5
  function planWorkflowWrite(existing, force) {
6
6
  if (existing === void 0) return { status: "created" };
7
7
  if (!force) return { status: "exists" };
@@ -28,6 +28,7 @@ function buildWorkflowYaml(opts) {
28
28
  ` - uses: actions/checkout@${CHECKOUT_SHA} # ${CHECKOUT_VERSION}`,
29
29
  " with:",
30
30
  " fetch-depth: 0",
31
+ " persist-credentials: false",
31
32
  ` - uses: oekazuma/svelte-vitals-action@${actionSha} # v${actionVersion}`,
32
33
  " with:",
33
34
  " diff: origin/${{ github.base_ref }}",
@@ -4,7 +4,7 @@ import {
4
4
  WORKFLOW_PATH,
5
5
  buildWorkflowYaml,
6
6
  planWorkflowWrite
7
- } from "./chunk-F5P3REI7.js";
7
+ } from "./chunk-CTMKTFST.js";
8
8
  import {
9
9
  realIO
10
10
  } from "./chunk-GE7TKVTX.js";
@@ -86,7 +86,7 @@ var CI_UPGRADE_ARGS = { "dry-run": CI_ARGS["dry-run"], help: CI_ARGS.help };
86
86
  var KNOWN_LONG_FLAGS = /* @__PURE__ */ new Set(["force", "dry-run", "help"]);
87
87
  var KNOWN_SHORT_FLAGS = /* @__PURE__ */ new Set(["h"]);
88
88
  async function buildCiHelpText(ciArgsCommand, locale) {
89
- const ja = locale === "ja" ? await import("./ja-BCE5T3H2.js") : void 0;
89
+ const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
90
90
  const optionsSection = stripAutoVersionLine(
91
91
  await localizedOptionsSection(ciArgsCommand, "svelte-vitals ci", locale, ja?.JA_ARG_DESCRIPTIONS.ci ?? {})
92
92
  );
@@ -17,10 +17,7 @@ import {
17
17
  import { cli } from "gunshi/bone";
18
18
  import { define } from "gunshi/definition";
19
19
  import "gunshi/generator";
20
- import { explainRule as explainRule2, allRules as allRules2 } from "@svelte-vitals/core";
21
-
22
- // src/explain.ts
23
- import { allRules, CATEGORIES } from "@svelte-vitals/core";
20
+ import { explainRule, allRules, CATEGORIES } from "@svelte-vitals/core";
24
21
  function describeOptions(id, options) {
25
22
  const MERGE = {
26
23
  integer: "replaces the default",
@@ -56,8 +53,6 @@ function renderRuleList() {
56
53
  "\n\n"
57
54
  );
58
55
  }
59
-
60
- // src/gunshi/explain.ts
61
56
  var BOOLEAN_FLAGS = ["json", "list", "help"];
62
57
  var KNOWN_LONG_FLAGS = new Set(BOOLEAN_FLAGS);
63
58
  var KNOWN_SHORT_FLAGS = /* @__PURE__ */ new Set(["h"]);
@@ -67,7 +62,7 @@ var EXPLAIN_ARGS = {
67
62
  help: { type: "boolean", short: "h", description: "Show this help" }
68
63
  };
69
64
  async function buildExplainHelpText(explainCommand, locale) {
70
- const ja = locale === "ja" ? await import("./ja-BCE5T3H2.js") : void 0;
65
+ const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
71
66
  const optionsSection = stripAutoVersionLine(
72
67
  await localizedOptionsSection(
73
68
  explainCommand,
@@ -113,7 +108,7 @@ async function runExplainCliGunshi(args, io = consoleIO, locale = "en") {
113
108
  }
114
109
  io.log(
115
110
  ctx.values.json ? JSON.stringify(
116
- allRules2.map((r) => ({ id: r.id, category: r.category, severity: r.severity, title: r.title })),
111
+ allRules.map((r) => ({ id: r.id, category: r.category, severity: r.severity, title: r.title })),
117
112
  null,
118
113
  2
119
114
  ) : renderRuleList()
@@ -130,7 +125,7 @@ async function runExplainCliGunshi(args, io = consoleIO, locale = "en") {
130
125
  exitCode = 2;
131
126
  return;
132
127
  }
133
- const info = explainRule2(id);
128
+ const info = explainRule(id);
134
129
  if (!info) {
135
130
  io.errorLog(`svelte-vitals: unknown rule id '${id}'.`);
136
131
  const hint = suggestClosest(id, knownRuleIds());
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-VNVRHWO3.js";
3
+ } from "./chunk-5UQ6P2M2.js";
4
4
  import {
5
5
  VALUE_FLAGS,
6
6
  parseRunArgs,
7
7
  resolveArgs
8
- } from "./chunk-MUIOPL5F.js";
8
+ } from "./chunk-43FGGQQZ.js";
9
9
  import {
10
10
  readCoreVersion,
11
11
  readPackageVersion
@@ -90,7 +90,7 @@ var ROOT_ARGS = {
90
90
  ignore: { type: "string", description: "Comma-separated rule ids to disable" },
91
91
  category: {
92
92
  type: "string",
93
- description: "Comma-separated categories to analyze: seo | performance | correctness | security | architecture"
93
+ description: "Comma-separated categories to analyze: seo | performance | correctness | security | architecture | a11y"
94
94
  },
95
95
  weights: {
96
96
  type: "string",
@@ -127,7 +127,7 @@ function neutralizeBareDiffAndBaseline(argv) {
127
127
  }
128
128
  async function buildHelpText(rootCommand, locale) {
129
129
  if (locale === "ja") {
130
- const { JA_ARG_DESCRIPTIONS, rootHelpJa } = await import("./ja-BCE5T3H2.js");
130
+ const { JA_ARG_DESCRIPTIONS, rootHelpJa } = await import("./ja-GAQSBSXF.js");
131
131
  const optionsSection2 = await localizedOptionsSection(
132
132
  rootCommand,
133
133
  "svelte-vitals",
@@ -137,7 +137,7 @@ async function buildHelpText(rootCommand, locale) {
137
137
  return rootHelpJa(optionsSection2);
138
138
  }
139
139
  const optionsSection = await localizedOptionsSection(rootCommand, "svelte-vitals", locale, {});
140
- return `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
140
+ return `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture \xB7 accessibility)
141
141
 
142
142
  Usage:
143
143
  svelte-vitals [path] [options]
@@ -31,7 +31,7 @@ var EMBEDDED_DOCS = [
31
31
  name: "config",
32
32
  title: "The config file",
33
33
  description: "Where svelte-vitals.config lives, every top-level option, how to disable or re-grade a rule, and how to scope rules to routes or files.",
34
- body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules` and `--ignore`\nare selection, not configuration: `--rules` narrows the run to the ids it names and overrides a\nconfig-file `off` for them, but keeps their declared severity and options; `--ignore` adds `off`\nentries for the ids it names, layered on top of whatever `rules` resolved to, and beats `--rules`\nwhen both name the same rule.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
34
+ body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture' | 'a11y'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules` and `--ignore`\nare selection, not configuration: `--rules` narrows the run to the ids it names and overrides a\nconfig-file `off` for them, but keeps their declared severity and options; `--ignore` adds `off`\nentries for the ids it names, layered on top of whatever `rules` resolved to, and beats `--rules`\nwhen both name the same rule.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
35
35
  },
36
36
  {
37
37
  name: "monorepo",
@@ -53,7 +53,7 @@ var EMBEDDED_DOCS = [
53
53
  }
54
54
  ];
55
55
 
56
- // src/docs/cli.ts
56
+ // src/gunshi/docs.ts
57
57
  var DOCS_HELP = `svelte-vitals docs \u2014 read the bundled guides without leaving the terminal
58
58
 
59
59
  Usage:
@@ -83,8 +83,6 @@ function renderList() {
83
83
  "Rule-level detail is a separate command: `svelte-vitals explain --list`."
84
84
  ].join("\n");
85
85
  }
86
-
87
- // src/gunshi/docs.ts
88
86
  var BOOLEAN_FLAGS = ["json", "help"];
89
87
  var HELP_ARG = { help: { type: "boolean", short: "h", description: "Show this help" } };
90
88
  var JSON_ARG = { json: { type: "boolean", description: "Machine-readable output (list only)" } };
@@ -98,7 +96,7 @@ var DOCS_SHOW_ARGS = {
98
96
  ...HELP_ARG
99
97
  };
100
98
  async function buildDocsHelpText(rootCommand, locale) {
101
- const ja = locale === "ja" ? await import("./ja-BCE5T3H2.js") : void 0;
99
+ const ja = locale === "ja" ? await import("./ja-GAQSBSXF.js") : void 0;
102
100
  const optionsSection = stripAutoVersionLine(
103
101
  await localizedOptionsSection(rootCommand, "svelte-vitals docs", locale, ja?.JA_ARG_DESCRIPTIONS.docs ?? {})
104
102
  );
@@ -2,8 +2,8 @@ import {
2
2
  CI_ARGS,
3
3
  CI_UPGRADE_ARGS,
4
4
  runCiCliGunshi
5
- } from "./chunk-ACQ5HB32.js";
6
- import "./chunk-F5P3REI7.js";
5
+ } from "./chunk-ICEJJDDA.js";
6
+ import "./chunk-CTMKTFST.js";
7
7
  import "./chunk-GE7TKVTX.js";
8
8
  import "./chunk-NMVBVKLX.js";
9
9
  export {
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  ROOT_ARGS
3
- } from "./chunk-P4YNVJUO.js";
4
- import "./chunk-VNVRHWO3.js";
3
+ } from "./chunk-MZO3HVTN.js";
4
+ import "./chunk-5UQ6P2M2.js";
5
5
  import {
6
6
  DOCS_LIST_ARGS,
7
7
  DOCS_ROOT_ARGS,
8
8
  DOCS_SHOW_ARGS
9
- } from "./chunk-MBOONBC4.js";
9
+ } from "./chunk-SLE22PPF.js";
10
10
  import {
11
11
  EXPLAIN_ARGS
12
- } from "./chunk-O2CQPUSP.js";
12
+ } from "./chunk-JFLNGYPX.js";
13
13
  import {
14
14
  INSTALL_ARGS
15
- } from "./chunk-4O5F7EUJ.js";
15
+ } from "./chunk-77MMJCFK.js";
16
16
  import {
17
17
  CATEGORIES,
18
18
  FAIL_ON_VALUES,
19
19
  TREAT_DYNAMIC_AS_VALUES
20
- } from "./chunk-MUIOPL5F.js";
20
+ } from "./chunk-43FGGQQZ.js";
21
21
  import {
22
22
  REPORTER_NAMES
23
23
  } from "./chunk-M5KM5SV7.js";
@@ -28,8 +28,8 @@ import "./chunk-TFBLQUAC.js";
28
28
  import {
29
29
  CI_ARGS,
30
30
  CI_UPGRADE_ARGS
31
- } from "./chunk-ACQ5HB32.js";
32
- import "./chunk-F5P3REI7.js";
31
+ } from "./chunk-ICEJJDDA.js";
32
+ import "./chunk-CTMKTFST.js";
33
33
  import "./chunk-GE7TKVTX.js";
34
34
  import "./chunk-NMVBVKLX.js";
35
35
 
@@ -43,7 +43,15 @@ function forCompletion(args) {
43
43
  const out = {};
44
44
  for (const [key, schema] of Object.entries(args)) {
45
45
  if (schema.hidden) continue;
46
- out[schema.type === "positional" || !schema.toKebab ? key : kebabnize(key)] = schema;
46
+ const outKey = schema.type === "positional" || !schema.toKebab ? key : kebabnize(key);
47
+ const description = schema.description?.replace(/\s*\n\s*/g, " ");
48
+ out[outKey] = description === schema.description ? schema : { ...schema, description };
49
+ }
50
+ for (const [key, schema] of Object.entries(out)) {
51
+ const stripped = key.startsWith("no-") ? key.slice(3) : void 0;
52
+ if (stripped && !(stripped in out)) {
53
+ out[stripped] = { type: "positional", required: false, description: schema.description };
54
+ }
47
55
  }
48
56
  return out;
49
57
  }
@@ -3,7 +3,7 @@ import {
3
3
  DOCS_ROOT_ARGS,
4
4
  DOCS_SHOW_ARGS,
5
5
  runDocsCliGunshi
6
- } from "./chunk-MBOONBC4.js";
6
+ } from "./chunk-SLE22PPF.js";
7
7
  import "./chunk-SLUMRYUD.js";
8
8
  import "./chunk-TFBLQUAC.js";
9
9
  import "./chunk-NMVBVKLX.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  EXPLAIN_ARGS,
3
3
  runExplainCliGunshi
4
- } from "./chunk-O2CQPUSP.js";
4
+ } from "./chunk-JFLNGYPX.js";
5
5
  import "./chunk-SLUMRYUD.js";
6
6
  import "./chunk-TFBLQUAC.js";
7
7
  import "./chunk-NMVBVKLX.js";
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  ROOT_ARGS
3
- } from "./chunk-P4YNVJUO.js";
4
- import "./chunk-VNVRHWO3.js";
3
+ } from "./chunk-MZO3HVTN.js";
4
+ import "./chunk-5UQ6P2M2.js";
5
5
  import {
6
6
  JA_ARG_DESCRIPTIONS
7
- } from "./chunk-MHRO4GU7.js";
7
+ } from "./chunk-A5DVJX5O.js";
8
8
  import {
9
9
  INSTALL_ARGS
10
- } from "./chunk-4O5F7EUJ.js";
11
- import "./chunk-MUIOPL5F.js";
10
+ } from "./chunk-77MMJCFK.js";
11
+ import "./chunk-43FGGQQZ.js";
12
12
  import "./chunk-M5KM5SV7.js";
13
13
  import "./chunk-SLUMRYUD.js";
14
14
  import "./chunk-TFBLQUAC.js";
15
- import "./chunk-F5P3REI7.js";
15
+ import "./chunk-CTMKTFST.js";
16
16
  import "./chunk-GE7TKVTX.js";
17
17
  import "./chunk-NMVBVKLX.js";
18
18
  export {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HeadTag, Config, RuleOptionsSpec, Severity, RuleSetting, Category, Result } from '@svelte-vitals/core';
1
+ import { HeadTag, BranchStep, Config, RuleOptionsSpec, Severity, RuleSetting, Category, Result } from '@svelte-vitals/core';
2
2
  export { defineConfig } from '@svelte-vitals/core';
3
3
  import { AST } from 'svelte/compiler';
4
4
 
@@ -34,12 +34,37 @@ interface ParsedHeading {
34
34
  /** 1-based source line, or 0 if unknown. */
35
35
  line: number;
36
36
  }
37
+
38
+ interface A11yNode {
39
+ kind: 'landmark' | 'id' | 'idref' | 'component';
40
+ /** landmark → 'main'|'banner'|'contentinfo'|'complementary'; id/idref → the literal id; component → component name */
41
+ key: string;
42
+ line: number;
43
+ /** inside {#each} body or {#snippet} definition at any depth (excluded from duplication counting) */
44
+ repeatable: boolean;
45
+ /** branch address from template root (empty = unconditional) */
46
+ path: BranchStep[];
47
+ /** for kind 'idref': the referencing attribute ('for', 'aria-labelledby', …, 'href') */
48
+ attr?: string;
49
+ /** the landmark ancestor element within this file, if any */
50
+ inLandmark?: string;
51
+ /** for kind 'landmark' from <header>/<footer>: at template top level in this file (which also implies "not inside sectioning content" — depth 0 has no ancestors at all) */
52
+ topLevel?: boolean;
53
+ }
54
+ interface ParsedA11y {
55
+ nodes: A11yNode[];
56
+ /** landmark ancestor of this file's <slot>/{@render children()} position, if any */
57
+ slotInLandmark?: string;
58
+ /** file contains {@html} or a spread attribute — poisons the closed world for no-missing-id-ref */
59
+ unknowableContent: boolean;
60
+ }
37
61
  interface ParsedFile {
38
62
  headTags: ParsedTag[];
39
63
  components: ComponentUse[];
40
64
  imports: ImportMap;
41
65
  images: ParsedImage[];
42
66
  headings: ParsedHeading[];
67
+ a11y: ParsedA11y;
43
68
  }
44
69
 
45
70
  /**
@@ -231,6 +256,8 @@ interface AnalyzeResult {
231
256
  ruleIds: string[];
232
257
  /** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
233
258
  examined: Record<string, Record<string, number>>;
259
+ /** Ids of rules `runRules` caught throwing — already folded into `config` via `withFailedRulesOff`; exposed separately so a caller with its own base config (the vite dev dashboard) can apply the same correction without adopting this call's `config`. */
260
+ failedRuleIds: string[];
234
261
  /** Non-fatal issues surfaced during analysis: config-file problems (unknown top-level keys, invalid enum values), version-floor notices, `--rules`/overrides conflicts, and skipped-file notices. Empty when none apply. */
235
262
  warnings: string[];
236
263
  /**
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  routeMatcher,
7
7
  run,
8
8
  spinnerEnabled
9
- } from "./chunk-VNVRHWO3.js";
9
+ } from "./chunk-5UQ6P2M2.js";
10
10
  import {
11
11
  loadConfigFile
12
12
  } from "./chunk-M5KM5SV7.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  INSTALL_ARGS,
3
3
  runInstallCliGunshi
4
- } from "./chunk-4O5F7EUJ.js";
5
- import "./chunk-MUIOPL5F.js";
4
+ } from "./chunk-77MMJCFK.js";
5
+ import "./chunk-43FGGQQZ.js";
6
6
  import "./chunk-M5KM5SV7.js";
7
7
  import "./chunk-SLUMRYUD.js";
8
8
  import "./chunk-TFBLQUAC.js";
9
- import "./chunk-F5P3REI7.js";
9
+ import "./chunk-CTMKTFST.js";
10
10
  import "./chunk-GE7TKVTX.js";
11
11
  import "./chunk-NMVBVKLX.js";
12
12
  export {
@@ -5,7 +5,7 @@ import {
5
5
  explainHelpJa,
6
6
  installHelpJa,
7
7
  rootHelpJa
8
- } from "./chunk-MHRO4GU7.js";
8
+ } from "./chunk-A5DVJX5O.js";
9
9
  export {
10
10
  JA_ARG_DESCRIPTIONS,
11
11
  ciHelpJa,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.45.1",
4
- "description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture) — not a runtime Web Vitals reporter.",
3
+ "version": "0.47.0",
4
+ "description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture, accessibility) — not a runtime Web Vitals reporter.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Kazuma Oe (https://github.com/oekazuma)",
@@ -52,7 +52,7 @@
52
52
  "magicast": "^0.5.4",
53
53
  "svelte": "^5.56.8",
54
54
  "tinyglobby": "^0.2.17",
55
- "@svelte-vitals/core": "0.41.1"
55
+ "@svelte-vitals/core": "0.43.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@gunshi/docs": "0.37.1",