ax-audit 2.3.0 → 3.0.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +90 -18
  3. package/dist/baseline.d.ts +22 -0
  4. package/dist/baseline.d.ts.map +1 -0
  5. package/dist/baseline.js +108 -0
  6. package/dist/baseline.js.map +1 -0
  7. package/dist/checks/agent-json.d.ts +10 -0
  8. package/dist/checks/agent-json.d.ts.map +1 -1
  9. package/dist/checks/agent-json.js +66 -2
  10. package/dist/checks/agent-json.js.map +1 -1
  11. package/dist/checks/html-rendering.d.ts +21 -0
  12. package/dist/checks/html-rendering.d.ts.map +1 -0
  13. package/dist/checks/html-rendering.js +204 -0
  14. package/dist/checks/html-rendering.js.map +1 -0
  15. package/dist/checks/html-utils.d.ts +37 -0
  16. package/dist/checks/html-utils.d.ts.map +1 -0
  17. package/dist/checks/html-utils.js +93 -0
  18. package/dist/checks/html-utils.js.map +1 -0
  19. package/dist/checks/http-headers.js +1 -1
  20. package/dist/checks/http-headers.js.map +1 -1
  21. package/dist/checks/index.d.ts.map +1 -1
  22. package/dist/checks/index.js +10 -0
  23. package/dist/checks/index.js.map +1 -1
  24. package/dist/checks/llms-txt.d.ts.map +1 -1
  25. package/dist/checks/llms-txt.js +17 -2
  26. package/dist/checks/llms-txt.js.map +1 -1
  27. package/dist/checks/mcp.d.ts.map +1 -1
  28. package/dist/checks/mcp.js +11 -2
  29. package/dist/checks/mcp.js.map +1 -1
  30. package/dist/checks/meta-tags.d.ts +13 -0
  31. package/dist/checks/meta-tags.d.ts.map +1 -1
  32. package/dist/checks/meta-tags.js +104 -24
  33. package/dist/checks/meta-tags.js.map +1 -1
  34. package/dist/checks/openapi.d.ts.map +1 -1
  35. package/dist/checks/openapi.js +11 -2
  36. package/dist/checks/openapi.js.map +1 -1
  37. package/dist/checks/robots-txt.d.ts.map +1 -1
  38. package/dist/checks/robots-txt.js +5 -2
  39. package/dist/checks/robots-txt.js.map +1 -1
  40. package/dist/checks/security-txt.js +1 -1
  41. package/dist/checks/seo-basics.d.ts +13 -0
  42. package/dist/checks/seo-basics.d.ts.map +1 -0
  43. package/dist/checks/seo-basics.js +222 -0
  44. package/dist/checks/seo-basics.js.map +1 -0
  45. package/dist/checks/sitemap.d.ts +12 -0
  46. package/dist/checks/sitemap.d.ts.map +1 -0
  47. package/dist/checks/sitemap.js +241 -0
  48. package/dist/checks/sitemap.js.map +1 -0
  49. package/dist/checks/structured-data.js +1 -1
  50. package/dist/checks/structured-data.js.map +1 -1
  51. package/dist/checks/tls-https.d.ts +13 -0
  52. package/dist/checks/tls-https.d.ts.map +1 -0
  53. package/dist/checks/tls-https.js +164 -0
  54. package/dist/checks/tls-https.js.map +1 -0
  55. package/dist/checks/utils.d.ts +13 -1
  56. package/dist/checks/utils.d.ts.map +1 -1
  57. package/dist/checks/utils.js +28 -0
  58. package/dist/checks/utils.js.map +1 -1
  59. package/dist/checks/well-known-ai.d.ts +17 -0
  60. package/dist/checks/well-known-ai.d.ts.map +1 -0
  61. package/dist/checks/well-known-ai.js +123 -0
  62. package/dist/checks/well-known-ai.js.map +1 -0
  63. package/dist/cli.d.ts.map +1 -1
  64. package/dist/cli.js +47 -1
  65. package/dist/cli.js.map +1 -1
  66. package/dist/constants.d.ts +19 -0
  67. package/dist/constants.d.ts.map +1 -1
  68. package/dist/constants.js +47 -10
  69. package/dist/constants.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +1 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/reporter/html.d.ts +2 -2
  75. package/dist/reporter/html.d.ts.map +1 -1
  76. package/dist/reporter/html.js +63 -7
  77. package/dist/reporter/html.js.map +1 -1
  78. package/dist/reporter/index.d.ts +2 -2
  79. package/dist/reporter/index.d.ts.map +1 -1
  80. package/dist/reporter/index.js +4 -4
  81. package/dist/reporter/index.js.map +1 -1
  82. package/dist/reporter/json.d.ts +2 -2
  83. package/dist/reporter/json.d.ts.map +1 -1
  84. package/dist/reporter/json.js +3 -2
  85. package/dist/reporter/json.js.map +1 -1
  86. package/dist/reporter/terminal.d.ts +2 -2
  87. package/dist/reporter/terminal.d.ts.map +1 -1
  88. package/dist/reporter/terminal.js +42 -3
  89. package/dist/reporter/terminal.js.map +1 -1
  90. package/dist/types.d.ts +27 -0
  91. package/dist/types.d.ts.map +1 -1
  92. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  All notable changes to ax-audit are documented here.
