@svelte-vitals/core 0.37.0 → 0.39.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/dist/index.d.ts CHANGED
@@ -971,7 +971,7 @@ interface Rule {
971
971
  category: Category;
972
972
  /** Default severity (overridable by config in later slices). */
973
973
  severity: Severity;
974
- /** 'route' = evaluated per route, 'project' = site-wide (design §10, §12). */
974
+ /** 'route' = evaluated per route, 'project' = site-wide, 'component' = evaluated per source file (design §10, §12). */
975
975
  scope: Scope;
976
976
  /** Why this rule matters — one or two sentences, surfaced by `svelte-vitals explain` (issue #24). */
977
977
  rationale: string;
@@ -1268,11 +1268,19 @@ declare const architectureDirectoryNaming: Rule;
1268
1268
 
1269
1269
  /**
1270
1270
  * architecture/reserved-directory-names — a directory's immediate subdirectories may only take names
1271
- * the project declared for that position (design 2026-07-29). L3: inert until a scope is declared.
1271
+ * the project declared for that position (design 2026-07-29, extended 2026-08-08 for lowercase units
1272
+ * issue #386).
1272
1273
  *
1273
- * Two option maps, differing in what their key names. A `scopes` key names the parent directly. A
1274
+ * The option maps differ in what their keys name. A `scopes` key names the parent directly. A
1274
1275
  * `unitScopes` key names a root, and the rule governs the children of whichever directories beneath
1275
- * it are units — the shape a glob cannot reach, because units nest to arbitrary depth.
1276
+ * it are units whose name begins A–Z — the shape a glob cannot reach, because units nest to arbitrary
1277
+ * depth. An `anyCaseUnitScopes` key names a root the same way, but governs units of *either* case:
1278
+ * `isUnitDir`'s letter test — A–Z plus a same-stemmed entry file, whatever its extension — excludes a
1279
+ * lowercase unit, so without this map no generic unit-map declaration governed one's children (a
1280
+ * `scopes` key naming the parent directly could still reach one) — measured at 129 of 299 units (43%)
1281
+ * on a real tree. Neither map is named with the bare word "unit": the
1282
+ * sibling rule `architecture/reserved-name-placement` records why that word alone is ambiguous between
1283
+ * the two predicates once both exist.
1276
1284
  *
1277
1285
  * There are no pass results. `computeScore` seeds every distinct `route` at 100 and averages, and the
1278
1286
  * subject here is a directory with no pre-existing score key, so a pass per directory would add
@@ -1643,13 +1651,11 @@ declare const APP_SCRIPT: string;
1643
1651
  declare function renderAppShell(snapshot: AppSnapshot): string;
1644
1652
  /**
1645
1653
  * Static (non-live) document over a prebuilt JsonReport — kept as the public name the
1646
- * html reporter has always exported. `routeBadges` preserves the old opts shape.
1654
+ * html reporter has always exported.
1647
1655
  */
1648
1656
  declare function buildHtmlDocument(report: JsonReport, meta: {
1649
1657
  version: string;
1650
1658
  coreVersion?: string;
1651
- }, opts?: {
1652
- routeBadges?: Record<string, RouteBadge>;
1653
1659
  }): string;
1654
1660
  /** Render results as the self-contained HTML report (the CLI's `--reporter html`). */
1655
1661
  declare function formatHtmlReport(results: Result[], config: Config, meta: {
package/dist/index.js CHANGED
@@ -167,17 +167,9 @@ function collectEachBlocks(node, source, acc) {
167
167
  }
168
168
  }
169
169
  var WALK_IGNORED_KEYS = /* @__PURE__ */ new Set(["type", "start", "end", "loc", "range"]);
170
+ var NO_BOUNDARIES = /* @__PURE__ */ new Set();
170
171
  function walkEstree(node, visit) {
171
- if (Array.isArray(node)) {
172
- for (const child of node) walkEstree(child, visit);
173
- return;
174
- }
175
- if (!node || typeof node !== "object" || typeof node.type !== "string") return;
176
- visit(node);
177
- for (const key of Object.keys(node)) {
178
- if (WALK_IGNORED_KEYS.has(key)) continue;
179
- walkEstree(node[key], visit);
180
- }
172
+ walkEvalScope(node, (n) => void visit(n), /* @__PURE__ */ new Set(), NO_BOUNDARIES);
181
173
  }
182
174
  function isEffectCall(node) {
183
175
  const c = node?.callee;
@@ -280,18 +272,7 @@ function scopeIntroducedNames(node) {
280
272
  return introduced;
281
273
  }
282
274
  function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
283
- if (Array.isArray(node)) {
284
- for (const child of node) walkScoped(child, visit, shadowed);
285
- return;
286
- }
287
- if (!node || typeof node !== "object" || typeof node.type !== "string") return;
288
- const introduced = scopeIntroducedNames(node);
289
- const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
290
- visit(node, scope);
291
- for (const key of Object.keys(node)) {
292
- if (WALK_IGNORED_KEYS.has(key)) continue;
293
- walkScoped(node[key], visit, scope);
294
- }
275
+ walkEvalScope(node, (n, scope) => void visit(n, scope), shadowed, NO_BOUNDARIES);
295
276
  }
296
277
  function collectStateWrites(root, stateNames, acc, kinds) {
297
278
  const record = (name, kind) => {
@@ -769,7 +750,7 @@ function collectPropNames(program, includeBindable) {
769
750
  let seen = 0;
770
751
  let ambiguous = false;
771
752
  walkEstree(program, (n) => {
772
- if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
753
+ if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(unwrapTs(n.init))) return;
773
754
  seen++;
774
755
  if (n.id?.type === "Identifier") {
775
756
  names.add(n.id.name);
@@ -842,10 +823,10 @@ function countProps(program) {
842
823
  let seen = 0;
843
824
  let uncountable = false;
844
825
  walkEstree(program, (n) => {
845
- if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(n.init)) return;
826
+ if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(unwrapTs(n.init))) return;
846
827
  seen++;
847
828
  const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
848
- if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
829
+ if (!Array.isArray(props)) {
849
830
  uncountable = true;
850
831
  return;
851
832
  }
@@ -958,19 +939,19 @@ var EVAL_SCOPE_BOUNDARIES = /* @__PURE__ */ new Set([
958
939
  "ClassDeclaration",
959
940
  "ClassExpression"
960
941
  ]);
961
- function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set()) {
942
+ function walkEvalScope(node, visit, shadowed = /* @__PURE__ */ new Set(), boundaries = EVAL_SCOPE_BOUNDARIES) {
962
943
  if (Array.isArray(node)) {
963
- for (const child of node) walkEvalScope(child, visit, shadowed);
944
+ for (const child of node) walkEvalScope(child, visit, shadowed, boundaries);
964
945
  return;
965
946
  }
966
947
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
967
948
  const introduced = scopeIntroducedNames(node);
968
949
  const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
969
950
  if (visit(node, scope)) return;
970
- if (EVAL_SCOPE_BOUNDARIES.has(node.type)) return;
951
+ if (boundaries.has(node.type)) return;
971
952
  for (const key of Object.keys(node)) {
972
953
  if (WALK_IGNORED_KEYS.has(key)) continue;
973
- walkEvalScope(node[key], visit, scope);
954
+ walkEvalScope(node[key], visit, scope, boundaries);
974
955
  }
975
956
  }
976
957
  function collectEvalScopeCalls(root, source, matcher, skipSubtree, initialShadowed) {
@@ -1261,13 +1242,13 @@ function collectModuleStateDecls(program, source) {
1261
1242
  const decl = unwrapExport(stmt);
1262
1243
  if (decl?.type === "VariableDeclaration") {
1263
1244
  for (const d of decl.declarations ?? []) {
1264
- if (d?.id?.type === "Identifier" && d.init && isStateDeclaration(d.init)) {
1245
+ if (d?.id?.type === "Identifier" && d.init && isStateDeclaration(unwrapTs(d.init))) {
1265
1246
  out.push({ name: d.id.name, line: lineOf(source, d.start) });
1266
1247
  }
1267
1248
  }
1268
1249
  } else if (decl?.type === "ClassDeclaration" && decl.id?.type === "Identifier") {
1269
1250
  const hasStateField = (decl.body?.body ?? []).some(
1270
- (m) => m?.type === "PropertyDefinition" && m.value && isStateDeclaration(m.value)
1251
+ (m) => m?.type === "PropertyDefinition" && m.value && isStateDeclaration(unwrapTs(m.value))
1271
1252
  );
1272
1253
  if (hasStateField) statefulClasses.add(decl.id.name);
1273
1254
  }
@@ -1303,6 +1284,17 @@ function parseModuleFacts(source, filename) {
1303
1284
  for (const l of raw) basePathLinks.push({ ...l, line: shift(l.line) });
1304
1285
  basePathLinks.sort((a, b) => a.line - b.line);
1305
1286
  }
1287
+ const importSpans = [];
1288
+ const namespaceImports = [];
1289
+ if (program) {
1290
+ const rawImportSpans = [];
1291
+ collectImportSources(program, wrapped, rawImportSpans);
1292
+ for (const s of rawImportSpans) importSpans.push({ ...s, line: shift(s.line) });
1293
+ const rawNamespaceImports = [];
1294
+ collectNamespaceImports(program, wrapped, rawNamespaceImports);
1295
+ for (const n of rawNamespaceImports) namespaceImports.push({ ...n, line: shift(n.line) });
1296
+ }
1297
+ const imports = importSpans.map((s) => s.source);
1306
1298
  return {
1307
1299
  eachBlocks: [],
1308
1300
  effects: [],
@@ -1310,9 +1302,9 @@ function parseModuleFacts(source, filename) {
1310
1302
  javascriptUrls: [],
1311
1303
  loc: 0,
1312
1304
  propCount: 0,
1313
- imports: [],
1314
- importSpans: [],
1315
- namespaceImports: [],
1305
+ imports,
1306
+ importSpans,
1307
+ namespaceImports,
1316
1308
  constableStates: [],
1317
1309
  mutatedProps: [],
1318
1310
  stalePropDerivations: [],
@@ -1409,11 +1401,12 @@ function parseComponentFacts(source, filename) {
1409
1401
  const stateDecls = [];
1410
1402
  walkEstree(program, (n) => {
1411
1403
  if (n.type !== "VariableDeclarator" || !n.init) return;
1412
- if (isStateDeclaration(n.init) && n.id?.type === "Identifier") {
1404
+ const init = unwrapTs(n.init);
1405
+ if (isStateDeclaration(init) && n.id?.type === "Identifier") {
1413
1406
  stateNames.add(n.id.name);
1414
1407
  stateDecls.push({ name: n.id.name, line: lineOf(source, n.start) });
1415
1408
  }
1416
- if (isStateDeclaration(n.init) || isDerivedDeclaration(n.init) || isPropsCall(n.init))
1409
+ if (isStateDeclaration(init) || isDerivedDeclaration(init) || isPropsCall(init))
1417
1410
  addBoundNames(n.id, reactiveNames);
1418
1411
  });
1419
1412
  walkEstree(program, (n) => {
@@ -1440,8 +1433,10 @@ function parseComponentFacts(source, filename) {
1440
1433
  for (const stmt of program.body ?? []) {
1441
1434
  if (stmt?.type !== "VariableDeclaration") continue;
1442
1435
  for (const d of stmt.declarations ?? []) {
1443
- if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1444
- const arg = unwrapTs(d.init.arguments?.[0]);
1436
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1437
+ const init = unwrapTs(d.init);
1438
+ if (!isPlainStateCall(init)) continue;
1439
+ const arg = unwrapTs(init.arguments?.[0]);
1445
1440
  if (arg?.type === "ObjectExpression" || arg?.type === "ArrayExpression") {
1446
1441
  rawableCandidates.push({ name: d.id.name, line: lineOf(source, d.start) });
1447
1442
  }
@@ -1475,8 +1470,10 @@ function parseComponentFacts(source, filename) {
1475
1470
  for (const stmt of program.body ?? []) {
1476
1471
  if (stmt?.type !== "VariableDeclaration") continue;
1477
1472
  for (const d of stmt.declarations ?? []) {
1478
- if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1479
- const arg = unwrapTs(d.init.arguments?.[0]);
1473
+ if (d?.id?.type !== "Identifier" || !d.init) continue;
1474
+ const init = unwrapTs(d.init);
1475
+ if (!isPlainStateCall(init)) continue;
1476
+ const arg = unwrapTs(init.arguments?.[0]);
1480
1477
  if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
1481
1478
  builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
1482
1479
  }
@@ -2740,6 +2737,13 @@ function imageRule(opts) {
2740
2737
  severity: opts.severity,
2741
2738
  detection: { presence: "own", value: "static" },
2742
2739
  route: route.route,
2740
+ // No single route-level file exists here (unlike ResolvedHead.file) — the
2741
+ // route's first image stands in as its attributed file (design
2742
+ // 2026-08-08-pass-result-location-design.md; this uncaught inline PASS literal
2743
+ // was missed by the design spike's grep and added to its blast-radius table
2744
+ // afterward, maintainer ruling, same date). `route.images.length === 0` already
2745
+ // continued above, so `[0]` is always defined here.
2746
+ location: route.images[0].file,
2743
2747
  message: opts.label,
2744
2748
  recommendation: opts.recommendation,
2745
2749
  docsUrl: docsUrl12
@@ -2839,6 +2843,13 @@ function linkRule(opts) {
2839
2843
  severity: opts.severity,
2840
2844
  detection: { presence: "own", value: "static" },
2841
2845
  route: head.route,
2846
+ // The route's own attributed file (design 2026-08-08-pass-result-location-design.md)
2847
+ // — this uncaught inline PASS literal was missed by the design spike's grep and
2848
+ // added to its blast-radius table afterward (maintainer ruling, same date). No
2849
+ // single per-tag location applies here (many links can back one pass), so the
2850
+ // route's own head file is the uniform attribution; per-tag penalized locations
2851
+ // above remain per-tag.
2852
+ location: head.file,
2842
2853
  message: opts.label,
2843
2854
  recommendation: opts.recommendation,
2844
2855
  docsUrl: docsUrl12
@@ -2941,6 +2952,11 @@ var performanceLcpImage = {
2941
2952
  severity: "warning",
2942
2953
  detection: { presence: "own", value: "static" },
2943
2954
  route: route.route,
2955
+ // Same `location` the penalized branch above uses (design
2956
+ // 2026-08-08-pass-result-location-design.md) — this uncaught inline PASS literal
2957
+ // was missed by the design spike's grep and added to its blast-radius table
2958
+ // afterward (maintainer ruling, same date).
2959
+ location: first.file,
2944
2960
  message: "LCP image eager loading",
2945
2961
  recommendation,
2946
2962
  docsUrl
@@ -2995,6 +3011,10 @@ var performanceRenderBlockingScript = {
2995
3011
  severity: "warning",
2996
3012
  detection: { presence: "own", value: "static" },
2997
3013
  route: head.route,
3014
+ // The route's own attributed file (design 2026-08-08-pass-result-location-design.md)
3015
+ // — this uncaught inline PASS literal was missed by the design spike's grep and
3016
+ // added to its blast-radius table afterward (maintainer ruling, same date).
3017
+ location: head.file,
2998
3018
  message: "No render-blocking scripts",
2999
3019
  recommendation: recommendation2,
3000
3020
  docsUrl: docsUrl2
@@ -3250,6 +3270,10 @@ var performancePreconnect = {
3250
3270
  severity: "info",
3251
3271
  detection: { presence: "own", value: "static" },
3252
3272
  route: head.route,
3273
+ // head.file — the same target used to resolve options above (design
3274
+ // 2026-08-08-pass-result-location-design.md) — so a `files:`-scoped override can
3275
+ // also match this passing seed via `severity: 'off'`.
3276
+ location: head.file,
3253
3277
  message: "Third-party origins are preconnected",
3254
3278
  recommendation: recommendation3,
3255
3279
  docsUrl: docsUrl3
@@ -3514,7 +3538,6 @@ var PLACEHOLDER_RES = [
3514
3538
  /yourcompany/i,
3515
3539
  /your name here/i
3516
3540
  ];
3517
- var PLACEHOLDERS = PLACEHOLDER_RES.map((r) => r.source);
3518
3541
  function hasPlaceholder(s) {
3519
3542
  return PLACEHOLDER_RES.some((re) => re.test(s));
3520
3543
  }
@@ -3592,6 +3615,9 @@ function jsonldRule(opts) {
3592
3615
  severity: opts.severity,
3593
3616
  detection: PASS,
3594
3617
  route: head.route,
3618
+ // Same `location` the penalized branch above uses (design
3619
+ // 2026-08-08-pass-result-location-design.md).
3620
+ location: head.file,
3595
3621
  message: opts.label,
3596
3622
  recommendation: opts.recommendation,
3597
3623
  docsUrl: docsUrl12
@@ -3645,6 +3671,9 @@ var seoJsonLdValidity = {
3645
3671
  severity: "warning",
3646
3672
  detection: PASS,
3647
3673
  route: head.route,
3674
+ // Same `location` the penalized branch above uses (design
3675
+ // 2026-08-08-pass-result-location-design.md).
3676
+ location: head.file,
3648
3677
  message: "JSON-LD validity",
3649
3678
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
3650
3679
  docsUrl: docsUrl12
@@ -3746,14 +3775,12 @@ var seoJsonLdRequiredProps = jsonldRule({
3746
3775
  });
3747
3776
 
3748
3777
  // src/rules/seo/text-metrics.ts
3749
- var segmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter() : void 0;
3778
+ var segmenter = new Intl.Segmenter();
3750
3779
  function collapseWhitespace(s) {
3751
3780
  return s.trim().replace(/\s+/g, " ");
3752
3781
  }
3753
3782
  function visibleLength(s) {
3754
- const collapsed = collapseWhitespace(s);
3755
- if (!segmenter) return [...collapsed].length;
3756
- return [...segmenter.segment(collapsed)].length;
3783
+ return [...segmenter.segment(collapseWhitespace(s))].length;
3757
3784
  }
3758
3785
 
3759
3786
  // src/rules/seo/length-rule.ts
@@ -3803,6 +3830,11 @@ function lengthRule(opts) {
3803
3830
  severity: "info",
3804
3831
  detection: PASS,
3805
3832
  route: head.route,
3833
+ // Same `location` the penalized branch above uses (design
3834
+ // 2026-08-08-pass-result-location-design.md) — without it, a `files:`-scoped
3835
+ // override can flip this result to PASS via its `options` but can never match
3836
+ // it to also apply `severity: 'off'`.
3837
+ location,
3806
3838
  message: opts.label,
3807
3839
  recommendation: recommendation12,
3808
3840
  docsUrl: docsUrl12
@@ -3926,6 +3958,9 @@ var seoHreflang = {
3926
3958
  severity: "warning",
3927
3959
  detection: PASS,
3928
3960
  route: head.route,
3961
+ // Same `location` the penalized branch above uses (design
3962
+ // 2026-08-08-pass-result-location-design.md).
3963
+ location,
3929
3964
  message: "hreflang",
3930
3965
  recommendation: recommendation4,
3931
3966
  docsUrl: docsUrl4
@@ -3978,6 +4013,10 @@ var seoSingleH1 = {
3978
4013
  severity: "warning",
3979
4014
  detection: PASS,
3980
4015
  route: route.route,
4016
+ // No single route-level file exists here (unlike ResolvedHead.file) — the
4017
+ // passing route's own <h1> stands in as its attributed file (design
4018
+ // 2026-08-08-pass-result-location-design.md). Only reached when h1.length === 1.
4019
+ location: h1[0].file,
3981
4020
  message: "Heading hierarchy",
3982
4021
  recommendation: recommendation5,
3983
4022
  docsUrl: docsUrl5
@@ -4027,6 +4066,9 @@ function uniquenessRule(opts) {
4027
4066
  severity: "warning",
4028
4067
  detection: PASS,
4029
4068
  route: e.route,
4069
+ // Same `location` the penalized branch above uses (design
4070
+ // 2026-08-08-pass-result-location-design.md).
4071
+ location: e.file,
4030
4072
  message: opts.label,
4031
4073
  recommendation: opts.recommendation,
4032
4074
  docsUrl: docsUrl12
@@ -4100,6 +4142,11 @@ var seoHeadingLevelSkip = {
4100
4142
  severity: "info",
4101
4143
  detection: PASS,
4102
4144
  route: route.route,
4145
+ // No single route-level file exists here (unlike ResolvedHead.file) — the
4146
+ // route's first heading stands in as its attributed file (design
4147
+ // 2026-08-08-pass-result-location-design.md). `route.headings.length === 0`
4148
+ // already continued above, so `[0]` is always defined here.
4149
+ location: route.headings[0].file,
4103
4150
  message: "Heading order",
4104
4151
  recommendation: recommendation6,
4105
4152
  docsUrl: docsUrl6
@@ -4139,6 +4186,9 @@ function kitModuleRule(opts) {
4139
4186
  severity,
4140
4187
  detection: PASS2,
4141
4188
  route: m.file,
4189
+ // Uniform PASS-result attribution (design 2026-08-08-pass-result-location-design.md):
4190
+ // same location a penalized result for this file would carry.
4191
+ location: m.file,
4142
4192
  message: opts.label,
4143
4193
  recommendation: opts.recommendation,
4144
4194
  docsUrl: docsUrl12
@@ -4218,6 +4268,9 @@ function componentRule(opts) {
4218
4268
  severity,
4219
4269
  detection: PASS3,
4220
4270
  route: c.file,
4271
+ // Uniform PASS-result attribution (design 2026-08-08-pass-result-location-design.md):
4272
+ // same location a penalized result for this file would carry.
4273
+ location: c.file,
4221
4274
  message: opts.label,
4222
4275
  recommendation: recommendation12,
4223
4276
  docsUrl: docsUrl12
@@ -4423,6 +4476,9 @@ function emitFile(out, file, issues, suppressions) {
4423
4476
  severity: "critical",
4424
4477
  detection: PASS4,
4425
4478
  route: file,
4479
+ // Uniform PASS-result attribution (design 2026-08-08-pass-result-location-design.md):
4480
+ // same location a penalized result for this file would carry.
4481
+ location: file,
4426
4482
  message: LABEL,
4427
4483
  recommendation: RECOMMENDATION,
4428
4484
  docsUrl: DOCS_URL
@@ -4514,6 +4570,9 @@ function emitFile2(out, file, links, suppressions) {
4514
4570
  severity: "warning",
4515
4571
  detection: PASS5,
4516
4572
  route: file,
4573
+ // Uniform PASS-result attribution (design 2026-08-08-pass-result-location-design.md):
4574
+ // same location a penalized result for this file would carry.
4575
+ location: file,
4517
4576
  message: LABEL2,
4518
4577
  recommendation: RECOMMENDATION2,
4519
4578
  docsUrl: DOCS_URL2
@@ -4581,6 +4640,9 @@ function emitFile3(out, file, issues, suppressions) {
4581
4640
  severity: "critical",
4582
4641
  detection: PASS6,
4583
4642
  route: file,
4643
+ // Uniform PASS-result attribution (design 2026-08-08-pass-result-location-design.md):
4644
+ // same location a penalized result for this file would carry.
4645
+ location: file,
4584
4646
  message: LABEL3,
4585
4647
  recommendation: RECOMMENDATION3,
4586
4648
  docsUrl: DOCS_URL3
@@ -4859,6 +4921,11 @@ var architecturePrivateScopeImport = {
4859
4921
  severity: "info",
4860
4922
  detection: { presence: "own", value: "static" },
4861
4923
  route: c.file,
4924
+ // Same location the penalized branch below uses (design
4925
+ // 2026-08-08-pass-result-location-design.md) — this uncaught inline PASS literal
4926
+ // was missed by the design spike's grep and added to its blast-radius table
4927
+ // afterward (maintainer ruling, same date).
4928
+ location: c.file,
4862
4929
  message: "No private-scope imports",
4863
4930
  recommendation: recommendation7,
4864
4931
  docsUrl: docsUrl7
@@ -5037,6 +5104,8 @@ var architectureUnitEntryFile = {
5037
5104
  ...Object.keys(mapOption(globalOptions, "pascalCaseUnits"))
5038
5105
  ]);
5039
5106
  const usedKeys = /* @__PURE__ */ new Set();
5107
+ const examinedCounts = {};
5108
+ for (const key of globalKeys) examinedCounts[key] = 0;
5040
5109
  const excludedDirs = [];
5041
5110
  const matchedSurviving = /* @__PURE__ */ new Set();
5042
5111
  for (const dir of [...dirs].sort()) {
@@ -5064,6 +5133,10 @@ var architectureUnitEntryFile = {
5064
5133
  ext = byCasing.best === void 0 ? void 0 : pascalUnits[byCasing.best];
5065
5134
  }
5066
5135
  if (ext === void 0) continue;
5136
+ const winningKey = viaUnits ? byPath.best : byCasing.best;
5137
+ if (winningKey !== void 0 && globalKeys.has(winningKey)) {
5138
+ examinedCounts[winningKey] = (examinedCounts[winningKey] ?? 0) + 1;
5139
+ }
5067
5140
  const expected = `${dir}/${baseName(dir)}${ext}`;
5068
5141
  if (fileSet.has(expected)) {
5069
5142
  out.push({
@@ -5113,6 +5186,7 @@ var architectureUnitEntryFile = {
5113
5186
  docsUrl: docsUrl8
5114
5187
  });
5115
5188
  }
5189
+ ctx.recordExamined?.(examinedCounts);
5116
5190
  return out;
5117
5191
  }
5118
5192
  };
@@ -5186,6 +5260,8 @@ var architectureDirectoryNaming = {
5186
5260
  const globalOptions = resolveRuleOptions(ID5, OPTIONS4, ctx.config);
5187
5261
  const globalMap = mapOption(globalOptions, "directories");
5188
5262
  const globalKeys = new Set(Object.keys(globalMap));
5263
+ const examinedCounts = {};
5264
+ for (const key of globalKeys) examinedCounts[key] = 0;
5189
5265
  const usedKeys = /* @__PURE__ */ new Set();
5190
5266
  const excludedDirs = [];
5191
5267
  for (const dir of [...dirs].sort()) {
@@ -5203,6 +5279,7 @@ var architectureDirectoryNaming = {
5203
5279
  if (m.best === void 0) continue;
5204
5280
  const decoded = decodeSegment(baseName(dir));
5205
5281
  if (decoded === void 0) continue;
5282
+ if (globalKeys.has(m.best)) examinedCounts[m.best] = (examinedCounts[m.best] ?? 0) + 1;
5206
5283
  const allowed = casingsOf(declared[m.best]).known;
5207
5284
  if (satisfiesCasing(decoded, allowed)) continue;
5208
5285
  const at = reportAt(dir, files);
@@ -5250,6 +5327,7 @@ var architectureDirectoryNaming = {
5250
5327
  docsUrl: docsUrl9
5251
5328
  });
5252
5329
  }
5330
+ ctx.recordExamined?.(examinedCounts);
5253
5331
  return out;
5254
5332
  }
5255
5333
  };
@@ -5261,6 +5339,7 @@ var recommendation10 = "Use one of the names this location declares, or add the
5261
5339
  var OPTIONS5 = {
5262
5340
  scopes: { kind: "string-map", default: {} },
5263
5341
  unitScopes: { kind: "string-map", default: {} },
5342
+ anyCaseUnitScopes: { kind: "string-map", default: {} },
5264
5343
  exclude: { kind: "string-list", default: [] }
5265
5344
  };
5266
5345
  function stem(file) {
@@ -5276,6 +5355,7 @@ function isAnyCaseUnitDir(dir, filesIn) {
5276
5355
  const own = filesIn.get(dir);
5277
5356
  return own !== void 0 && own.some((f) => stem(f) === name);
5278
5357
  }
5358
+ var PRIORITY = { scopes: 0, unitScopes: 1, anyCaseUnitScopes: 2 };
5279
5359
  var architectureReservedDirectoryNames = {
5280
5360
  id: ID6,
5281
5361
  title: "Reserved directory names",
@@ -5307,26 +5387,45 @@ var architectureReservedDirectoryNames = {
5307
5387
  const globalOptions = resolveRuleOptions(ID6, OPTIONS5, ctx.config);
5308
5388
  const globalScopes = mapOption(globalOptions, "scopes");
5309
5389
  const globalUnits = mapOption(globalOptions, "unitScopes");
5310
- const globalKeys = /* @__PURE__ */ new Set([...Object.keys(globalScopes), ...Object.keys(globalUnits)]);
5390
+ const globalAnyUnits = mapOption(globalOptions, "anyCaseUnitScopes");
5391
+ const globalKeys = /* @__PURE__ */ new Set([
5392
+ ...Object.keys(globalScopes),
5393
+ ...Object.keys(globalUnits),
5394
+ ...Object.keys(globalAnyUnits)
5395
+ ]);
5311
5396
  const usedKeys = /* @__PURE__ */ new Set();
5312
5397
  const excludedDirs = [];
5313
5398
  const nonUnitDirs = [];
5314
- const collisions = /* @__PURE__ */ new Set();
5315
- const noteCollisions = (scopesMap, unitMap) => {
5399
+ const nonAnyUnitDirs = [];
5400
+ const collisions = /* @__PURE__ */ new Map();
5401
+ const collisionMessage = (losers) => {
5402
+ const maps = ["scopes", ...losers];
5403
+ const list = maps.length === 2 ? `both ${maps[0]} and ${maps[1]}` : maps.join(", ");
5404
+ return `declared in ${list}, so the scopes entry wins wherever ${losers.length > 1 ? "they" : "both"} apply`;
5405
+ };
5406
+ const examinedCounts = {};
5407
+ for (const key of globalKeys) examinedCounts[key] = 0;
5408
+ const noteCollisions = (scopesMap, unitMap, anyUnitMap) => {
5316
5409
  for (const key of Object.keys(scopesMap)) {
5317
- if (!Object.hasOwn(unitMap, key)) continue;
5318
5410
  if (namesOf(scopesMap[key]).length === 0) continue;
5319
- if (namesOf(unitMap[key]).length === 0) continue;
5320
- collisions.add(key);
5411
+ const losers = [];
5412
+ if (Object.hasOwn(unitMap, key) && namesOf(unitMap[key]).length > 0) losers.push("unitScopes");
5413
+ if (Object.hasOwn(anyUnitMap, key) && namesOf(anyUnitMap[key]).length > 0) {
5414
+ losers.push("anyCaseUnitScopes");
5415
+ }
5416
+ if (losers.length > 0) collisions.set(key, collisionMessage(losers));
5321
5417
  }
5322
5418
  };
5323
- noteCollisions(globalScopes, globalUnits);
5419
+ noteCollisions(globalScopes, globalUnits, globalAnyUnits);
5324
5420
  for (const dir of [...dirs].sort()) {
5325
5421
  const o = resolveRuleOptions(ID6, OPTIONS5, ctx.config, { route: dir, file: dir }, compiledOverrides);
5326
5422
  const scopes = mapOption(o, "scopes");
5327
5423
  const unitScopes = mapOption(o, "unitScopes");
5328
- if (Object.keys(scopes).length === 0 && Object.keys(unitScopes).length === 0) continue;
5329
- noteCollisions(scopes, unitScopes);
5424
+ const anyCaseUnitScopes = mapOption(o, "anyCaseUnitScopes");
5425
+ if (Object.keys(scopes).length === 0 && Object.keys(unitScopes).length === 0 && Object.keys(anyCaseUnitScopes).length === 0) {
5426
+ continue;
5427
+ }
5428
+ noteCollisions(scopes, unitScopes, anyCaseUnitScopes);
5330
5429
  const excluded = compile(listOption(o, "exclude"));
5331
5430
  if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
5332
5431
  excludedDirs.push(dir);
@@ -5334,22 +5433,46 @@ var architectureReservedDirectoryNames = {
5334
5433
  }
5335
5434
  const liveScopes = Object.keys(scopes).filter((k) => namesOf(scopes[k]).length > 0);
5336
5435
  const isUnit = isUnitDir(dir, filesIn);
5436
+ const isAnyUnit = isAnyCaseUnitDir(dir, filesIn);
5337
5437
  const liveUnits = isUnit ? Object.keys(unitScopes).filter((k) => namesOf(unitScopes[k]).length > 0) : [];
5438
+ const liveAnyUnits = isAnyUnit ? Object.keys(anyCaseUnitScopes).filter((k) => namesOf(anyCaseUnitScopes[k]).length > 0) : [];
5338
5439
  if (!isUnit) nonUnitDirs.push(dir);
5440
+ if (!isAnyUnit) nonAnyUnitDirs.push(dir);
5339
5441
  const byPosition = matchKeys(dir, compile(liveScopes, true));
5340
5442
  const byUnit = matchKeys(dir, compile(liveUnits, true));
5443
+ const byAnyUnit = matchKeys(dir, compile(liveAnyUnits, true));
5341
5444
  for (const k of byPosition.matched) if (globalKeys.has(k)) usedKeys.add(k);
5342
5445
  for (const k of byUnit.matched) if (globalKeys.has(k)) usedKeys.add(k);
5343
- let governing;
5344
- if (byPosition.best !== void 0 && byUnit.best !== void 0) {
5345
- governing = moreSpecificGlob(byUnit.best, byPosition.best) ? namesOf(unitScopes[byUnit.best]) : namesOf(scopes[byPosition.best]);
5346
- } else if (byPosition.best !== void 0) {
5347
- governing = namesOf(scopes[byPosition.best]);
5348
- } else if (byUnit.best !== void 0) {
5349
- governing = namesOf(unitScopes[byUnit.best]);
5446
+ for (const k of byAnyUnit.matched) if (globalKeys.has(k)) usedKeys.add(k);
5447
+ const candidates = [];
5448
+ if (byPosition.best !== void 0) {
5449
+ candidates.push({ kind: "scopes", best: byPosition.best, names: namesOf(scopes[byPosition.best]) });
5450
+ }
5451
+ if (byUnit.best !== void 0) {
5452
+ candidates.push({
5453
+ kind: "unitScopes",
5454
+ best: byUnit.best,
5455
+ names: namesOf(unitScopes[byUnit.best])
5456
+ });
5457
+ }
5458
+ if (byAnyUnit.best !== void 0) {
5459
+ candidates.push({
5460
+ kind: "anyCaseUnitScopes",
5461
+ best: byAnyUnit.best,
5462
+ names: namesOf(anyCaseUnitScopes[byAnyUnit.best])
5463
+ });
5464
+ }
5465
+ let winner;
5466
+ for (const c of candidates) {
5467
+ if (winner === void 0 || moreSpecificGlob(c.best, winner.best)) {
5468
+ winner = c;
5469
+ } else if (!moreSpecificGlob(winner.best, c.best) && PRIORITY[c.kind] < PRIORITY[winner.kind]) {
5470
+ winner = c;
5471
+ }
5350
5472
  }
5351
- if (governing === void 0) continue;
5352
- const allowed = new Set(governing);
5473
+ if (winner === void 0) continue;
5474
+ if (globalKeys.has(winner.best)) examinedCounts[winner.best] = (examinedCounts[winner.best] ?? 0) + 1;
5475
+ const allowed = new Set(winner.names);
5353
5476
  for (const child of kids.get(dir) ?? []) {
5354
5477
  if (allowed.has(baseName(child))) continue;
5355
5478
  if (isExcluded(child, ancestorDirs2(child), excluded)) continue;
@@ -5370,7 +5493,7 @@ var architectureReservedDirectoryNames = {
5370
5493
  detection: { presence: "none", value: "absent" },
5371
5494
  route: child,
5372
5495
  location: at,
5373
- message: `${child} is not one of the names declared here: ${governing.join(", ")}.`,
5496
+ message: `${child} is not one of the names declared here: ${winner.names.join(", ")}.`,
5374
5497
  recommendation: recommendation10,
5375
5498
  docsUrl: docsUrl10,
5376
5499
  fix: {
@@ -5380,19 +5503,24 @@ var architectureReservedDirectoryNames = {
5380
5503
  }
5381
5504
  }
5382
5505
  const notes = /* @__PURE__ */ new Map();
5383
- for (const key of collisions) {
5384
- notes.set(key, "declared in both scopes and unitScopes, so the scopes entry wins wherever both apply");
5385
- }
5506
+ for (const [key, message] of collisions) notes.set(key, message);
5386
5507
  for (const key of globalKeys) {
5387
5508
  if (notes.has(key)) continue;
5388
5509
  const scopesEmpty = Object.hasOwn(globalScopes, key) && namesOf(globalScopes[key]).length === 0;
5389
5510
  const unitsEmpty = Object.hasOwn(globalUnits, key) && namesOf(globalUnits[key]).length === 0;
5390
- if (scopesEmpty || unitsEmpty) {
5511
+ const anyUnitsEmpty = Object.hasOwn(globalAnyUnits, key) && namesOf(globalAnyUnits[key]).length === 0;
5512
+ if (scopesEmpty || unitsEmpty || anyUnitsEmpty) {
5391
5513
  notes.set(key, "names no directory name at all");
5392
5514
  }
5393
5515
  }
5394
5516
  const unused = [...globalKeys].filter((k) => !notes.has(k) && !usedKeys.has(k));
5395
- const unitOnly = unused.filter((k) => Object.hasOwn(globalUnits, k) && !Object.hasOwn(globalScopes, k));
5517
+ const anyUnitOnly = unused.filter((k) => Object.hasOwn(globalAnyUnits, k) && !Object.hasOwn(globalScopes, k));
5518
+ for (const key of keysMatchingAny(anyUnitOnly, nonAnyUnitDirs, compile)) {
5519
+ notes.set(key, "matched directories but never a unit of either case");
5520
+ }
5521
+ const unitOnly = unused.filter(
5522
+ (k) => Object.hasOwn(globalUnits, k) && !Object.hasOwn(globalScopes, k) && !notes.has(k)
5523
+ );
5396
5524
  for (const key of keysMatchingAny(unitOnly, nonUnitDirs, compile)) {
5397
5525
  notes.set(key, "matched directories but never a unit");
5398
5526
  }
@@ -5416,6 +5544,7 @@ var architectureReservedDirectoryNames = {
5416
5544
  docsUrl: docsUrl10
5417
5545
  });
5418
5546
  }
5547
+ ctx.recordExamined?.(examinedCounts);
5419
5548
  return out;
5420
5549
  }
5421
5550
  };
@@ -6618,6 +6747,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
6618
6747
  .dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
6619
6748
  .dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
6620
6749
  .dv-bar>i{display:block;height:100%;border-radius:999px}
6750
+ .dv-cat-reach{font-size:11.5px;color:var(--muted);margin-top:5px}
6621
6751
  .dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
6622
6752
  .dv-chip{font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;background:var(--panel);border:1px solid var(--line-strong);color:var(--muted);padding:5px 12px;border-radius:999px}
6623
6753
  .dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
@@ -7090,12 +7220,21 @@ var APP_SCRIPT = `
7090
7220
  var band = scoreBand(c.score);
7091
7221
  var weight = s.report.weights[cat];
7092
7222
  var name = cat === 'seo' ? 'SEO' : cat.charAt(0).toUpperCase() + cat.slice(1);
7223
+ // keys/affectedKeys are absent on hand-built snapshots (older fixtures, tests) \u2014
7224
+ // render nothing rather than "undefined of undefined". 0 affected of N keys is still
7225
+ // rendered: on a real project that's the signal a thin score can't give, that the
7226
+ // category is clean project-wide and not just on the one key it happened to look at
7227
+ // (design: 2026-08-05-score-floor-and-reach-design.md).
7228
+ var reach = typeof c.keys === 'number' && c.keys > 0
7229
+ ? h('div', { class: 'dv-cat-reach', text: c.affectedKeys + ' of ' + c.keys + ' keys affected' }, [])
7230
+ : null;
7093
7231
  return h('div', { class: 'dv-cat' }, [
7094
7232
  h('div', { class: 'dv-cat-top' }, [
7095
7233
  h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
7096
7234
  h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
7097
7235
  ]),
7098
- h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])])
7236
+ h('div', { class: 'dv-bar' }, [h('i', { style: 'width:' + c.score + '%;background:' + BAND_COLOR[band] }, [])]),
7237
+ reach
7099
7238
  ]);
7100
7239
  });
7101
7240
  var chips = renderFilterChips(s.report.categories);
@@ -7202,13 +7341,10 @@ function renderAppShell(snapshot) {
7202
7341
  const title = snapshot.live ? "svelte-vitals dashboard" : "svelte-vitals report";
7203
7342
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title><style>${APP_STYLE}</style></head><body><div class="dv-app" id="dv-app"><header class="dv-topbar" id="dv-topbar"></header><nav class="dv-sidebar" id="dv-sidebar"></nav><main class="dv-detail" id="dv-detail"></main></div><script type="application/json" id="svelte-vitals-data">${embedJson(safe)}</script><script>${APP_SCRIPT}</script></body></html>`;
7204
7343
  }
7205
- function buildHtmlDocument(report, meta, opts) {
7206
- const badges = Object.fromEntries(
7207
- Object.entries(opts?.routeBadges ?? {}).filter(([, b]) => b === "measured" || b === "static")
7208
- );
7344
+ function buildHtmlDocument(report, meta) {
7209
7345
  return renderAppShell({
7210
7346
  report,
7211
- badges,
7347
+ badges: {},
7212
7348
  analyzing: false,
7213
7349
  sequence: 0,
7214
7350
  live: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",