@svelte-vitals/core 0.5.0 → 0.6.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
@@ -151,12 +151,18 @@ interface Rule {
151
151
  severity: Severity;
152
152
  /** 'route' = evaluated per route, 'project' = site-wide (design §10, §12). */
153
153
  scope: Scope;
154
+ /** Why this rule matters — one or two sentences, surfaced by explain_rule (issue #24). */
155
+ rationale: string;
156
+ /** Canonical remediation template, shared by findings and explain_rule (issue #24). */
157
+ fix?: Fix;
154
158
  /**
155
159
  * Evaluate the resolved heads. A single rule may return one Result per route,
156
160
  * so it always returns an array. Project-scoped rules return a single element.
157
161
  */
158
162
  check(ctx: RuleContext): Promise<Result[]>;
159
163
  }
164
+ /** Documentation URL for a rule id. Single source so no per-rule URL can drift (issue #24). */
165
+ declare function docsUrlFor(id: string): string;
160
166
  /**
161
167
  * Whether a detection should be penalized by scoring (design §12). Shared by the
162
168
  * future Scorer and by the Slice 0 reporter so pass/fail is decided in one place.
@@ -194,6 +200,18 @@ declare const seo009HtmlLang: Rule;
194
200
 
195
201
  declare const allRules: Rule[];
196
202
 
203
+ interface RuleInfo {
204
+ id: string;
205
+ title: string;
206
+ category: Category;
207
+ severity: Severity;
208
+ rationale: string;
209
+ docsUrl: string;
210
+ fix?: Fix;
211
+ }
212
+ /** Look up a rule's static metadata for the MCP explain_rule tool (issue #24). Rule ids are matched case-insensitively. */
213
+ declare function explainRule(id: string): RuleInfo | undefined;
214
+
197
215
  interface HeadTagRuleOptions {
198
216
  id: string;
199
217
  title: string;
@@ -203,6 +221,8 @@ interface HeadTagRuleOptions {
203
221
  /** Short human label, e.g. 'description'. */
204
222
  label: string;
205
223
  recommendation: string;
224
+ /** Why this rule matters — surfaced by explain_rule (issue #24). */
225
+ rationale: string;
206
226
  /** Agent-actionable remediation attached to every finding (issue #18). */
207
227
  fix?: Fix;
208
228
  }
@@ -239,6 +259,50 @@ interface ConsoleReportOptions {
239
259
  */
240
260
  declare function formatConsoleReport(results: Result[], config: Config, options?: ConsoleReportOptions): string;
241
261
 
262
+ interface ScoreModel {
263
+ routeAverage: number;
264
+ sitePenalty: number;
265
+ /** Headline cap value when it actually lowered the score, else null. */
266
+ criticalCap: number | null;
267
+ }
268
+ interface ScoreResult {
269
+ score: number;
270
+ scoreModel: ScoreModel;
271
+ }
272
+ interface ScoreOptions {
273
+ applyCriticalCap?: boolean;
274
+ }
275
+ /** Compute the headline score and its breakdown (design §12). */
276
+ declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
277
+
278
+ declare function issueOf(result: Result): {
279
+ fix?: Fix | undefined;
280
+ docsUrl?: string | undefined;
281
+ id: string;
282
+ title: string;
283
+ detection: Detection;
284
+ location: string | undefined;
285
+ recommendation: string | undefined;
286
+ };
287
+ type JsonIssue = ReturnType<typeof issueOf> & {
288
+ severity: ReturnType<typeof effectiveSeverity>;
289
+ };
290
+ interface JsonReport {
291
+ version: string;
292
+ score: number;
293
+ scoreModel: ScoreModel;
294
+ summary: Summary;
295
+ routes: Array<{
296
+ route: string;
297
+ score: number;
298
+ issues: JsonIssue[];
299
+ }>;
300
+ siteIssues: JsonIssue[];
301
+ }
302
+ /** Build the structured JSON report object (design §7). Shared by the json reporter and the MCP `analyze` tool (issue #24). */
303
+ declare function buildJsonReport(results: Result[], config: Config, meta: {
304
+ version: string;
305
+ }): JsonReport;
242
306
  /** Render results as the documented JSON report string (design §7). */
243
307
  declare function formatJsonReport(results: Result[], config: Config, meta: {
244
308
  version: string;
@@ -263,20 +327,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
263
327
  /** Apply per-rule severity overrides to results (design §6). */
264
328
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
265
329
 
266
- interface ScoreModel {
267
- routeAverage: number;
268
- sitePenalty: number;
269
- /** Headline cap value when it actually lowered the score, else null. */
270
- criticalCap: number | null;
271
- }
272
- interface ScoreResult {
273
- score: number;
274
- scoreModel: ScoreModel;
275
- }
276
- interface ScoreOptions {
277
- applyCriticalCap?: boolean;
278
- }
279
- /** Compute the headline score and its breakdown (design §12). */
280
- declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
281
-
282
- export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type Result, type Rule, type RuleContext, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, classify, computeScore, defaultConfig, defaultProject, defineConfig, effectiveSeverity, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
330
+ export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildJsonReport, classify, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
package/dist/index.js CHANGED
@@ -27,6 +27,9 @@ var SITEMAP_SOURCE_PATHS = [
27
27
  ];
28
28
 
29
29
  // src/rule.ts
30
+ function docsUrlFor(id) {
31
+ return `https://svelte-vitals.dev/rules/${id}`;
32
+ }
30
33
  function isPenalized(detection, treatDynamicAs) {
31
34
  if (detection.presence === "none") return true;
32
35
  if (detection.value === "absent") return true;
@@ -41,7 +44,11 @@ async function runRules(rules, ctx) {
41
44
  }
42
45
 
43
46
  // src/rules/seo/seo001-title.ts
44
- var DOCS_URL = "https://svelte-vitals.dev/rules/SEO001";
47
+ var FIX = {
48
+ description: "Add a <title> inside <svelte:head> (a dynamic title is fine).",
49
+ snippet: "<svelte:head>\n <title>{data.title}</title>\n</svelte:head>",
50
+ lang: "svelte"
51
+ };
45
52
  function detectTitle(head) {
46
53
  const title = head.tags.find((t) => t.kind === "title");
47
54
  if (!title) {
@@ -60,6 +67,8 @@ var seo001Title = {
60
67
  category: "seo",
61
68
  severity: "critical",
62
69
  scope: "route",
70
+ rationale: "A unique, non-empty <title> is the single strongest on-page SEO signal and the text shown in search results and browser tabs.",
71
+ fix: FIX,
63
72
  async check(ctx) {
64
73
  return ctx.heads.map((head) => {
65
74
  const detection = detectTitle(head);
@@ -71,12 +80,8 @@ var seo001Title = {
71
80
  location: head.file,
72
81
  message: messageFor(detection),
73
82
  recommendation: "Add a <title> inside <svelte:head>, e.g. <title>{data.title}</title>, or set it via your meta component.",
74
- docsUrl: DOCS_URL,
75
- fix: {
76
- description: "Add a <title> inside <svelte:head> (a dynamic title is fine).",
77
- snippet: "<svelte:head>\n <title>{data.title}</title>\n</svelte:head>",
78
- lang: "svelte"
79
- }
83
+ docsUrl: docsUrlFor("SEO001"),
84
+ fix: { ...FIX }
80
85
  };
81
86
  });
82
87
  }
@@ -88,13 +93,15 @@ function detect(head, match) {
88
93
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
89
94
  }
90
95
  function headTagRule(opts) {
91
- const docsUrl = `https://svelte-vitals.dev/rules/${opts.id}`;
96
+ const docsUrl = docsUrlFor(opts.id);
92
97
  return {
93
98
  id: opts.id,
94
99
  title: opts.title,
95
100
  category: "seo",
96
101
  severity: opts.severity,
97
102
  scope: "route",
103
+ rationale: opts.rationale,
104
+ ...opts.fix ? { fix: opts.fix } : {},
98
105
  async check(ctx) {
99
106
  return ctx.heads.map((head) => {
100
107
  const detection = detect(head, opts.match);
@@ -125,6 +132,7 @@ var seo002Description = headTagRule({
125
132
  match: (t) => t.kind === "meta" && t.name === "description",
126
133
  label: '<meta name="description">',
127
134
  recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.',
135
+ rationale: "A meta description is the snippet search engines show under your title; without one they invent one from page text, often poorly.",
128
136
  fix: {
129
137
  description: 'Add a <meta name="description"> inside <svelte:head>, or set description on your meta component.',
130
138
  snippet: '<svelte:head>\n <meta name="description" content="A concise page summary." />\n</svelte:head>',
@@ -138,6 +146,7 @@ var seo003Canonical = headTagRule({
138
146
  match: (t) => t.kind === "link" && t.rel === "canonical",
139
147
  label: '<link rel="canonical">',
140
148
  recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.',
149
+ rationale: "A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query strings and trailing-slash variants.",
141
150
  fix: {
142
151
  description: 'Add <link rel="canonical"> inside <svelte:head>, or set the canonical prop on your meta component.',
143
152
  snippet: '<svelte:head>\n <link rel="canonical" href="https://example.com/this-page" />\n</svelte:head>',
@@ -151,6 +160,7 @@ var seo004OgImage = headTagRule({
151
160
  match: (t) => t.kind === "meta" && t.property === "og:image",
152
161
  label: '<meta property="og:image">',
153
162
  recommendation: 'Add <meta property="og:image">, or set openGraph.images on your meta component.',
163
+ rationale: "og:image is the preview thumbnail shown when the page is shared on social platforms; without it links render bare and get fewer clicks.",
154
164
  fix: {
155
165
  description: 'Add <meta property="og:image">, or set openGraph.images on your meta component.',
156
166
  snippet: '<svelte:head>\n <meta property="og:image" content="https://example.com/og.png" />\n</svelte:head>',
@@ -164,6 +174,7 @@ var seo005OgTitle = headTagRule({
164
174
  match: (t) => t.kind === "meta" && t.property === "og:title",
165
175
  label: '<meta property="og:title">',
166
176
  recommendation: 'Add <meta property="og:title">, or set openGraph.title on your meta component.',
177
+ rationale: "og:title controls the headline shown when the page is shared on social platforms, independent of the document <title>.",
167
178
  fix: {
168
179
  description: 'Add <meta property="og:title">, or set openGraph.title on your meta component.',
169
180
  snippet: '<svelte:head>\n <meta property="og:title" content="Page title" />\n</svelte:head>',
@@ -177,6 +188,7 @@ var seo008JsonLd = headTagRule({
177
188
  match: (t) => t.kind === "jsonld",
178
189
  label: 'JSON-LD (<script type="application/ld+json">)',
179
190
  recommendation: "Add JSON-LD structured data, e.g. via <svelte:head> or a JsonLd component.",
191
+ rationale: "JSON-LD structured data lets search engines render rich results (breadcrumbs, articles, products) for the page.",
180
192
  fix: {
181
193
  // Svelte ships <script> contents verbatim (the body is raw text, not Svelte
182
194
  // markup), so use literal JSON here — an interpolation like {JSON.stringify(...)}
@@ -190,12 +202,19 @@ var seo008JsonLd = headTagRule({
190
202
  // src/rules/seo/project-rules.ts
191
203
  var present = { presence: "own", value: "static" };
192
204
  var absent = { presence: "none", value: "absent" };
205
+ var SEO006_FIX = {
206
+ description: "Create static/robots.txt (or a src/routes/robots.txt/+server endpoint).",
207
+ snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
208
+ lang: "text"
209
+ };
193
210
  var seo006Robots = {
194
211
  id: "SEO006",
195
212
  title: "robots.txt",
196
213
  category: "seo",
197
214
  severity: "warning",
198
215
  scope: "project",
216
+ rationale: "robots.txt tells crawlers which paths they may fetch and points them to your sitemap; missing it leaves crawl behaviour to defaults.",
217
+ fix: SEO006_FIX,
199
218
  async check(ctx) {
200
219
  const detection = ctx.project.hasRobotsTxt ? present : absent;
201
220
  return [
@@ -205,22 +224,25 @@ var seo006Robots = {
205
224
  detection,
206
225
  message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
207
226
  recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
208
- docsUrl: "https://svelte-vitals.dev/rules/SEO006",
209
- fix: {
210
- description: "Create static/robots.txt (or a src/routes/robots.txt/+server endpoint).",
211
- snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
212
- lang: "text"
213
- }
227
+ docsUrl: docsUrlFor("SEO006"),
228
+ fix: { ...SEO006_FIX }
214
229
  }
215
230
  ];
216
231
  }
217
232
  };
233
+ var SEO007_FIX = {
234
+ description: "Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint).",
235
+ snippet: '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://example.com/</loc></url>\n</urlset>',
236
+ lang: "xml"
237
+ };
218
238
  var seo007Sitemap = {
219
239
  id: "SEO007",
220
240
  title: "sitemap.xml",
221
241
  category: "seo",
222
242
  severity: "warning",
223
243
  scope: "project",
244
+ rationale: "A sitemap.xml lists your URLs so search engines can discover and prioritise them, especially pages not well linked internally.",
245
+ fix: SEO007_FIX,
224
246
  async check(ctx) {
225
247
  const detection = ctx.project.hasSitemap ? present : absent;
226
248
  return [
@@ -230,22 +252,25 @@ var seo007Sitemap = {
230
252
  detection,
231
253
  message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
232
254
  recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
233
- docsUrl: "https://svelte-vitals.dev/rules/SEO007",
234
- fix: {
235
- description: "Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint).",
236
- snippet: '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://example.com/</loc></url>\n</urlset>',
237
- lang: "xml"
238
- }
255
+ docsUrl: docsUrlFor("SEO007"),
256
+ fix: { ...SEO007_FIX }
239
257
  }
240
258
  ];
241
259
  }
242
260
  };
261
+ var SEO009_FIX = {
262
+ description: "Set the lang attribute on <html> in src/app.html.",
263
+ snippet: '<html lang="en">',
264
+ lang: "html"
265
+ };
243
266
  var seo009HtmlLang = {
244
267
  id: "SEO009",
245
268
  title: "<html lang>",
246
269
  category: "seo",
247
270
  severity: "warning",
248
271
  scope: "project",
272
+ rationale: "The <html lang> attribute declares the page language for search engines, screen readers, and translation tools.",
273
+ fix: SEO009_FIX,
249
274
  async check(ctx) {
250
275
  const detection = ctx.project.htmlLang;
251
276
  const message = detection.presence === "none" ? "Missing <html lang>" : detection.value === "absent" ? "Empty <html lang>" : "<html lang>";
@@ -256,12 +281,8 @@ var seo009HtmlLang = {
256
281
  detection,
257
282
  message,
258
283
  recommendation: 'Set <html lang="..."> in src/app.html.',
259
- docsUrl: "https://svelte-vitals.dev/rules/SEO009",
260
- fix: {
261
- description: "Set the lang attribute on <html> in src/app.html.",
262
- snippet: '<html lang="en">',
263
- lang: "html"
264
- }
284
+ docsUrl: docsUrlFor("SEO009"),
285
+ fix: { ...SEO009_FIX }
265
286
  }
266
287
  ];
267
288
  }
@@ -279,6 +300,20 @@ var allRules = [
279
300
  seo008JsonLd,
280
301
  seo009HtmlLang
281
302
  ];
303
+ function explainRule(id) {
304
+ const target = id.toUpperCase();
305
+ const rule = allRules.find((r) => r.id === target);
306
+ if (!rule) return void 0;
307
+ return {
308
+ id: rule.id,
309
+ title: rule.title,
310
+ category: rule.category,
311
+ severity: rule.severity,
312
+ rationale: rule.rationale,
313
+ docsUrl: docsUrlFor(rule.id),
314
+ ...rule.fix ? { fix: rule.fix } : {}
315
+ };
316
+ }
282
317
 
283
318
  // src/summary.ts
284
319
  function classify(result, config) {
@@ -432,10 +467,11 @@ function issueOf(result) {
432
467
  detection: result.detection,
433
468
  location: result.location,
434
469
  recommendation: result.recommendation,
470
+ ...result.docsUrl ? { docsUrl: result.docsUrl } : {},
435
471
  ...result.fix ? { fix: result.fix } : {}
436
472
  };
437
473
  }
438
- function formatJsonReport(results, config, meta) {
474
+ function buildJsonReport(results, config, meta) {
439
475
  const { score, scoreModel } = computeScore(results, config);
440
476
  const summary = summarize(results, config);
441
477
  const routeMap = /* @__PURE__ */ new Map();
@@ -450,7 +486,10 @@ function formatJsonReport(results, config, meta) {
450
486
  issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
451
487
  }));
452
488
  const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
453
- return JSON.stringify({ version: meta.version, score, scoreModel, summary, routes, siteIssues }, null, 2);
489
+ return { version: meta.version, score, scoreModel, summary, routes, siteIssues };
490
+ }
491
+ function formatJsonReport(results, config, meta) {
492
+ return JSON.stringify(buildJsonReport(results, config, meta), null, 2);
454
493
  }
455
494
 
456
495
  // src/reporter/agent.ts
@@ -509,11 +548,11 @@ function severityToGithubLevel(sev) {
509
548
  function messageText(result) {
510
549
  return result.recommendation ? `${result.message} ${result.recommendation}` : result.message;
511
550
  }
512
- function docsUrlFor(id) {
551
+ function docsUrlFor2(id) {
513
552
  return `https://svelte-vitals.dev/rules/${id}`;
514
553
  }
515
554
  var RULE_META = new Map(
516
- allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor(r.id) }])
555
+ allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor2(r.id) }])
517
556
  );
518
557
  function ruleMetaById(id) {
519
558
  return RULE_META.get(id);
@@ -533,7 +572,7 @@ function formatSarifReport(results, config, meta) {
533
572
  id: r.id,
534
573
  name,
535
574
  shortDescription: { text: name },
536
- helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor(r.id),
575
+ helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor2(r.id),
537
576
  defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
538
577
  });
539
578
  }
@@ -605,12 +644,15 @@ export {
605
644
  SITEMAP_SOURCE_PATHS,
606
645
  allRules,
607
646
  applyRuleSeverities,
647
+ buildJsonReport,
608
648
  classify,
609
649
  computeScore,
610
650
  defaultConfig,
611
651
  defaultProject,
612
652
  defineConfig,
653
+ docsUrlFor,
613
654
  effectiveSeverity,
655
+ explainRule,
614
656
  formatAgentReport,
615
657
  formatConsoleReport,
616
658
  formatGithubReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",