4
4
 
5
+ ## [3.0.0] - 2026-04-30
6
+
7
+ ### Added — five new checks (full agent-optimization coverage)
8
+
9
+ - **html-rendering** (weight 9%): detects whether the static HTML response actually contains content, since most AI crawlers (GPTBot, ClaudeBot, CCBot, …) do not execute JavaScript. Heuristics: text length, word count, text-to-markup ratio, empty SPA mount points (`#root`, `#__next`, `#__nuxt`, `#app`, `#svelte`, `#gatsby`), semantic landmarks (`<main>`, `<article>`, `<header>`, `<footer>`, `<nav>`), single `<h1>`, `<noscript>` fallback, and `<img alt>` coverage.
10
+ - **sitemap** (weight 4%): locates the sitemap via `robots.txt` `Sitemap:` directive or `/sitemap.xml`, validates XML shape, parses `<urlset>` and `<sitemapindex>`, samples child sitemaps from indexes, scores `<lastmod>` coverage and freshness (>365d → stale), enforces 50k-URL / 50MB limits.
11
+ - **seo-basics** (weight 7%): `<title>` length 20–70, `<meta name="description">` length 70–160, `<link rel="canonical">` (absolute, single), `<html lang>` (BCP 47), `<meta charset="utf-8">`, `<meta name="viewport">`, hreflang completeness with `x-default`. Title/description duplication detection.
12
+ - **tls-https** (weight 5%): site is served over HTTPS, HTTP redirects to HTTPS, HSTS `max-age` >= 6 months (1 year for preload), `includeSubDomains`, `preload` directive eligibility per https://hstspreload.org.
13
+ - **well-known-ai** (weight 3%): emerging AI-specific discovery files — `/.well-known/ai.txt` (Spawning), `/.well-known/genai.txt`, `/ai-plugin.json` (legacy ChatGPT plugin), `/agents.json` (Wildcard / OpenAgents), `/.well-known/nlweb.json` (Microsoft NLWeb). Each present file scores; coverage is bonus rather than baseline.
14
+
15
+ ### Improved — existing checks
16
+
17
+ - **meta-tags**: now validates Open Graph completeness (`og:title`, `og:description`, `og:url`, `og:type`, `og:image`, `og:site_name`) and Twitter Card completeness (`twitter:card`, `twitter:title`, `twitter:description`, `twitter:image`). Reuses shared HTML utilities for tag matching.
18
+ - **agent-json**: validates the `url` field is absolute and matches the audited origin, and that every `skills[]` entry has both `id` and `description`.
19
+ - **llms-txt / agent-json / mcp / openapi**: validate `Content-Type` of the fetched resource (`text/plain` / `text/markdown` for llms.txt; `application/json` for the JSON manifests). Penalty: −5 per mismatch.
20
+ - **robots-txt**: `CORE_AI_CRAWLERS` extended (now 8 entries: GPTBot, ClaudeBot, ChatGPT-User, Claude-SearchBot, Google-Extended, PerplexityBot, OAI-SearchBot, CCBot). `ALL_AI_CRAWLERS` extended with MistralAI-User, KagiBot, GeminiBot, Goose, AwarioBot family, Bingbot, ImagesiftBot, omgili, Webzio-Extended, and others (47 known crawlers total).
21
+
22
+ ### Refactored
23
+
24
+ - New shared module `src/checks/html-utils.ts` with regex-based primitives for HTML inspection (`getMetaContent`, `findLinkTags`, `findMetaTagsByPrefix`, `extractVisibleText`, `countExecutableScripts`, `getTagAttribute`, …). Eliminates duplicated regex code across `meta-tags`, `seo-basics`, `html-rendering`, and `structured-data`.
25
+ - New shared utility `checkContentType` in `src/checks/utils.ts` for consistent Content-Type validation.
26
+
27
+ ### Scoring
28
+
29
+ - Weights redistributed across 14 checks, total still sums to 100. New highest-weight signals are llms-txt and robots-txt (11% each) followed by html-rendering / structured-data / http-headers (9%).
30
+
31
+ ### Tests
32
+
33
+ - 198 tests total (77 new). New suites: html-rendering (14), sitemap (12), seo-basics (19), tls-https (11), well-known-ai (8). Plus expanded meta-tags / agent-json / mcp / openapi / llms-txt suites for the new validations.
34
+
35
+ ### Breaking
36
+
37
+ - Score deltas vs v2.x are expected on the same site because (a) weights were redistributed across 14 checks instead of 9, and (b) Content-Type validation on `/llms.txt` and the `.well-known` JSON manifests now applies a −5 penalty per mismatch. Sites previously scoring 100 may drop a few points until the new signals are addressed. Use `--baseline` to track regressions explicitly.
38
+
39
+ ## [2.4.0] - 2026-04-16
40
+
41
+ ### Added
42
+
43
+ - **Baseline comparison**: `--save-baseline <path>` saves audit results as a baseline JSON file; `--baseline <path>` compares against a previous baseline and shows per-check score deltas (▲/▼) in terminal, JSON, and HTML output
44
+ - **Regression gate**: `--fail-on-regression <points>` exits with code 1 if any individual check regresses by more than the specified threshold — ideal for CI/CD quality gates
45
+ - **Programmatic API**: new `saveBaseline()`, `loadBaseline()`, `diffBaseline()`, and `toBaselineData()` exports with full TypeScript types (`BaselineData`, `BaselineDiff`, `CheckDiff`)
46
+ - **15 new tests** for baseline save/load/diff logic, including edge cases for missing files, invalid JSON, removed checks, and mixed regressions/improvements
47
+
48
+ ### Fixed
49
+
50
+ - **Test runner glob**: `npm test` now correctly discovers test files in both `test/` root and subdirectories
51
+
5
52
  ## [2.0.0] - 2026-02-27
