@svelte-vitals/core 0.32.0 → 0.33.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
@@ -474,6 +474,11 @@ interface ComponentFacts {
474
474
  }[];
475
475
  /** Inline `svelte-vitals-disable-next-line` directives found in this file's source — component-rule escape hatch (issue #92). Optional: absent is equivalent to no directives, so existing external constructors of `ComponentFacts` are unaffected. */
476
476
  suppressions?: SuppressionDirective[];
477
+ /** Markdown links `[label](url)` appearing inside a comment (architecture/doc-link-target). */
478
+ commentLinks: {
479
+ url: string;
480
+ line: number;
481
+ }[];
477
482
  }
478
483
 
479
484
  /** What the per-file parsers produce — `ComponentFacts` minus `file`, with `suppressions` always present. */
@@ -1266,6 +1271,8 @@ declare const architectureReservedDirectoryNames: Rule;
1266
1271
 
1267
1272
  declare const architectureRouteComponentImport: Rule;
1268
1273
 
1274
+ declare const architectureDocLinkTarget: Rule;
1275
+
1269
1276
  declare const performanceHeavyImport: Rule;
1270
1277
 
1271
1278
  declare const performanceNamespaceImport: Rule;
@@ -1458,6 +1465,8 @@ interface ScoreResult {
1458
1465
  }
1459
1466
  interface ScoreOptions {
1460
1467
  applyCriticalCap?: boolean;
1468
+ /** The rules that ran. Defaults to the selected registry; supplied by tests and custom rule sets. */
1469
+ rules?: readonly Rule[];
1461
1470
  }
1462
1471
  /** Compute the headline score and its breakdown (design §12). */
