@svelte-vitals/core 0.33.0 → 0.35.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
@@ -1462,6 +1462,10 @@ interface ScoreResult {
1462
1462
  */
1463
1463
  rawScore: number;
1464
1464
  scoreModel: ScoreModel;
1465
+ /** Keys this result set touched. */
1466
+ keys: number;
1467
+ /** Keys carrying at least one penalized finding. */
1468
+ affectedKeys: number;
1465
1469
  }
1466
1470
  interface ScoreOptions {
1467
1471
  applyCriticalCap?: boolean;
@@ -1471,7 +1475,7 @@ interface ScoreOptions {
1471
1475
  /** Compute the headline score and its breakdown (design §12). */
1472
1476
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
1473
1477
  /** Compute an independent score per category present in `results` (issue #10). */
1474
- declare function scoresByCategory(results: Result[], config: Config): Partial<Record<Category, ScoreResult>>;
1478
+ declare function scoresByCategory(results: Result[], config: Config, options?: ScoreOptions): Partial<Record<Category, ScoreResult>>;
1475
1479
  interface HealthResult {
1476
1480
  /** Weighted overall score across present categories (0–100). */
1477
1481
  health: number;
@@ -1512,15 +1516,26 @@ interface JsonReport {
1512
1516
  categories: Record<string, {
1513
1517
  score: number;
1514
1518
  scoreModel: ScoreModel;
1519
+ keys: number;
1520
+ affectedKeys: number;
1515
1521
  }>;
1516
1522
  summary: Summary;
1517
1523
  rules: Record<string, RuleEvidence>;
1518
1524
  routes: Array<{
1519
1525
  route: string;
1520
1526
  score: number;
1527
+ categories: Record<string, number>;
1521
1528
  issues: JsonIssue[];
1522
1529
  }>;
1523
1530
  siteIssues: JsonIssue[];
1531
+ /**
1532
+ * Floored severity weight per `"<category>::<scope>"` pair. Reproduces a `routes[].categories` entry
1533
+ * (`100 - 100 * failed / inventories[pair]`): a key is either a route id or a source file path, and
1534
+ * those two key spaces never overlap, so a category's results on one key always draw on a single scope.
1535
+ * It does not reproduce `routes[].score`: a route spanning more than one pair sums their *raw* weights
1536
+ * and floors that sum once, so adding this map's already-floored entries and re-dividing can disagree.
1537
+ */
1538
+ inventories: Record<string, number>;
1524
1539
  }
1525
1540
  /** Build the structured JSON report object (design §7). The shape the `json` reporter emits (issue #24). */
1526
1541
  declare function buildJsonReport(results: Result[], config: Config, meta: {
package/dist/index.js CHANGED
@@ -5823,6 +5823,7 @@ function ruleScopes(rules) {
5823
5823
 
5824
5824
  // src/scoring/score.ts
5825
5825
  var CRITICAL_CAP = 79;
5826
+ var INVENTORY_FLOOR = 25;
5826
5827
  function clamp(n) {
5827
5828
  return Math.max(0, Math.min(100, n));
5828
5829
  }
@@ -5848,14 +5849,16 @@ function computeScore(results, config, options = {}) {
5848
5849
  const prev = perRule.get(r.id) ?? 0;
5849
5850
  if (DEDUCTION[sev] > prev) perRule.set(r.id, DEDUCTION[sev]);
5850
5851
  }
5852
+ let affectedKeys = 0;
5851
5853
  let totalDeficit = 0;
5852
5854
  for (const [key, pairs] of observed) {
5853
5855
  let failed = 0;
5854
5856
  for (const d of ruleMax.get(key)?.values() ?? []) failed += d;
5857
+ if (failed > 0) affectedKeys += 1;
5855
5858
  let inventoryWeight = 0;
5856
5859
  for (const p of pairs) inventoryWeight += inventory.get(p) ?? 0;
5857
- inventoryWeight = Math.max(inventoryWeight, failed);
5858
- totalDeficit += inventoryWeight === 0 ? 0 : 100 * failed / inventoryWeight;
5860
+ inventoryWeight = Math.max(inventoryWeight, failed, INVENTORY_FLOOR);
5861
+ totalDeficit += 100 * failed / inventoryWeight;
5859
5862
  }
5860
5863
  const keyCount = observed.size;
5861
5864
  const rawRouteAverage = keyCount === 0 ? 100 : 100 - totalDeficit / keyCount;
@@ -5875,9 +5878,15 @@ function computeScore(results, config, options = {}) {
5875
5878
  const capBinds = applyCap && anyCritical && rawUncapped > CRITICAL_CAP;
5876
5879
  const criticalCap = capBinds ? CRITICAL_CAP : null;
5877
5880
  const rawScore = clamp(capBinds ? CRITICAL_CAP : rawUncapped);
5878
- return { score: Math.floor(rawScore), rawScore, scoreModel: { routeAverage, sitePenalty, criticalCap } };
5881
+ return {
5882
+ score: Math.floor(rawScore),
5883
+ rawScore,
5884
+ scoreModel: { routeAverage, sitePenalty, criticalCap },
5885
+ keys: keyCount,
5886
+ affectedKeys
5887
+ };
5879
5888
  }
5880
- function scoresByCategory(results, config) {
5889
+ function scoresByCategory(results, config, options = {}) {
5881
5890
  const byCat = /* @__PURE__ */ new Map();
5882
5891
  for (const r of results) {
5883
5892
  const cat = r.category ?? "seo";
@@ -5886,7 +5895,7 @@ function scoresByCategory(results, config) {
5886
5895
  bucket.push(r);
5887
5896
  }
5888
5897
  const out = {};
5889
- for (const [cat, rs] of byCat) out[cat] = computeScore(rs, config);
5898
+ for (const [cat, rs] of byCat) out[cat] = computeScore(rs, config, options);
5890
5899
  return out;
5891
5900
  }
5892
5901
  function computeHealth(results, config) {
@@ -6093,7 +6102,10 @@ function buildJsonReport(results, config, meta, ruleIds) {
6093
6102
  const summary = summarize(results, config);
6094
6103
  const rules = ruleEvidence(results, config, ruleIds);
6095
6104
  const categories = Object.fromEntries(
6096
- Object.entries(byCat).map(([cat, sr]) => [cat, { score: sr.score, scoreModel: sr.scoreModel }])
6105
+ Object.entries(byCat).map(([cat, sr]) => [
6106
+ cat,
6107
+ { score: sr.score, scoreModel: sr.scoreModel, keys: sr.keys, affectedKeys: sr.affectedKeys }
6108
+ ])
6097
6109
  );
6098
6110
  const routeMap = /* @__PURE__ */ new Map();
6099
6111
  for (const r of results) {
@@ -6104,10 +6116,18 @@ function buildJsonReport(results, config, meta, ruleIds) {
6104
6116
  const routes = [...routeMap.values()].sort((a, b) => a.route.localeCompare(b.route)).map(({ route, results: rs }) => ({
6105
6117
  route,
6106
6118
  score: computeScore(rs, config, { applyCriticalCap: false }).score,
6119
+ // Per category, scored against that category's own inventory, so this is not guaranteed to average to
6120
+ // `score` — which is one ratio over the union of the pairs the route touched.
6121
+ categories: Object.fromEntries(
6122
+ Object.entries(scoresByCategory(rs, config, { applyCriticalCap: false })).map(([cat, sr]) => [cat, sr.score])
6123
+ ),
6107
6124
  issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
6108
6125
  }));
6109
6126
  const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
6110
- return { version: meta.version, score: health, weights, categories, summary, rules, routes, siteIssues };
6127
+ const inventories = Object.fromEntries(
6128
+ [...buildInventory(config)].map(([pair, weight]) => [pair, Math.max(weight, INVENTORY_FLOOR)])
6129
+ );
6130
+ return { version: meta.version, score: health, weights, categories, summary, rules, routes, siteIssues, inventories };
6111
6131
  }
6112
6132
  function formatJsonReport(results, config, meta, ruleIds) {
6113
6133
  return JSON.stringify(buildJsonReport(results, config, meta, ruleIds), null, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",