6
53
 
7
54
  ### Added
package/README.md CHANGED
@@ -19,16 +19,28 @@ npx ax-audit https://your-site.com
19
19
  AX Audit Report
20
20
  https://lucioduran.com
21
21
 
22
- ███████████████████████████████████████░ 98/100 Excellent
22
+ ███████████████████████████████████░░░░░ 88/100 Good
23
23
 
24
24
  LLMs.txt (100/100)
25
25
  PASS /llms.txt exists
26
+ PASS /llms.txt Content-Type OK (text/plain)
26
27
  PASS H1 heading: "Lucio Duran — Personal Portfolio"
27
28
  PASS /llms-full.txt also available (bonus)
28
29
 
29
30
  Robots.txt (100/100)
30
- PASS All 6 core AI crawlers explicitly configured
31
- PASS 31/31 known AI crawlers have explicit rules
31
+ PASS All 8 core AI crawlers explicitly configured
32
+ PASS 32/47 known AI crawlers have explicit rules
33
+
34
+ HTML Rendering (90/100)
35
+ PASS Server-rendered content detected (473 words)
36
+ PASS Semantic landmarks present (main, article, header, footer, nav)
37
+ PASS Single <h1> heading
38
+ PASS 3/3 <img> tags have alt attributes
39
+
40
+ TLS / HTTPS (100/100)
41
+ PASS Site is served over HTTPS
42
+ PASS HTTP requests redirect to HTTPS
43
+ PASS HSTS preload-eligible
32
44
  ...