1463
1472
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
@@ -1487,6 +1496,15 @@ declare function issueOf(result: Result): {
1487
1496
  type JsonIssue = ReturnType<typeof issueOf> & {
1488
1497
  severity: ReturnType<typeof effectiveSeverity>;
1489
1498
  };
1499
+ /**
1500
+ * Per-rule counts. A rule present with `findings: 0` ran and reported nothing, and an absent rule was not
1501
+ * selected — but only when the caller supplied `ruleIds`. Without it the map is seeded from results alone,
1502
+ * so absence means "produced nothing" rather than "not selected".
1503
+ */
1504
+ interface RuleEvidence {
1505
+ findings: number;
1506
+ passed: number;
1507
+ }
1490
1508
  interface JsonReport {
1491
1509
  version: string;
1492
1510
  score: number;
@@ -1496,6 +1514,7 @@ interface JsonReport {
1496
1514
  scoreModel: ScoreModel;
1497
1515
  }>;
1498
1516
  summary: Summary;
1517
+ rules: Record<string, RuleEvidence>;
1499
1518
  routes: Array<{
1500
1519
  route: string;
1501
1520
  score: number;
@@ -1506,11 +1525,11 @@ interface JsonReport {
1506
1525
  /** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
1507
1526
  declare function buildJsonReport(results: Result[], config: Config, meta: {
1508
1527
  version: string;
1509
- }): JsonReport;
1528
+ }, ruleIds?: readonly string[]): JsonReport;
1510
1529
  /** Render results as the documented JSON report string (design §7). */
1511
1530
  declare function formatJsonReport(results: Result[], config: Config, meta: {
1512
1531
  version: string;
1513
- }): string;
1532
+ }, ruleIds?: readonly string[]): string;
1514
1533
 
1515
1534
  /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
1516
1535
  declare function formatAgentReport(results: Result[], config: Config): string;
@@ -1602,4 +1621,4 @@ declare function escapeHtml(s: string): string;
1602
1621
  */
1603
1622
  declare function safeHref(url: string): string | null;
1604
1623
 
1605
- export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CATEGORIES, CHILD_NODE_KEYS, type Category, type Classification, type CompiledOverride, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type KitAlias, type KitModuleFacts, type OrphanEffectFact, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type RawKitAliases, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleOptionInfo, type RuleOptionSpec, type RuleOptions, type RuleOptionsSpec, type RuleOverride, type RuleSetting, type RuleSettingObject, type Runtime, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, VITE_CONFIG_FILES, type Value, type ViteKitConfigResult, allRules, applyOverrides, applyRuleSeverities, architectureComponentSize, architectureDirectoryNaming, architecturePrivateScopeImport, architecturePropCount, architectureReservedDirectoryNames, architectureRouteComponentImport, architectureUnitEntryFile, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, compileOverrides, computeHealth, computeScore, correctnessBasePathNavigation, correctnessCheckableBindValue, correctnessEachIndexKey, correctnessEachKey, correctnessEffectAsDerived, correctnessEffectAsOnMount, correctnessInstanceBrowserGlobal, correctnessNonreactiveBuiltinState, correctnessOrphanEffect, correctnessOrphanLifecycle, correctnessPropMutation, correctnessServerBrowserGlobal, correctnessStalePropDerivation, correctnessUnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, findKitAliasesInSvelteConfig, findKitPathsBaseInSvelteConfig, findKitPathsBaseInViteConfig, findMinifyDisabled, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, intOption, isMentionedAnywhere, isPenalized, lineOf, linkRule, listOption, mapOption, noColorPalette, overrideMatches, parseComponentFacts, parseKitModuleFacts, performanceFontPreloadCrossorigin, performanceHeavyImport, performanceImageDimensions, performanceImageLoadingHint, performanceLcpImage, performanceLoadWaterfall, performanceMinifyDisabled, performanceNamespaceImport, performancePreconnect, performancePreloadMissingAs, performanceRenderBlockingScript, performanceResponsiveImage, performanceSequentialAwaits, performanceStateRaw, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, securityHandlerStateWrite, securityJavascriptUrl, securityRawHtml, securityServerModuleState, securitySharedStateImport, selectRules, seoCanonicalUrl, seoCharset, seoDescriptionLength, seoDescriptionPresence, seoDuplicateDescription, seoDuplicateTitle, seoHeadingLevelSkip, seoHreflang, seoHtmlLang, seoImageAlt, seoIndexability, seoJsonLd, seoJsonLdDateFormat, seoJsonLdDeprecatedType, seoJsonLdPlaceholder, seoJsonLdRelativeUrl, seoJsonLdRequiredProps, seoJsonLdValidity, seoOgDescription, seoOgImage, seoOgTitle, seoOgUrl, seoRobotsTxt, seoSingleH1, seoSitemapInRobots, seoSitemapXml, seoSsrDisabled, seoTitleLength, seoTitlePresence, seoTwitterCard, seoViewport, settingOptions, settingSeverity, shouldSkipRangeCheck, summarize, textFromNodes, validateRuleOptions, validateRuleSetting, valueFromNodes };
1624
+ export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CATEGORIES, CHILD_NODE_KEYS, type Category, type Classification, type CompiledOverride, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type KitAlias, type KitModuleFacts, type OrphanEffectFact, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type RawKitAliases, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleEvidence, type RuleInfo, type RuleOptionInfo, type RuleOptionSpec, type RuleOptions, type RuleOptionsSpec, type RuleOverride, type RuleSetting, type RuleSettingObject, type Runtime, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, VITE_CONFIG_FILES, type Value, type ViteKitConfigResult, allRules, applyOverrides, applyRuleSeverities, architectureComponentSize, architectureDirectoryNaming, architectureDocLinkTarget, architecturePrivateScopeImport, architecturePropCount, architectureReservedDirectoryNames, architectureRouteComponentImport, architectureUnitEntryFile, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, compileOverrides, computeHealth, computeScore, correctnessBasePathNavigation, correctnessCheckableBindValue, correctnessEachIndexKey, correctnessEachKey, correctnessEffectAsDerived, correctnessEffectAsOnMount, correctnessInstanceBrowserGlobal, correctnessNonreactiveBuiltinState, correctnessOrphanEffect, correctnessOrphanLifecycle, correctnessPropMutation, correctnessServerBrowserGlobal, correctnessStalePropDerivation, correctnessUnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, findKitAliasesInSvelteConfig, findKitPathsBaseInSvelteConfig, findKitPathsBaseInViteConfig, findMinifyDisabled, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, intOption, isMentionedAnywhere, isPenalized, lineOf, linkRule, listOption, mapOption, noColorPalette, overrideMatches, parseComponentFacts, parseKitModuleFacts, performanceFontPreloadCrossorigin, performanceHeavyImport, performanceImageDimensions, performanceImageLoadingHint, performanceLcpImage, performanceLoadWaterfall, performanceMinifyDisabled, performanceNamespaceImport, performancePreconnect, performancePreloadMissingAs, performanceRenderBlockingScript, performanceResponsiveImage, performanceSequentialAwaits, performanceStateRaw, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, securityHandlerStateWrite, securityJavascriptUrl, securityRawHtml, securityServerModuleState, securitySharedStateImport, selectRules, seoCanonicalUrl, seoCharset, seoDescriptionLength, seoDescriptionPresence, seoDuplicateDescription, seoDuplicateTitle, seoHeadingLevelSkip, seoHreflang, seoHtmlLang, seoImageAlt, seoIndexability, seoJsonLd, seoJsonLdDateFormat, seoJsonLdDeprecatedType, seoJsonLdPlaceholder, seoJsonLdRelativeUrl, seoJsonLdRequiredProps, seoJsonLdValidity, seoOgDescription, seoOgImage, seoOgTitle, seoOgUrl, seoRobotsTxt, seoSingleH1, seoSitemapInRobots, seoSitemapXml, seoSsrDisabled, seoTitleLength, seoTitlePresence, seoTwitterCard, seoViewport, settingOptions, settingSeverity, shouldSkipRangeCheck, summarize, textFromNodes, validateRuleOptions, validateRuleSetting, valueFromNodes };
package/dist/index.js CHANGED
@@ -904,6 +904,53 @@ function collectSuppressions(source) {
904
904
  });
