@svelte-vitals/core 0.38.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
package/dist/index.js CHANGED
@@ -826,7 +826,7 @@ function countProps(program) {
826
826
  if (n.type !== "VariableDeclarator" || !n.init || !isPropsCall(unwrapTs(n.init))) return;
827
827
  seen++;
828
828
  const props = n.id?.type === "ObjectPattern" ? n.id.properties : void 0;
829
- if (!Array.isArray(props) || props.some((p) => p?.type === "RestElement")) {
829
+ if (!Array.isArray(props)) {
830
830
  uncountable = true;
831
831
  return;
832
832
  }
@@ -2737,6 +2737,13 @@ function imageRule(opts) {
2737
2737
  severity: opts.severity,
2738
2738
  detection: { presence: "own", value: "static" },
2739
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,
2740
2747
  message: opts.label,
2741
2748
  recommendation: opts.recommendation,
2742
2749
  docsUrl: docsUrl12
@@ -2836,6 +2843,13 @@ function linkRule(opts) {
2836
2843
  severity: opts.severity,
2837
2844
  detection: { presence: "own", value: "static" },
2838
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,
2839
2853
  message: opts.label,
2840
2854
  recommendation: opts.recommendation,
2841
2855
  docsUrl: docsUrl12
@@ -2938,6 +2952,11 @@ var performanceLcpImage = {
2938
2952
  severity: "warning",
2939
2953
  detection: { presence: "own", value: "static" },
2940
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,
2941
2960
  message: "LCP image eager loading",
2942
2961
  recommendation,
2943
2962
  docsUrl
@@ -2992,6 +3011,10 @@ var performanceRenderBlockingScript = {
2992
3011
  severity: "warning",
2993
3012
  detection: { presence: "own", value: "static" },
2994
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,
2995
3018
  message: "No render-blocking scripts",
2996
3019
  recommendation: recommendation2,
2997
3020
  docsUrl: docsUrl2
@@ -3247,6 +3270,10 @@ var performancePreconnect = {
3247
3270
  severity: "info",
3248
3271
  detection: { presence: "own", value: "static" },
3249
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,
3250
3277
  message: "Third-party origins are preconnected",
3251
3278
  recommendation: recommendation3,
3252
3279
  docsUrl: docsUrl3
@@ -3588,6 +3615,9 @@ function jsonldRule(opts) {
3588
3615
  severity: opts.severity,
3589
3616
  detection: PASS,
3590
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,
3591
3621
  message: opts.label,
3592
3622
  recommendation: opts.recommendation,
3593
3623
  docsUrl: docsUrl12
@@ -3641,6 +3671,9 @@ var seoJsonLdValidity = {
3641
3671
  severity: "warning",
3642
3672
  detection: PASS,
3643
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,
3644
3677
  message: "JSON-LD validity",
3645
3678
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
3646
3679
  docsUrl: docsUrl12
@@ -3797,6 +3830,11 @@ function lengthRule(opts) {
3797
3830
  severity: "info",
3798
3831
  detection: PASS,
3799
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,
3800
3838
  message: opts.label,
3801
3839
  recommendation: recommendation12,
3802
3840
  docsUrl: docsUrl12
@@ -3920,6 +3958,9 @@ var seoHreflang = {
3920
3958
  severity: "warning",
3921
3959
  detection: PASS,
3922
3960
  route: head.route,
3961
+ // Same `location` the penalized branch above uses (design
3962
+ // 2026-08-08-pass-result-location-design.md).
3963
+ location,
3923
3964
  message: "hreflang",
3924
3965
  recommendation: recommendation4,
3925
3966
  docsUrl: docsUrl4
@@ -3972,6 +4013,10 @@ var seoSingleH1 = {
3972
4013
  severity: "warning",
3973
4014
  detection: PASS,
3974
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,
3975
4020
  message: "Heading hierarchy",
3976
4021
  recommendation: recommendation5,
3977
4022
  docsUrl: docsUrl5
@@ -4021,6 +4066,9 @@ function uniquenessRule(opts) {
4021
4066
  severity: "warning",
4022
4067
  detection: PASS,
4023
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,
4024
4072
  message: opts.label,
4025
4073
  recommendation: opts.recommendation,
4026
4074
  docsUrl: docsUrl12
@@ -4094,6 +4142,11 @@ var seoHeadingLevelSkip = {
4094
4142
  severity: "info",
4095
4143
  detection: PASS,
4096
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,
4097
4150
  message: "Heading order",
4098
4151
  recommendation: recommendation6,
4099
4152
  docsUrl: docsUrl6
@@ -4133,6 +4186,9 @@ function kitModuleRule(opts) {
4133
4186
  severity,
4134
4187
  detection: PASS2,
4135
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,
4136
4192
  message: opts.label,
4137
4193
  recommendation: opts.recommendation,
4138
4194
  docsUrl: docsUrl12
@@ -4212,6 +4268,9 @@ function componentRule(opts) {
4212
4268
  severity,
4213
4269
  detection: PASS3,
4214
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,
4215
4274
  message: opts.label,
4216
4275
  recommendation: recommendation12,
4217
4276
  docsUrl: docsUrl12
@@ -4417,6 +4476,9 @@ function emitFile(out, file, issues, suppressions) {
4417
4476
  severity: "critical",
4418
4477
  detection: PASS4,
4419
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,
4420
4482
  message: LABEL,
4421
4483
  recommendation: RECOMMENDATION,
4422
4484
  docsUrl: DOCS_URL
@@ -4508,6 +4570,9 @@ function emitFile2(out, file, links, suppressions) {
4508
4570
  severity: "warning",
4509
4571
  detection: PASS5,
4510
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,
4511
4576
  message: LABEL2,
4512
4577
  recommendation: RECOMMENDATION2,
4513
4578
  docsUrl: DOCS_URL2
@@ -4575,6 +4640,9 @@ function emitFile3(out, file, issues, suppressions) {
4575
4640
  severity: "critical",
4576
4641
  detection: PASS6,
4577
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,
4578
4646
  message: LABEL3,
4579
4647
  recommendation: RECOMMENDATION3,
4580
4648
  docsUrl: DOCS_URL3
@@ -4853,6 +4921,11 @@ var architecturePrivateScopeImport = {
4853
4921
  severity: "info",
4854
4922
  detection: { presence: "own", value: "static" },
4855
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,
4856
4929
  message: "No private-scope imports",
4857
4930
  recommendation: recommendation7,
4858
4931
  docsUrl: docsUrl7
@@ -5031,6 +5104,8 @@ var architectureUnitEntryFile = {
5031
5104
  ...Object.keys(mapOption(globalOptions, "pascalCaseUnits"))
5032
5105
  ]);
5033
5106
  const usedKeys = /* @__PURE__ */ new Set();
5107
+ const examinedCounts = {};
5108
+ for (const key of globalKeys) examinedCounts[key] = 0;
5034
5109
  const excludedDirs = [];
5035
5110
  const matchedSurviving = /* @__PURE__ */ new Set();
5036
5111
  for (const dir of [...dirs].sort()) {
@@ -5058,6 +5133,10 @@ var architectureUnitEntryFile = {
5058
5133
  ext = byCasing.best === void 0 ? void 0 : pascalUnits[byCasing.best];
5059
5134
  }
5060
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
+ }
5061
5140
  const expected = `${dir}/${baseName(dir)}${ext}`;
5062
5141
  if (fileSet.has(expected)) {
5063
5142
  out.push({
@@ -5107,6 +5186,7 @@ var architectureUnitEntryFile = {
5107
5186
  docsUrl: docsUrl8
5108
5187
  });
5109
5188
  }
5189
+ ctx.recordExamined?.(examinedCounts);
5110
5190
  return out;
5111
5191
  }
5112
5192
  };
@@ -5180,6 +5260,8 @@ var architectureDirectoryNaming = {
5180
5260
  const globalOptions = resolveRuleOptions(ID5, OPTIONS4, ctx.config);
5181
5261
  const globalMap = mapOption(globalOptions, "directories");
5182
5262
  const globalKeys = new Set(Object.keys(globalMap));
5263
+ const examinedCounts = {};
5264
+ for (const key of globalKeys) examinedCounts[key] = 0;
5183
5265
  const usedKeys = /* @__PURE__ */ new Set();
5184
5266
  const excludedDirs = [];
5185
5267
  for (const dir of [...dirs].sort()) {
@@ -5197,6 +5279,7 @@ var architectureDirectoryNaming = {
5197
5279
  if (m.best === void 0) continue;
5198
5280
  const decoded = decodeSegment(baseName(dir));
5199
5281
  if (decoded === void 0) continue;
5282
+ if (globalKeys.has(m.best)) examinedCounts[m.best] = (examinedCounts[m.best] ?? 0) + 1;
5200
5283
  const allowed = casingsOf(declared[m.best]).known;
5201
5284
  if (satisfiesCasing(decoded, allowed)) continue;
5202
5285
  const at = reportAt(dir, files);
@@ -5244,6 +5327,7 @@ var architectureDirectoryNaming = {
5244
5327
  docsUrl: docsUrl9
5245
5328
  });
5246
5329
  }
5330
+ ctx.recordExamined?.(examinedCounts);
5247
5331
  return out;
5248
5332
  }
5249
5333
  };
@@ -5255,6 +5339,7 @@ var recommendation10 = "Use one of the names this location declares, or add the
5255
5339
  var OPTIONS5 = {
5256
5340
  scopes: { kind: "string-map", default: {} },
5257
5341
  unitScopes: { kind: "string-map", default: {} },
5342
+ anyCaseUnitScopes: { kind: "string-map", default: {} },
5258
5343
  exclude: { kind: "string-list", default: [] }
5259
5344
  };
5260
5345
  function stem(file) {
@@ -5270,6 +5355,7 @@ function isAnyCaseUnitDir(dir, filesIn) {
5270
5355
  const own = filesIn.get(dir);
5271
5356
  return own !== void 0 && own.some((f) => stem(f) === name);
5272
5357
  }
5358
+ var PRIORITY = { scopes: 0, unitScopes: 1, anyCaseUnitScopes: 2 };
5273
5359
  var architectureReservedDirectoryNames = {
5274
5360
  id: ID6,
5275
5361
  title: "Reserved directory names",
@@ -5301,26 +5387,45 @@ var architectureReservedDirectoryNames = {
5301
5387
  const globalOptions = resolveRuleOptions(ID6, OPTIONS5, ctx.config);
5302
5388
  const globalScopes = mapOption(globalOptions, "scopes");
5303
5389
  const globalUnits = mapOption(globalOptions, "unitScopes");
5304
- 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
+ ]);
5305
5396
  const usedKeys = /* @__PURE__ */ new Set();
5306
5397
  const excludedDirs = [];
5307
5398
  const nonUnitDirs = [];
5308
- const collisions = /* @__PURE__ */ new Set();
5309
- 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) => {
5310
5409
  for (const key of Object.keys(scopesMap)) {
5311
- if (!Object.hasOwn(unitMap, key)) continue;
5312
5410
  if (namesOf(scopesMap[key]).length === 0) continue;
5313
- if (namesOf(unitMap[key]).length === 0) continue;
5314
- 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));
5315
5417
  }
5316
5418
  };
5317
- noteCollisions(globalScopes, globalUnits);
5419
+ noteCollisions(globalScopes, globalUnits, globalAnyUnits);
5318
5420
  for (const dir of [...dirs].sort()) {
5319
5421
  const o = resolveRuleOptions(ID6, OPTIONS5, ctx.config, { route: dir, file: dir }, compiledOverrides);
5320
5422
  const scopes = mapOption(o, "scopes");
5321
5423
  const unitScopes = mapOption(o, "unitScopes");
5322
- if (Object.keys(scopes).length === 0 && Object.keys(unitScopes).length === 0) continue;
5323
- 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);
5324
5429
  const excluded = compile(listOption(o, "exclude"));
5325
5430
  if (isExcluded(dir, ancestorDirs2(dir), excluded)) {
5326
5431
  excludedDirs.push(dir);
@@ -5328,22 +5433,46 @@ var architectureReservedDirectoryNames = {
5328
5433
  }
5329
5434
  const liveScopes = Object.keys(scopes).filter((k) => namesOf(scopes[k]).length > 0);
5330
5435
  const isUnit = isUnitDir(dir, filesIn);
5436
+ const isAnyUnit = isAnyCaseUnitDir(dir, filesIn);
5331
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) : [];
5332
5439
  if (!isUnit) nonUnitDirs.push(dir);
5440
+ if (!isAnyUnit) nonAnyUnitDirs.push(dir);
5333
5441
  const byPosition = matchKeys(dir, compile(liveScopes, true));
5334
5442
  const byUnit = matchKeys(dir, compile(liveUnits, true));
5443
+ const byAnyUnit = matchKeys(dir, compile(liveAnyUnits, true));
5335
5444
  for (const k of byPosition.matched) if (globalKeys.has(k)) usedKeys.add(k);
5336
5445
  for (const k of byUnit.matched) if (globalKeys.has(k)) usedKeys.add(k);
5337
- let governing;
5338
- if (byPosition.best !== void 0 && byUnit.best !== void 0) {
5339
- governing = moreSpecificGlob(byUnit.best, byPosition.best) ? namesOf(unitScopes[byUnit.best]) : namesOf(scopes[byPosition.best]);
5340
- } else if (byPosition.best !== void 0) {
5341
- governing = namesOf(scopes[byPosition.best]);
5342
- } else if (byUnit.best !== void 0) {
5343
- 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
+ }
5344
5472
  }
5345
- if (governing === void 0) continue;
5346
- 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);
5347
5476
  for (const child of kids.get(dir) ?? []) {
5348
5477
  if (allowed.has(baseName(child))) continue;
5349
5478
  if (isExcluded(child, ancestorDirs2(child), excluded)) continue;
@@ -5364,7 +5493,7 @@ var architectureReservedDirectoryNames = {
5364
5493
  detection: { presence: "none", value: "absent" },
5365
5494
  route: child,
5366
5495
  location: at,
5367
- 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(", ")}.`,
5368
5497
  recommendation: recommendation10,
5369
5498
  docsUrl: docsUrl10,
5370
5499
  fix: {
@@ -5374,19 +5503,24 @@ var architectureReservedDirectoryNames = {
5374
5503
  }
5375
5504
  }
5376
5505
  const notes = /* @__PURE__ */ new Map();
5377
- for (const key of collisions) {
5378
- notes.set(key, "declared in both scopes and unitScopes, so the scopes entry wins wherever both apply");
5379
- }
5506
+ for (const [key, message] of collisions) notes.set(key, message);
5380
5507
  for (const key of globalKeys) {
5381
5508
  if (notes.has(key)) continue;
5382
5509
  const scopesEmpty = Object.hasOwn(globalScopes, key) && namesOf(globalScopes[key]).length === 0;
5383
5510
  const unitsEmpty = Object.hasOwn(globalUnits, key) && namesOf(globalUnits[key]).length === 0;
5384
- if (scopesEmpty || unitsEmpty) {
5511
+ const anyUnitsEmpty = Object.hasOwn(globalAnyUnits, key) && namesOf(globalAnyUnits[key]).length === 0;
5512
+ if (scopesEmpty || unitsEmpty || anyUnitsEmpty) {
5385
5513
  notes.set(key, "names no directory name at all");
5386
5514
  }
5387
5515
  }
5388
5516
  const unused = [...globalKeys].filter((k) => !notes.has(k) && !usedKeys.has(k));
5389
- 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
+ );
5390
5524
  for (const key of keysMatchingAny(unitOnly, nonUnitDirs, compile)) {
5391
5525
  notes.set(key, "matched directories but never a unit");
5392
5526
  }
@@ -5410,6 +5544,7 @@ var architectureReservedDirectoryNames = {
5410
5544
  docsUrl: docsUrl10
5411
5545
  });
5412
5546
  }
5547
+ ctx.recordExamined?.(examinedCounts);
5413
5548
  return out;
5414
5549
  }
5415
5550
  };
@@ -6612,6 +6747,7 @@ body{background:var(--ground);color:var(--ink);font-family:var(--sans);line-heig
6612
6747
  .dv-cat-top{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}
6613
6748
  .dv-bar{height:7px;border-radius:999px;background:var(--line);overflow:hidden}
6614
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}
6615
6751
  .dv-filters{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}
6616
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}
6617
6753
  .dv-chip[aria-pressed="true"]{background:var(--active-bg);border-color:var(--active-bg);color:var(--active-ink)}
@@ -7084,12 +7220,21 @@ var APP_SCRIPT = `
7084
7220
  var band = scoreBand(c.score);
7085
7221
  var weight = s.report.weights[cat];
7086
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;
7087
7231
  return h('div', { class: 'dv-cat' }, [
7088
7232
  h('div', { class: 'dv-cat-top' }, [
7089
7233
  h('span', { text: name + (weight !== undefined ? ' (weight ' + weight + ')' : '') }, []),
7090
7234
  h('span', { style: 'color:' + BAND_COLOR[band], text: String(c.score) }, [])
7091
7235
  ]),
7092
- 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
7093
7238
  ]);
7094
7239
  });
7095
7240
  var chips = renderFilterChips(s.report.categories);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.38.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",