33
45
  ```
34
46
 
@@ -40,15 +52,20 @@ AI agents and LLMs are increasingly crawling, indexing, and interacting with web
40
52
 
41
53
  | Check | What it audits | Weight |
42
54
  |---|---|---|
43
- | **LLMs.txt** | `/llms.txt` presence and [llmstxt.org](https://llmstxt.org) spec compliance | 15% |
44
- | **Robots.txt** | AI crawler configuration, wildcard detection, partial path restrictions | 15% |
45
- | **Structured Data** | JSON-LD on homepage (schema.org, `@graph`, entity types) | 13% |
46
- | **HTTP Headers** | Security headers + AI discovery `Link` headers + CORS on `.well-known` | 13% |
47
- | **Agent Card** | `/.well-known/agent.json` [A2A protocol](https://a2a-protocol.org) compliance | 10% |
48
- | **MCP** | `/.well-known/mcp.json` [Model Context Protocol](https://modelcontextprotocol.io) server config | 10% |
49
- | **Security.txt** | `/.well-known/security.txt` [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116) compliance | 8% |
50
- | **Meta Tags** | AI meta tags (`ai:*`), `rel="alternate"` to llms.txt, `rel="me"` identity links | 8% |
51
- | **OpenAPI** | `/.well-known/openapi.json` presence and schema validity | 8% |
55
+ | **LLMs.txt** | `/llms.txt` presence, [llmstxt.org](https://llmstxt.org) spec, Content-Type | 11% |
56
+ | **Robots.txt** | AI crawler configuration (40+ known crawlers), wildcard detection, partial path restrictions | 11% |
57
+ | **HTML Rendering** | Server-rendered content, semantic landmarks, SPA-shell detection, alt coverage | 9% |
58
+ | **Structured Data** | JSON-LD on homepage (schema.org, `@graph`, entity types) | 9% |
59
+ | **HTTP Headers** | Security headers + AI discovery `Link` headers + CORS on `.well-known` | 9% |
60
+ | **Agent Card** | `/.well-known/agent.json` [A2A protocol](https://a2a-protocol.org) + same-origin url + skill quality | 7% |
61
+ | **MCP** | `/.well-known/mcp.json` [Model Context Protocol](https://modelcontextprotocol.io) server config | 7% |
62
+ | **SEO Basics** | `<title>`, meta description, canonical, `<html lang>`, charset, viewport, hreflang | 7% |
63
+ | **Security.txt** | `/.well-known/security.txt` [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116) compliance | 6% |
64
+ | **Meta Tags** | AI meta tags (`ai:*`), `rel="alternate"`, `rel="me"`, OpenGraph + Twitter Card completeness | 6% |
65
+ | **OpenAPI** | `/.well-known/openapi.json` presence, schema validity, Content-Type | 6% |
66
+ | **TLS / HTTPS** | HTTPS, HTTP→HTTPS redirect, HSTS with `preload` + `includeSubDomains` | 5% |
67
+ | **Sitemap** | `sitemap.xml` (or `Sitemap:` from robots.txt) — XML validity, `<lastmod>` coverage, freshness, sitemap-index handling | 4% |
68
+ | **AI Well-Known** | Emerging files: `/.well-known/ai.txt`, `genai.txt`, `ai-plugin.json`, `agents.json`, `nlweb.json` | 3% |
52
69
 
53
70
  ## Install
54
71
 
@@ -88,8 +105,51 @@ ax-audit https://example.com --verbose
88
105
 
89
106
  # Only show failures and warnings (hide passing findings)
90
107
  ax-audit https://example.com --only-failures
108
+
109
+ # Save a baseline for future comparison
110
+ ax-audit https://example.com --save-baseline baseline.json
111
+
112
+ # Compare against a baseline — shows per-check score deltas
113
+ ax-audit https://example.com --baseline baseline.json
114
+
115
+ # Fail CI if any check regresses by more than 5 points
116
+ ax-audit https://example.com --baseline baseline.json --fail-on-regression 5
117
+ ```
118
+
119
+ ### Baseline Comparison
120
+
121
+ Track score changes over time by saving a baseline and comparing against it in subsequent runs:
122
+
123
+ ```bash
124
+ # First run — save the baseline
125
+ ax-audit https://example.com --save-baseline .ax-baseline.json
126
+
127
+ # Later — compare against the baseline
128
+ ax-audit https://example.com --baseline .ax-baseline.json
129
+ ```
130
+
131
+ ```
132
+ AX Audit Report
133
+ https://example.com
134
+ Baseline: 2026-04-15T12:00:00.000Z
135
+
136
+ ████████████████████████████████░░░░░░░░ 82/100 Good ▲7
137
+
138
+ LLMs.txt (100/100) ▲20
139
+ Robots.txt (70/100) ▼10
140
+ ...
141
+
142
+ Regressions
143
+ Robots.txt: 80 → 70 (▼10)
144
+
145
+ Improvements
146
+ LLMs.txt: 80 → 100 (▲20)
91
147
  ```
