@svelte-vitals/core 0.4.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,19 +259,6 @@ interface ConsoleReportOptions {
239
259
  */
240
260
  declare function formatConsoleReport(results: Result[], config: Config, options?: ConsoleReportOptions): string;
241
261
 
242
- /** Render results as the documented JSON report string (design §7). */
243
- declare function formatJsonReport(results: Result[], config: Config, meta: {
244
- version: string;
245
- }): string;
246
-
247
- /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
248
- declare function formatAgentReport(results: Result[], config: Config): string;
249
-
250
- /** Drop rules disabled via config (design §6). */
251
- declare function selectRules(rules: Rule[], config: Config): Rule[];
252
- /** Apply per-rule severity overrides to results (design §6). */
253
- declare function applyRuleSeverities(results: Result[], config: Config): Result[];
254
-
255
262
  interface ScoreModel {
256
263
  routeAverage: number;
257
264
  sitePenalty: number;
@@ -268,4 +275,56 @@ interface ScoreOptions {
268
275
  /** Compute the headline score and its breakdown (design §12). */
269
276
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
270
277
 
271
- 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, formatJsonReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
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;
306
+ /** Render results as the documented JSON report string (design §7). */
307
+ declare function formatJsonReport(results: Result[], config: Config, meta: {
308
+ version: string;
309
+ }): string;
310
+
311
+ /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
312
+ declare function formatAgentReport(results: Result[], config: Config): string;
313
+
314
+ /** Render penalized findings as a SARIF 2.1.0 log string (issue #18, design slice 5). */
315
+ declare function formatSarifReport(results: Result[], config: Config, meta: {
316
+ version: string;
317
+ }): string;
318
+
319
+ /**
320
+ * Render penalized findings as GitHub Actions workflow commands (issue #18, design slice 5).
321
+ * GitHub turns these into inline PR annotations and run-annotation entries. Returns '' when clean.
322
+ */
323
+ declare function formatGithubReport(results: Result[], config: Config): string;
324
+
325
+ /** Drop rules disabled via config (design §6). */
326
+ declare function selectRules(rules: Rule[], config: Config): Rule[];
327
+ /** Apply per-rule severity overrides to results (design §6). */
328
+ declare function applyRuleSeverities(results: Result[], config: Config): Result[];
329
+
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
@@ -499,6 +538,97 @@ function formatAgentReport(results, config) {
499
538
  return lines.join("\n").replace(/\n+$/, "\n");
500
539
  }
501
540
 
541
+ // src/reporter/shared.ts
542
+ function severityToSarifLevel(sev) {
543
+ return sev === "critical" ? "error" : sev === "warning" ? "warning" : "note";
544
+ }
545
+ function severityToGithubLevel(sev) {
546
+ return sev === "critical" ? "error" : sev === "warning" ? "warning" : "notice";
547
+ }
548
+ function messageText(result) {
549
+ return result.recommendation ? `${result.message} ${result.recommendation}` : result.message;
550
+ }
551
+ function docsUrlFor2(id) {
552
+ return `https://svelte-vitals.dev/rules/${id}`;
553
+ }
554
+ var RULE_META = new Map(
555
+ allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor2(r.id) }])
556
+ );
557
+ function ruleMetaById(id) {
558
+ return RULE_META.get(id);
559
+ }
560
+
561
+ // src/reporter/sarif.ts
562
+ function formatSarifReport(results, config, meta) {
563
+ const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
564
+ const rules = [];
565
+ const ruleIndex = /* @__PURE__ */ new Map();
566
+ const sarifResults = penalized.map((r) => {
567
+ if (!ruleIndex.has(r.id)) {
568
+ const m = ruleMetaById(r.id);
569
+ const name = m?.title ?? r.id;
570
+ ruleIndex.set(r.id, rules.length);
571
+ rules.push({
572
+ id: r.id,
573
+ name,
574
+ shortDescription: { text: name },
575
+ helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor2(r.id),
576
+ defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
577
+ });
578
+ }
579
+ const result = {
580
+ ruleId: r.id,
581
+ ruleIndex: ruleIndex.get(r.id),
582
+ level: severityToSarifLevel(effectiveSeverity(r, config)),
583
+ message: { text: messageText(r) },
584
+ partialFingerprints: { "svelteVitals/v1": `${r.id}:${r.route ?? "project"}` }
585
+ };
586
+ if (r.location) {
587
+ result.locations = [{ physicalLocation: { artifactLocation: { uri: r.location } } }];
588
+ }
589
+ return result;
590
+ });
591
+ const log = {
592
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
593
+ version: "2.1.0",
594
+ runs: [
595
+ {
596
+ tool: {
597
+ driver: {
598
+ name: "svelte-vitals",
599
+ informationUri: "https://svelte-vitals.dev",
600
+ version: meta.version,
601
+ rules
602
+ }
603
+ },
604
+ results: sarifResults
605
+ }
606
+ ]
607
+ };
608
+ return JSON.stringify(log, null, 2);
609
+ }
610
+
611
+ // src/reporter/github.ts
612
+ function escapeData(s) {
613
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
614
+ }
615
+ function escapeProp(s) {
616
+ return escapeData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
617
+ }
618
+ function formatGithubReport(results, config) {
619
+ const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
620
+ const lines = penalized.map((r) => {
621
+ const level = severityToGithubLevel(effectiveSeverity(r, config));
622
+ const meta = ruleMetaById(r.id);
623
+ const title = meta ? `${r.id}: ${meta.title}` : r.id;
624
+ const props = [];
625
+ if (r.location) props.push(`file=${escapeProp(r.location)}`);
626
+ props.push(`title=${escapeProp(title)}`);
627
+ return `::${level} ${props.join(",")}::${escapeData(messageText(r))}`;
628
+ });
629
+ return lines.join("\n");
630
+ }
631
+
502
632
  // src/config-apply.ts
503
633
  function selectRules(rules, config) {
504
634
  return rules.filter((rule) => config.rules[rule.id] !== "off");
@@ -514,15 +644,20 @@ export {
514
644
  SITEMAP_SOURCE_PATHS,
515
645
  allRules,
516
646
  applyRuleSeverities,
647
+ buildJsonReport,
517
648
  classify,
518
649
  computeScore,
519
650
  defaultConfig,
520
651
  defaultProject,
521
652
  defineConfig,
653
+ docsUrlFor,
522
654
  effectiveSeverity,
655
+ explainRule,
523
656
  formatAgentReport,
524
657
  formatConsoleReport,
658
+ formatGithubReport,
525
659
  formatJsonReport,
660
+ formatSarifReport,
526
661
  hasFailureAtOrAbove,
527
662
  headTagRule,
528
663
  isPenalized,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.4.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",