905
905
  return out;
906
906
  }
907
+ var MD_LINK = /\[[^\]]*\]\(([^)\s]+)\)/g;
908
+ var SCRIPT_OPEN = /<script(?:\s[^>]*)?>/;
909
+ var SCRIPT_CLOSE = /<\/script\s*>/;
910
+ var STYLE_OPEN = /<style(?:\s[^>]*)?>/;
911
+ var STYLE_CLOSE = /<\/style\s*>/;
912
+ function collectCommentLinks(source, { wholeFileIsScript = false } = {}) {
913
+ const out = [];
914
+ let htmlOpen = false;
915
+ let block = wholeFileIsScript ? "script" : void 0;
916
+ source.split("\n").forEach((line, i) => {
917
+ let text = "";
918
+ if (block !== void 0) {
919
+ if (block === "script" && /^\s*\/\//.test(line)) text = line.replace(/^\s*\/\//, "");
920
+ if (!wholeFileIsScript && (block === "script" ? SCRIPT_CLOSE : STYLE_CLOSE).test(line)) block = void 0;
921
+ } else {
922
+ let plain = "";
923
+ let rest = line;
924
+ while (rest.length > 0) {
925
+ if (htmlOpen) {
926
+ const end = rest.indexOf("-->");
927
+ if (end === -1) {
928
+ text += rest;
929
+ break;
930
+ }
931
+ text += rest.slice(0, end);
932
+ htmlOpen = false;
933
+ rest = rest.slice(end + 3);
934
+ continue;
935
+ }
936
+ const start = rest.indexOf("<!--");
937
+ if (start === -1) {
938
+ plain += rest;
939
+ break;
940
+ }
941
+ plain += rest.slice(0, start);
942
+ htmlOpen = true;
943
+ rest = rest.slice(start + 4);
944
+ }
945
+ const opened = SCRIPT_OPEN.test(plain) ? "script" : STYLE_OPEN.test(plain) ? "style" : void 0;
946
+ if (opened !== void 0 && !(opened === "script" ? SCRIPT_CLOSE : STYLE_CLOSE).test(plain)) block = opened;
947
+ }
948
+ for (const m of text.matchAll(MD_LINK)) {
949
+ if (m[1] !== void 0) out.push({ url: m[1], line: i + 1 });
950
+ }
951
+ });
952
+ return out;
953
+ }
907
954
  var EVAL_SCOPE_BOUNDARIES = /* @__PURE__ */ new Set([
908
955
  "FunctionDeclaration",
909
956
  "FunctionExpression",
@@ -1274,6 +1321,7 @@ function parseModuleFacts(source, filename) {
1274
1321
  checkableBindValues: [],
1275
1322
  basePathLinks,
1276
1323
  suppressions: collectSuppressions(source),
1324
+ commentLinks: collectCommentLinks(source, { wholeFileIsScript: true }),
1277
1325
  orphanEffects,
1278
1326
  orphanLifecycleCalls,
1279
1327
  browserGlobalRefs,
@@ -1480,7 +1528,8 @@ function parseComponentFacts(source, filename) {
1480
1528
  orphanLifecycleCalls,
1481
1529
  browserGlobalRefs,
1482
1530
  moduleStateDecls: [],
1483
- suppressions
1531
+ suppressions,
1532
+ commentLinks: collectCommentLinks(source)
1484
1533
  };
1485
1534
  }
1486
1535
 
@@ -1508,7 +1557,8 @@ function emptyComponentFacts(file) {
1508
1557
  orphanLifecycleCalls: [],
1509
1558
  browserGlobalRefs: [],
1510
1559
  moduleStateDecls: [],
1511
- suppressions: []
1560
+ suppressions: [],
1561
+ commentLinks: []
1512
1562
  };
1513
1563
  }
1514
1564
  async function collectComponentFacts(rt, cwd) {
@@ -5416,6 +5466,60 @@ var architectureRouteComponentImport = componentRule({
5416
5466
  }
5417
5467
  });
5418
5468
 
5469
+ // src/rules/architecture/doc-link-target.ts
5470
+ var ID8 = "architecture/doc-link-target";
5471
+ function stripFragment(url) {
5472
+ const i = url.search(/[#?]/);
5473
+ return i === -1 ? url : url.slice(0, i);
5474
+ }
5475
+ function baseOf(root) {
5476
+ return root.replace(/\/+$/, "");
5477
+ }
5478
+ function remainderUnder(url, root) {
5479
+ const base = baseOf(root);
5480
+ if (url === base) return "";
5481
+ return url.startsWith(`${base}/`) ? baseOf(url.slice(base.length + 1)) : void 0;
5482
+ }
5483
+ function isUnderSrc(target) {
5484
+ return target.startsWith("src/");
5485
+ }
5486
+ function rootFor(url, roots) {
5487
+ let best;
5488
+ for (const r of roots) {
5489
+ const remainder = remainderUnder(url, r);
5490
+ if (remainder === void 0) continue;
5491
+ const base = baseOf(r);
5492
+ if (best === void 0 || base.length > best.base.length) best = { base, remainder };
5493
+ }
5494
+ return best;
5495
+ }
5496
+ function targetExists(path, sourceFiles) {
5497
+ const prefix = `${path}/`;
5498
+ return sourceFiles.some((f) => f === path || f.startsWith(prefix));
5499
+ }
5500
+ function references(links, roots) {
5501
+ const out = [];
5502
+ for (const { url, line } of links) {
5503
+ const match = rootFor(stripFragment(url), roots);
5504
+ if (match === void 0) continue;
5505
+ if (!isUnderSrc(match.remainder)) continue;
5506
+ out.push({ line, target: match.remainder });
5507
+ }
5508
+ return out;
5509
+ }
5510
+ var architectureDocLinkTarget = componentRule({
5511
+ id: ID8,
5512
+ title: "Documentation link target",
5513
+ category: "architecture",
5514
+ severity: "info",
5515
+ label: "Documentation link targets",
5516
+ options: { urlRoots: { kind: "string-list", default: [] } },
5517
+ recommendation: "Point the link at the unit that exists now, or remove it. A link inside a comment has nothing to resolve it, so a rename leaves it silently broken.",
5518
+ rationale: "A documentation link written in a comment is invisible to type checking, module resolution and the test runner, so a convention-driven rename leaves it pointing at nothing and only human review notices.",
5519
+ applies: (c, o, ctx) => ctx.sourceFiles !== void 0 && references(c.commentLinks, listOption(o, "urlRoots")).length > 0,
5520
+ bad: (c, o, ctx) => references(c.commentLinks, listOption(o, "urlRoots")).filter(({ target }) => !targetExists(target, ctx.sourceFiles ?? [])).map(({ line, target }) => ({ line, message: `${target} does not exist` }))
5521
+ });
5522
+
5419
5523
  // src/rules/perf/heavy-import.ts
5420
5524
  var HEAVY_PACKAGES = {
5421
5525
  lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
@@ -5632,6 +5736,7 @@ var allRules = [
5632
5736
  architectureDirectoryNaming,
5633
5737
  architectureReservedDirectoryNames,
5634
5738
  architectureRouteComponentImport,
5739
+ architectureDocLinkTarget,
5635
5740
  performanceHeavyImport,
5636
5741
  performanceNamespaceImport,
5637
5742
  performanceMinifyDisabled,
@@ -5692,8 +5797,31 @@ function hasFailureAtOrAbove(summary, min) {
5692
5797
  return order.some((sev, idx) => idx >= threshold && summary[sev] > 0);
5693
5798
  }
5694
5799
 
5695
- // src/scoring/score.ts
5800
+ // src/scoring/inventory.ts
5696
5801
  var DEDUCTION = { critical: 15, warning: 5, info: 1 };
5802
+ function pairKey(category, scope) {
5803
+ return `${category}::${scope}`;
5804
+ }
5805
+ function severityOf(rule, config) {
5806
+ const setting = settingSeverity(config.rules[rule.id]);
5807
+ if (setting === "off") return void 0;
5808
+ return setting ?? rule.severity;
5809
+ }
5810
+ function buildInventory(config, rules = selectRules(allRules, config)) {
5811
+ const out = /* @__PURE__ */ new Map();
5812
+ for (const rule of rules) {
5813
+ const severity = severityOf(rule, config);
5814
+ if (severity === void 0) continue;
5815
+ const key = pairKey(rule.category, rule.scope);
5816
+ out.set(key, (out.get(key) ?? 0) + DEDUCTION[severity]);
5817
+ }
5818
+ return out;
5819
+ }
5820
+ function ruleScopes(rules) {
5821
+ return new Map(rules.map((r) => [r.id, pairKey(r.category, r.scope)]));
5822
+ }
5823
+
5824
+ // src/scoring/score.ts
5697
5825
  var CRITICAL_CAP = 79;
5698
5826
  function clamp(n) {
5699
5827
  return Math.max(0, Math.min(100, n));
@@ -5701,27 +5829,36 @@ function clamp(n) {
5701
5829
  function computeScore(results, config, options = {}) {
5702
5830
  const routeResults = results.filter((r) => r.route !== void 0);
5703
5831
  const projectResults = results.filter((r) => r.route === void 0);
5704
- const routeScores = /* @__PURE__ */ new Map();
5705
- for (const r of routeResults) if (!routeScores.has(r.route)) routeScores.set(r.route, 100);
5832
+ const rules = selectRules([...options.rules ?? allRules], config);
5833
+ const inventory = buildInventory(config, rules);
5834
+ const pairOf = ruleScopes(rules);
5706
5835
  let anyCritical = false;
5707
- const routeRuleMax = /* @__PURE__ */ new Map();
5836
+ const observed = /* @__PURE__ */ new Map();
5837
+ const ruleMax = /* @__PURE__ */ new Map();
5708
5838
  for (const r of routeResults) {
5839
+ const key = r.route;
5840
+ if (!observed.has(key)) observed.set(key, /* @__PURE__ */ new Set());
5841
+ const pair = pairOf.get(r.id);
5842
+ if (pair !== void 0) observed.get(key).add(pair);
5709
5843
  if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
5710
5844
  const sev = effectiveSeverity(r, config);
5711
5845
  if (sev === "critical") anyCritical = true;
5712
- const route = r.route;
5713
- let perRule = routeRuleMax.get(route);
5714
- if (!perRule) routeRuleMax.set(route, perRule = /* @__PURE__ */ new Map());
5846
+ let perRule = ruleMax.get(key);
5847
+ if (!perRule) ruleMax.set(key, perRule = /* @__PURE__ */ new Map());
5715
5848
  const prev = perRule.get(r.id) ?? 0;
5716
5849
  if (DEDUCTION[sev] > prev) perRule.set(r.id, DEDUCTION[sev]);
5717
5850
  }
5718
- for (const [route, perRule] of routeRuleMax) {
5719
- let deduction = 0;
5720
- for (const d of perRule.values()) deduction += d;
5721
- routeScores.set(route, routeScores.get(route) - deduction);
5722
- }
5723
- const scores = [...routeScores.values()].map(clamp);
5724
- const rawRouteAverage = scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : 100;
5851
+ let totalDeficit = 0;
5852
+ for (const [key, pairs] of observed) {
5853
+ let failed = 0;
5854
+ for (const d of ruleMax.get(key)?.values() ?? []) failed += d;
5855
+ let inventoryWeight = 0;
5856
+ for (const p of pairs) inventoryWeight += inventory.get(p) ?? 0;
5857
+ inventoryWeight = Math.max(inventoryWeight, failed);
5858
+ totalDeficit += inventoryWeight === 0 ? 0 : 100 * failed / inventoryWeight;
5859
+ }
5860
+ const keyCount = observed.size;
5861
+ const rawRouteAverage = keyCount === 0 ? 100 : 100 - totalDeficit / keyCount;
5725
5862
  const routeAverage = Math.floor(rawRouteAverage);
5726
5863
  const projectRuleMax = /* @__PURE__ */ new Map();
5727
5864
  for (const r of projectResults) {
@@ -5941,9 +6078,20 @@ function issueOf(result) {
5941
6078
  ...result.fix ? { fix: result.fix } : {}
5942
6079
  };
5943
6080
  }
5944
- function buildJsonReport(results, config, meta) {
6081
+ function ruleEvidence(results, config, ruleIds) {
6082
+ const out = {};
6083
+ for (const id of ruleIds ?? []) out[id] = { findings: 0, passed: 0 };
6084
+ for (const r of results) {
6085
+ const entry = out[r.id] ??= { findings: 0, passed: 0 };
6086
+ if (isPenalized(r.detection, config.treatDynamicAs)) entry.findings += 1;
6087
+ else entry.passed += 1;
6088
+ }
6089
+ return out;
6090
+ }
6091
+ function buildJsonReport(results, config, meta, ruleIds) {
5945
6092
  const { health, categories: byCat, weights } = computeHealth(results, config);
5946
6093
  const summary = summarize(results, config);
6094
+ const rules = ruleEvidence(results, config, ruleIds);
5947
6095
  const categories = Object.fromEntries(
5948
6096
  Object.entries(byCat).map(([cat, sr]) => [cat, { score: sr.score, scoreModel: sr.scoreModel }])
5949
6097
  );
@@ -5959,10 +6107,10 @@ function buildJsonReport(results, config, meta) {
5959
6107
  issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
5960
6108
  }));
5961
6109
  const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
5962
- return { version: meta.version, score: health, weights, categories, summary, routes, siteIssues };
6110
+ return { version: meta.version, score: health, weights, categories, summary, rules, routes, siteIssues };
5963
6111
  }
5964
- function formatJsonReport(results, config, meta) {
5965
- return JSON.stringify(buildJsonReport(results, config, meta), null, 2);
6112
+ function formatJsonReport(results, config, meta, ruleIds) {
6113
+ return JSON.stringify(buildJsonReport(results, config, meta, ruleIds), null, 2);
5966
6114
  }
5967
6115
 
5968
6116
  // src/reporter/agent.ts
@@ -6896,6 +7044,7 @@ export {
6896
7044
  applyRuleSeverities,
6897
7045
  architectureComponentSize,
6898
7046
  architectureDirectoryNaming,
7047
+ architectureDocLinkTarget,
6899
7048
  architecturePrivateScopeImport,
6900
7049
  architecturePropCount,
6901
7050
  architectureReservedDirectoryNames,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",