92
148
 
149
+ Works with all output formats (terminal, JSON, HTML). In JSON mode, a `baselineDiff` object is included with per-check deltas.
150
+
151
+ Use `--fail-on-regression <points>` in CI to fail the build if any individual check drops by more than the specified threshold.
152
+
93
153
  ### Batch Mode
94
154
 
95
155
  Pass multiple URLs to audit them sequentially. Each gets its own full report, followed by a summary table:
@@ -141,7 +201,7 @@ console.log(batch.summary.averageScore); // Average across all URLs
141
201
  console.log(batch.summary.passed); // Number of URLs scoring >= 70
142
202
  ```
143
203
 
144
- Also exports `calculateOverallScore`, `getGrade`, and `checks` for advanced usage.
204
+ Also exports `calculateOverallScore`, `getGrade`, `checks`, `saveBaseline`, `loadBaseline`, `diffBaseline`, and `toBaselineData` for advanced usage.
145
205
 
146
206
  ## Scoring
147
207
 
@@ -178,19 +238,31 @@ Save the report as an artifact:
178
238
  path: ax-report.json
179
239
  ```
180
240
 
241
+ Fail on regressions using a committed baseline:
242
+
243
+ ```yaml
244
+ - name: AX Audit (regression gate)
245
+ run: npx ax-audit https://your-site.com --baseline .ax-baseline.json --fail-on-regression 5
246
+ ```
247
+
181
248
  ## Available Checks
182
249
 
183
250
  | Check ID | Use with `--checks` |
184
251
  |---|---|
185
- | `llms-txt` | LLMs.txt spec compliance |
186
- | `robots-txt` | AI crawler configuration |
252
+ | `llms-txt` | LLMs.txt spec + Content-Type |
253
+ | `robots-txt` | AI crawler configuration (40+ crawlers) |
254
+ | `html-rendering` | SSR / SPA-shell detection + semantic HTML |
187
255
  | `structured-data` | JSON-LD structured data |
188
256
  | `http-headers` | Security + AI discovery headers |
189
- | `agent-json` | A2A Agent Card |
257
+ | `agent-json` | A2A Agent Card + same-origin validation |
190
258
  | `mcp` | MCP server configuration |
259
+ | `seo-basics` | title / description / canonical / lang / hreflang |
191
260
  | `security-txt` | RFC 9116 Security.txt |
192
- | `meta-tags` | AI meta tags and identity links |
261
+ | `meta-tags` | AI meta tags + OpenGraph + Twitter Card |
193
262
  | `openapi` | OpenAPI specification |
263
+ | `tls-https` | HTTPS + HTTP→HTTPS redirect + HSTS preload |
264
+ | `sitemap` | sitemap.xml validation + freshness |
265
+ | `well-known-ai` | Emerging AI discovery files |
194
266
 
195
267
  ## Testing
196
268
 
@@ -198,7 +270,7 @@ Save the report as an artifact:
198
270
  npm test
