@svelte-vitals/core 0.31.1 → 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. */
@@ -544,6 +549,20 @@ interface KitModuleFacts {
544
549
  name: string;
545
550
  line: number;
546
551
  }[];
552
+ /**
553
+ * `.set()`/`.update()` in a handler on an import resolving under the `$lib` server root.
554
+ * The call shape alone cannot tell a persistence client (`db.set(…)`) from a hand-rolled
555
+ * in-memory store, so the decision needs the target module — which this pure parse cannot
556
+ * read. `collectKitModuleFacts` resolves each one and promotes the in-memory ones into
557
+ * `importedStateWrites`; a consumer that ignores this field sees the pre-arbitration
558
+ * behaviour, i.e. every one of these exempt.
559
+ */
560
+ pendingServerStoreWrites: {
561
+ name: string;
562
+ imported: string;
563
+ resolved: string;
564
+ line: number;
565
+ }[];
547
566
  /** Value imports whose specifier resolves to a repo-local `.svelte.ts`/`.svelte.js` runes module (security/shared-state-import). */
548
567
  runesModuleImports: {
549
568
  source: string;
@@ -1252,6 +1271,8 @@ declare const architectureReservedDirectoryNames: Rule;
1252
1271
 
1253
1272
  declare const architectureRouteComponentImport: Rule;
1254
1273
 
1274
+ declare const architectureDocLinkTarget: Rule;
1275
+
1255
1276
  declare const performanceHeavyImport: Rule;
1256
1277
 
1257
1278
  declare const performanceNamespaceImport: Rule;
@@ -1444,6 +1465,8 @@ interface ScoreResult {
1444
1465
  }
1445
1466
  interface ScoreOptions {
1446
1467
  applyCriticalCap?: boolean;
1468
+ /** The rules that ran. Defaults to the selected registry; supplied by tests and custom rule sets. */
1469
+ rules?: readonly Rule[];
1447
1470
  }
1448
1471
  /** Compute the headline score and its breakdown (design §12). */
1449
1472
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
@@ -1473,6 +1496,15 @@ declare function issueOf(result: Result): {
1473
1496
  type JsonIssue = ReturnType<typeof issueOf> & {
1474
1497
  severity: ReturnType<typeof effectiveSeverity>;
1475
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
+ }
1476
1508
  interface JsonReport {
1477
1509
  version: string;
1478
1510
  score: number;
@@ -1482,6 +1514,7 @@ interface JsonReport {
1482
1514
  scoreModel: ScoreModel;
1483
1515
  }>;
1484
1516
  summary: Summary;
1517
+ rules: Record<string, RuleEvidence>;
1485
1518
  routes: Array<{
1486
1519
  route: string;
1487
1520
  score: number;
@@ -1492,11 +1525,11 @@ interface JsonReport {
1492
1525
  /** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
1493
1526
  declare function buildJsonReport(results: Result[], config: Config, meta: {
1494
1527
  version: string;
1495
- }): JsonReport;
1528
+ }, ruleIds?: readonly string[]): JsonReport;
1496
1529
  /** Render results as the documented JSON report string (design §7). */
1497
1530
  declare function formatJsonReport(results: Result[], config: Config, meta: {
1498
1531
  version: string;
1499
- }): string;
1532
+ }, ruleIds?: readonly string[]): string;
1500
1533
 
1501
1534
  /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
1502
1535
  declare function formatAgentReport(results: Result[], config: Config): string;
@@ -1588,4 +1621,4 @@ declare function escapeHtml(s: string): string;
1588
1621
  */
1589
1622
  declare function safeHref(url: string): string | null;
1590
1623
 
1591
- 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) {
@@ -1842,6 +1892,13 @@ function libServerRoot(aliases) {
1842
1892
  if (lib && lib.replacement === null) return void 0;
1843
1893
  return `${lib?.replacement ?? "src/lib"}/server`;
1844
1894
  }
1895
+ function serverRootRelativePath(spec, importerFile, aliases) {
1896
+ const serverRoot = libServerRoot(aliases);
1897
+ if (serverRoot === void 0) return void 0;
1898
+ const path = resolveRepoLocalPath(spec, importerFile, aliases);
1899
+ if (path === void 0) return void 0;
1900
+ return path === serverRoot || path.startsWith(`${serverRoot}/`) ? path : void 0;
1901
+ }
1845
1902
  function isLocalStateSpecifier(spec, importerFile, aliases) {
1846
1903
  const serverRoot = libServerRoot(aliases);
1847
1904
  if (serverRoot === void 0) return false;
@@ -1849,12 +1906,32 @@ function isLocalStateSpecifier(spec, importerFile, aliases) {
1849
1906
  if (path === void 0) return false;
1850
1907
  return path !== serverRoot && !path.startsWith(`${serverRoot}/`);
1851
1908
  }
1909
+ var IN_MEMORY_CTORS = /* @__PURE__ */ new Set(["Map", "Set", "WeakMap", "WeakSet"]);
1910
+ function isInMemoryInit(init) {
1911
+ if (!init) return false;
1912
+ if (init.type === "ObjectExpression" || init.type === "ArrayExpression") return true;
1913
+ return init.type === "NewExpression" && init.callee?.type === "Identifier" && IN_MEMORY_CTORS.has(init.callee.name);
1914
+ }
1915
+ function parseInMemoryExports(source, filename) {
1916
+ const names = /* @__PURE__ */ new Set();
1917
+ const { program } = parseModuleProgram(source, filename);
1918
+ for (const stmt of program?.body ?? []) {
1919
+ if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
1920
+ const decl = stmt.declaration;
1921
+ if (decl.type !== "VariableDeclaration") continue;
1922
+ for (const d of decl.declarations ?? []) {
1923
+ if (d?.id?.type === "Identifier" && isInMemoryInit(d.init)) names.add(d.id.name);
1924
+ }
1925
+ }
1926
+ return names;
1927
+ }
1852
1928
  function parseKitModuleFacts(source, filename, aliases) {
1853
1929
  const suppressions = collectSuppressions(source);
1854
1930
  const { program, wrapped } = parseModuleProgram(source, filename);
1855
1931
  const moduleStateReassignments = [];
1856
1932
  const importedStateWrites = [];
1857
1933
  const importedStateWritesOutsideHandlers = [];
1934
+ const pendingServerStoreWrites = [];
1858
1935
  const runesModuleImports = [];
1859
1936
  const lifecycleCalls = [];
1860
1937
  const browserGlobalRefs = [];
@@ -1863,6 +1940,7 @@ function parseKitModuleFacts(source, filename, aliases) {
1863
1940
  moduleStateReassignments,
1864
1941
  importedStateWrites,
1865
1942
  importedStateWritesOutsideHandlers,
1943
+ pendingServerStoreWrites,
1866
1944
  runesModuleImports,
1867
1945
  lifecycleCalls,
1868
1946
  browserGlobalRefs,
@@ -1872,6 +1950,7 @@ function parseKitModuleFacts(source, filename, aliases) {
1872
1950
  }
1873
1951
  const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
1874
1952
  const importedSpecifiers = /* @__PURE__ */ new Map();
1953
+ const importedNames = /* @__PURE__ */ new Map();
1875
1954
  for (const stmt of program.body ?? []) {
1876
1955
  if (stmt?.type !== "ImportDeclaration" || stmt.importKind === "type") continue;
1877
1956
  const spec = typeof stmt.source?.value === "string" ? stmt.source.value : "";
@@ -1880,6 +1959,10 @@ function parseKitModuleFacts(source, filename, aliases) {
1880
1959
  if (s?.importKind === "type" || s?.local?.type !== "Identifier") continue;
1881
1960
  names.push(s.local.name);
1882
1961
  importedSpecifiers.set(s.local.name, spec);
1962
+ importedNames.set(
1963
+ s.local.name,
1964
+ s.type === "ImportSpecifier" && s.imported?.type === "Identifier" ? s.imported.name : s.local.name
1965
+ );
1883
1966
  }
1884
1967
  if (names.length === 0) continue;
1885
1968
  const resolved = resolveRunesModuleSpecifier(spec, filename, aliases);
@@ -1956,8 +2039,21 @@ function parseKitModuleFacts(source, filename, aliases) {
1956
2039
  const method = n.callee.property?.type === "Identifier" ? n.callee.property.name : void 0;
1957
2040
  if (method === "set" || method === "update") {
1958
2041
  const r = importedRoot(n.callee.object);
1959
- if (r && isLocalStateSpecifier(importedSpecifiers.get(r), filename, aliases))
1960
- write = { name: r, via: "set-call" };
2042
+ const spec = r ? importedSpecifiers.get(r) : void 0;
2043
+ if (r && spec !== void 0) {
2044
+ if (isLocalStateSpecifier(spec, filename, aliases)) write = { name: r, via: "set-call" };
2045
+ else {
2046
+ const resolved = serverRootRelativePath(spec, filename, aliases);
2047
+ if (resolved !== void 0 && inHandler) {
2048
+ pendingServerStoreWrites.push({
2049
+ name: r,
2050
+ imported: importedNames.get(r) ?? r,
2051
+ resolved,
2052
+ line: line(n.start)
2053
+ });
2054
+ }
2055
+ }
2056
+ }
1961
2057
  }
1962
2058
  } else if (n.type === "AssignmentExpression" && (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern")) {
1963
2059
  const scanPatternTargets = (pat) => {
@@ -2005,6 +2101,7 @@ function parseKitModuleFacts(source, filename, aliases) {
2005
2101
  moduleStateReassignments: byLine(moduleStateReassignments),
2006
2102
  importedStateWrites: byLine(importedStateWrites),
2007
2103
  importedStateWritesOutsideHandlers: byLine(importedStateWritesOutsideHandlers),
2104
+ pendingServerStoreWrites: byLine(pendingServerStoreWrites),
2008
2105
  runesModuleImports: byLine(runesModuleImports),
2009
2106
  lifecycleCalls: byLine(lifecycleCalls),
2010
2107
  browserGlobalRefs: byLine(browserGlobalRefs),
@@ -2024,6 +2121,7 @@ function emptyKitModuleFacts(file, kind) {
2024
2121
  moduleStateReassignments: [],
2025
2122
  importedStateWrites: [],
2026
2123
  importedStateWritesOutsideHandlers: [],
2124
+ pendingServerStoreWrites: [],
2027
2125
  runesModuleImports: [],
2028
2126
  lifecycleCalls: [],
2029
2127
  browserGlobalRefs: [],
@@ -2044,7 +2142,7 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
2044
2142
  ];
2045
2143
  const lists = await Promise.all(patterns.map((p) => rt.glob(p, cwd)));
2046
2144
  const files = [...new Set(lists.flat())];
2047
- return Promise.all(
2145
+ const facts = await Promise.all(
2048
2146
  files.sort().map(async (rel) => {
2049
2147
  const kind = kindOf(rel);
2050
2148
  try {
@@ -2055,6 +2153,38 @@ async function collectKitModuleFacts(rt, cwd, aliases) {
2055
2153
  }
2056
2154
  })
2057
2155
  );
2156
+ return arbitrateServerStoreWrites(rt, cwd, facts);
2157
+ }
2158
+ function moduleCandidates(repoPath) {
2159
+ if (repoPath.endsWith(".js")) return [repoPath, `${repoPath.slice(0, -3)}.ts`];
2160
+ if (repoPath.endsWith(".ts")) return [repoPath];
2161
+ return [`${repoPath}.ts`, `${repoPath}.js`, `${repoPath}/index.ts`, `${repoPath}/index.js`];
2162
+ }
2163
+ async function inMemoryExportsOf(rt, cwd, repoPath) {
2164
+ for (const rel of moduleCandidates(repoPath)) {
2165
+ try {
2166
+ if (!await rt.exists(rt.join(cwd, rel))) continue;
2167
+ return parseInMemoryExports(await rt.readFile(rt.join(cwd, rel)), rel);
2168
+ } catch {
2169
+ return /* @__PURE__ */ new Set();
2170
+ }
2171
+ }
2172
+ return /* @__PURE__ */ new Set();
2173
+ }
2174
+ async function arbitrateServerStoreWrites(rt, cwd, facts) {
2175
+ const targets = [...new Set(facts.flatMap((f) => f.pendingServerStoreWrites.map((w) => w.resolved)))];
2176
+ if (targets.length === 0) return facts;
2177
+ const byPath = new Map(
2178
+ await Promise.all(targets.map(async (t) => [t, await inMemoryExportsOf(rt, cwd, t)]))
2179
+ );
2180
+ return facts.map((f) => {
2181
+ const promoted = f.pendingServerStoreWrites.filter((w) => byPath.get(w.resolved)?.has(w.imported)).map((w) => ({ name: w.name, line: w.line, via: "set-call" }));
2182
+ if (promoted.length === 0) return f;
2183
+ return {
2184
+ ...f,
2185
+ importedStateWrites: [...f.importedStateWrites, ...promoted].sort((a, b) => a.line - b.line)
2186
+ };
2187
+ });
2058
2188
  }
2059
2189
 
2060
2190
  // src/config-object.ts
@@ -5336,6 +5466,60 @@ var architectureRouteComponentImport = componentRule({
5336
5466
  }
5337
5467
  });
5338
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
+
5339
5523
  // src/rules/perf/heavy-import.ts
5340
5524
  var HEAVY_PACKAGES = {
5341
5525
  lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
@@ -5552,6 +5736,7 @@ var allRules = [
5552
5736
  architectureDirectoryNaming,
5553
5737
  architectureReservedDirectoryNames,
5554
5738
  architectureRouteComponentImport,
5739
+ architectureDocLinkTarget,
5555
5740
  performanceHeavyImport,
5556
5741
  performanceNamespaceImport,
5557
5742
  performanceMinifyDisabled,
@@ -5612,8 +5797,31 @@ function hasFailureAtOrAbove(summary, min) {
5612
5797
  return order.some((sev, idx) => idx >= threshold && summary[sev] > 0);
5613
5798
  }
5614
5799
 
5615
- // src/scoring/score.ts
5800
+ // src/scoring/inventory.ts
5616
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
5617
5825
  var CRITICAL_CAP = 79;
5618
5826
  function clamp(n) {
5619
5827
  return Math.max(0, Math.min(100, n));
@@ -5621,27 +5829,36 @@ function clamp(n) {
5621
5829
  function computeScore(results, config, options = {}) {
5622
5830
  const routeResults = results.filter((r) => r.route !== void 0);
5623
5831
  const projectResults = results.filter((r) => r.route === void 0);
5624
- const routeScores = /* @__PURE__ */ new Map();
5625
- 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);
5626
5835
  let anyCritical = false;
5627
- const routeRuleMax = /* @__PURE__ */ new Map();
5836
+ const observed = /* @__PURE__ */ new Map();
5837
+ const ruleMax = /* @__PURE__ */ new Map();
5628
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);
5629
5843
  if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
5630
5844
  const sev = effectiveSeverity(r, config);
5631
5845
  if (sev === "critical") anyCritical = true;
5632
- const route = r.route;
5633
- let perRule = routeRuleMax.get(route);
5634
- 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());
5635
5848
  const prev = perRule.get(r.id) ?? 0;
5636
5849
  if (DEDUCTION[sev] > prev) perRule.set(r.id, DEDUCTION[sev]);
5637
5850
  }
5638
- for (const [route, perRule] of routeRuleMax) {
5639
- let deduction = 0;
5640
- for (const d of perRule.values()) deduction += d;
5641
- routeScores.set(route, routeScores.get(route) - deduction);
5642
- }
5643
- const scores = [...routeScores.values()].map(clamp);
5644
- 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;
5645
5862
  const routeAverage = Math.floor(rawRouteAverage);
5646
5863
  const projectRuleMax = /* @__PURE__ */ new Map();
5647
5864
  for (const r of projectResults) {
@@ -5861,9 +6078,20 @@ function issueOf(result) {
5861
6078
  ...result.fix ? { fix: result.fix } : {}
5862
6079
  };
5863
6080
  }
5864
- 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) {
5865
6092
  const { health, categories: byCat, weights } = computeHealth(results, config);
5866
6093
  const summary = summarize(results, config);
6094
+ const rules = ruleEvidence(results, config, ruleIds);
5867
6095
  const categories = Object.fromEntries(
5868
6096
  Object.entries(byCat).map(([cat, sr]) => [cat, { score: sr.score, scoreModel: sr.scoreModel }])
5869
6097
  );
@@ -5879,10 +6107,10 @@ function buildJsonReport(results, config, meta) {
5879
6107
  issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
5880
6108
  }));
5881
6109
  const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
5882
- return { version: meta.version, score: health, weights, categories, summary, routes, siteIssues };
6110
+ return { version: meta.version, score: health, weights, categories, summary, rules, routes, siteIssues };
5883
6111
  }
5884
- function formatJsonReport(results, config, meta) {
5885
- 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);
5886
6114
  }
5887
6115
 
5888
6116
  // src/reporter/agent.ts
@@ -6816,6 +7044,7 @@ export {
6816
7044
  applyRuleSeverities,
6817
7045
  architectureComponentSize,
6818
7046
  architectureDirectoryNaming,
7047
+ architectureDocLinkTarget,
6819
7048
  architecturePrivateScopeImport,
6820
7049
  architecturePropCount,
6821
7050
  architectureReservedDirectoryNames,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.31.1",
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",