199
271
  ```
200
272
 
201
- 97 tests covering all 9 checks, the scorer, and edge cases. Uses Node.js built-in test runner (`node:test`), no extra test dependencies.
273
+ 198 tests covering all 14 checks, the scorer, baseline comparison, HTML parsing utilities, and edge cases. Uses Node.js built-in test runner (`node:test`), no extra test dependencies.
202
274
 
203
275
  ## Tech Stack
204
276
 
@@ -0,0 +1,22 @@
1
+ import type { AuditReport, BaselineData, BaselineDiff } from './types.js';
2
+ /**
3
+ * Extract a minimal, stable snapshot from an AuditReport suitable for
4
+ * persistence and future comparison.
5
+ */
6
+ export declare function toBaselineData(report: AuditReport): BaselineData;
7
+ /**
8
+ * Persist a baseline to disk as pretty-printed JSON.
9
+ * Creates intermediate directories if they don't exist.
10
+ */
11
+ export declare function saveBaseline(path: string, report: AuditReport): void;
12
+ /**
13
+ * Load a previously saved baseline from disk.
14
+ * Throws with a clear message on missing file or invalid JSON.
15
+ */
16
+ export declare function loadBaseline(path: string): BaselineData;
17
+ /**
18
+ * Compare a current audit report against a stored baseline, producing
19
+ * per-check deltas and overall regression/improvement lists.
20
+ */
21
+ export declare function diffBaseline(baseline: BaselineData, report: AuditReport): BaselineDiff;
22
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAErF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAWhE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAIpE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAwBvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,YAAY,CAsCtF"}
@@ -0,0 +1,108 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ /**
4
+ * Extract a minimal, stable snapshot from an AuditReport suitable for
5
+ * persistence and future comparison.
6
+ */
7
+ export function toBaselineData(report) {
8
+ const checks = {};
9
+ for (const r of report.results) {
10
+ checks[r.id] = r.score;
11
+ }
12
+ return {
13
+ url: report.url,
14
+ timestamp: report.timestamp,
15
+ overallScore: report.overallScore,
16
+ checks,
17
+ };
18
+ }
19
+ /**
20
+ * Persist a baseline to disk as pretty-printed JSON.
21
+ * Creates intermediate directories if they don't exist.
22
+ */
23
+ export function saveBaseline(path, report) {
24
+ const data = toBaselineData(report);
25
+ mkdirSync(dirname(path), { recursive: true });
26
+ writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
27
+ }
28
+ /**
29
+ * Load a previously saved baseline from disk.
30
+ * Throws with a clear message on missing file or invalid JSON.
31
+ */
32
+ export function loadBaseline(path) {
33
+ let raw;
34
+ try {
35
+ raw = readFileSync(path, 'utf-8');
36
+ }
37
+ catch (err) {
38
+ const code = err.code;
39
+ if (code === 'ENOENT') {
40
+ throw new Error(`Baseline file not found: ${path}`, { cause: err });
41
+ }
42
+ throw err;
43
+ }
44
+ let data;
45
+ try {
46
+ data = JSON.parse(raw);
47
+ }
48
+ catch (cause) {
49
+ throw new Error(`Baseline file is not valid JSON: ${path}`, { cause });
50
+ }
51
+ if (!isBaselineData(data)) {
52
+ throw new Error(`Baseline file has invalid structure (expected url, timestamp, overallScore, checks): ${path}`);
53
+ }
54
+ return data;
55
+ }
56
+ /**
57
+ * Compare a current audit report against a stored baseline, producing
58
+ * per-check deltas and overall regression/improvement lists.
59
+ */
60
+ export function diffBaseline(baseline, report) {
61
+ const checks = report.results.map((r) => {
62
+ const previous = baseline.checks[r.id] ?? 0;
63
+ return {
64
+ id: r.id,
65
+ name: r.name,
66
+ previous,
67
+ current: r.score,
68
+ delta: r.score - previous,
69
+ };
70
+ });
71
+ // Include checks that existed in the baseline but were removed from the current run
72
+ for (const [id, score] of Object.entries(baseline.checks)) {
73
+ if (!checks.some((c) => c.id === id)) {
74
+ checks.push({
75
+ id,
76
+ name: id, // no human-readable name available for removed checks
77
+ previous: score,
78
+ current: 0,
79
+ delta: -score,
80
+ });
81
+ }
82
+ }
83
+ const overallDelta = report.overallScore - baseline.overallScore;
84
+ return {
85
+ url: report.url,
86
+ baselineTimestamp: baseline.timestamp,
87
+ currentTimestamp: report.timestamp,
88
+ overallPrevious: baseline.overallScore,
89
+ overallCurrent: report.overallScore,
90
+ overallDelta,
91
+ checks,
92
+ regressions: checks.filter((c) => c.delta < 0),
93
+ improvements: checks.filter((c) => c.delta > 0),
94
+ };
95
+ }
96
+ /* ── Internal helpers ─────────────────────────────────────── */
97
+ function isBaselineData(value) {
98
+ if (typeof value !== 'object' || value === null)
99
+ return false;
100
+ const obj = value;
101
+ return (typeof obj.url === 'string' &&
102
+ typeof obj.timestamp === 'string' &&
103
+ typeof obj.overallScore === 'number' &&
104
+ typeof obj.checks === 'object' &&
105
+ obj.checks !== null &&
106
+ !Array.isArray(obj.checks));
107
+ }
108
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,14 @@
1
1
  import type { CheckContext, CheckResult, CheckMeta } from '../types.js';
2
+ /**
3
+ * "agent-json" — `/.well-known/agent.json` per the A2A (Agent-to-Agent) protocol.
4
+ *
5
+ * Validates the document on three axes:
6
+ * 1. JSON well-formedness
7
+ * 2. Required fields per the A2A spec (`name`, `description`, `url`, `skills`)
8
+ * 3. Field semantics: `url` should resolve to the same origin as the audited site,
9
+ * `skills[]` should each declare an `id` and `description`, and protocol/optional
10
+ * fields are present where expected.
11
+ */
2
12
  export declare const meta: CheckMeta;
3
13
  export default function check(ctx: CheckContext): Promise<CheckResult>;
4
14
  //# sourceMappingURL=agent-json.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-json.d.ts","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAGjF,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAEF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+F3E"}
1
+ {"version":3,"file":"agent-json.d.ts","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAGjF;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAEF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA0I3E"}
@@ -1,11 +1,21 @@
1
1
  import { AGENT_JSON_REQUIRED_FIELDS } from '../constants.js';
2
2
  import { guideUrl } from '../guide-urls.js';
3
- import { buildResult } from './utils.js';
3
+ import { buildResult, checkContentType } from './utils.js';
4
+ /**
5
+ * "agent-json" — `/.well-known/agent.json` per the A2A (Agent-to-Agent) protocol.
6
+ *
7
+ * Validates the document on three axes:
8
+ * 1. JSON well-formedness
9
+ * 2. Required fields per the A2A spec (`name`, `description`, `url`, `skills`)
10
+ * 3. Field semantics: `url` should resolve to the same origin as the audited site,
11
+ * `skills[]` should each declare an `id` and `description`, and protocol/optional
12
+ * fields are present where expected.
13
+ */
4
14
  export const meta = {
5
15
  id: 'agent-json',
6
16
  name: 'Agent Card (A2A)',
7
17
  description: 'Checks /.well-known/agent.json A2A protocol compliance',
8
- weight: 10,
18
+ weight: 7,
9
19
  };
10
20
  export default async function check(ctx) {
11
21
  const start = performance.now();
@@ -23,6 +33,15 @@ export default async function check(ctx) {
23
33
  return buildResult(meta, 0, findings, start);
24
34
  }
25
35
  findings.push({ status: 'pass', message: '/.well-known/agent.json exists' });
36
+ const ctFinding = checkContentType(res, ['application/json'], {
37
+ checkId: meta.id,
38
+ resourceLabel: '/.well-known/agent.json',
39
+ anchor: 'wrong-content-type',
40
+ });
41
+ if (ctFinding) {
42
+ findings.push(ctFinding);
43
+ score -= 5;
44
+ }
26
45
  let data;
27
46
  try {
28
47
  data = JSON.parse(res.body);
@@ -51,8 +70,42 @@ export default async function check(ctx) {
51
70
  score -= 15;
52
71
  }
53
72
  }
73
+ if (typeof data.url === 'string' && data.url.length > 0) {
74
+ const sameOrigin = sameHost(data.url, ctx.url);
75
+ if (sameOrigin === false) {
76
+ findings.push({
77
+ status: 'warn',
78
+ message: `agent.json "url" points to a different origin: ${data.url}`,
79
+ hint: 'The url field should match the audited site origin. Pointing it elsewhere can confuse agents about the canonical agent endpoint.',
80
+ learnMoreUrl: guideUrl(meta.id, 'url-mismatch'),
81
+ });
82
+ score -= 5;
83
+ }
84
+ else if (sameOrigin === null) {
85
+ findings.push({
86
+ status: 'warn',
87
+ message: `agent.json "url" is not a valid absolute URL: ${data.url}`,
88
+ hint: 'Provide an absolute https:// URL for the url field.',
89
+ learnMoreUrl: guideUrl(meta.id, 'url-invalid'),
90
+ });
91
+ score -= 5;
92
+ }
93
+ }
54
94
  if (Array.isArray(data.skills) && data.skills.length > 0) {
55
95
  findings.push({ status: 'pass', message: `${data.skills.length} skill(s) defined` });
96
+ const incomplete = data.skills.filter((s) => !s.id || !s.description);
97
+ if (incomplete.length === 0) {
98
+ findings.push({ status: 'pass', message: 'All skills have id + description' });
99
+ }
100
+ else {
101
+ findings.push({
102
+ status: 'warn',
103
+ message: `${incomplete.length}/${data.skills.length} skill(s) missing id or description`,
104
+ hint: 'Each entry in skills[] should include both an id and a description so agents can address it and reason about its purpose.',
105
+ learnMoreUrl: guideUrl(meta.id, 'incomplete-skills'),
106
+ });
107
+ score -= 5;
108
+ }
56
109
  }
57
110
  else if (Array.isArray(data.skills)) {
58
111
  findings.push({
@@ -100,4 +153,15 @@ export default async function check(ctx) {
100
153
  }
101
154
  return buildResult(meta, score, findings, start);
102
155
  }
156
+ /** Returns `true` when the two URLs share host (case-insensitive), `false` if hosts differ, `null` on parse error. */
157
+ function sameHost(a, b) {
158
+ try {
159
+ const ua = new URL(a);
160
+ const ub = new URL(b);
161
+ return ua.host.toLowerCase() === ub.host.toLowerCase();
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
103
167
  //# sourceMappingURL=agent-json.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-json.js","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-json.js","sourceRoot":"","sources":["../../src/checks/agent-json.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { CheckContext, CheckResult, CheckMeta } from '../types.js';
2
+ /**
3
+ * "html-rendering" — does the HTML delivered to a non-JS agent actually contain content?
4
+ *
5
+ * Most AI crawlers (GPTBot, ClaudeBot, CCBot, etc.) do **not** execute JavaScript. A site
6
+ * built as a client-rendered SPA returns an empty `<div id="root"></div>` shell to those
7
+ * agents, and is effectively invisible regardless of how good its `llms.txt` and structured
8
+ * data are. This check estimates server-side rendering by inspecting the static HTML body.
9
+ *
10
+ * Signals (each contributes to the score):
11
+ * - Visible text length and word count (low → likely JS-rendered shell)
12
+ * - Text-to-markup ratio (high JS, no text → SPA)
13
+ * - Presence of semantic landmarks (`<main>`, `<article>`, `<header>`, `<footer>`, `<nav>`)
14
+ * - Single, meaningful `<h1>`
15
+ * - `<noscript>` fallback for JS-only frameworks
16
+ * - Non-empty SPA root containers (`#root`, `#app`, `#__next`)
17
+ * - Image `alt` attribute coverage
18
+ */
19
+ export declare const meta: CheckMeta;
20
+ export default function check(ctx: CheckContext): Promise<CheckResult>;
21
+ //# sourceMappingURL=html-rendering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-rendering.d.ts","sourceRoot":"","sources":["../../src/checks/html-rendering.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAW,MAAM,aAAa,CAAC;AAIjF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI,EAAE,SAKlB,CAAC;AAOF,wBAA8B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA2K3E"}