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.
- package/CHANGELOG.md +47 -0
- package/README.md +90 -18
- package/dist/baseline.d.ts +22 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +108 -0
- package/dist/baseline.js.map +1 -0
- package/dist/checks/agent-json.d.ts +10 -0
- package/dist/checks/agent-json.d.ts.map +1 -1
- package/dist/checks/agent-json.js +66 -2
- package/dist/checks/agent-json.js.map +1 -1
- package/dist/checks/html-rendering.d.ts +21 -0
- package/dist/checks/html-rendering.d.ts.map +1 -0
- package/dist/checks/html-rendering.js +204 -0
- package/dist/checks/html-rendering.js.map +1 -0
- package/dist/checks/html-utils.d.ts +37 -0
- package/dist/checks/html-utils.d.ts.map +1 -0
- package/dist/checks/html-utils.js +93 -0
- package/dist/checks/html-utils.js.map +1 -0
- package/dist/checks/http-headers.js +1 -1
- package/dist/checks/http-headers.js.map +1 -1
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +10 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/llms-txt.d.ts.map +1 -1
- package/dist/checks/llms-txt.js +17 -2
- package/dist/checks/llms-txt.js.map +1 -1
- package/dist/checks/mcp.d.ts.map +1 -1
- package/dist/checks/mcp.js +11 -2
- package/dist/checks/mcp.js.map +1 -1
- package/dist/checks/meta-tags.d.ts +13 -0
- package/dist/checks/meta-tags.d.ts.map +1 -1
- package/dist/checks/meta-tags.js +104 -24
- package/dist/checks/meta-tags.js.map +1 -1
- package/dist/checks/openapi.d.ts.map +1 -1
- package/dist/checks/openapi.js +11 -2
- package/dist/checks/openapi.js.map +1 -1
- package/dist/checks/robots-txt.d.ts.map +1 -1
- package/dist/checks/robots-txt.js +5 -2
- package/dist/checks/robots-txt.js.map +1 -1
- package/dist/checks/security-txt.js +1 -1
- package/dist/checks/seo-basics.d.ts +13 -0
- package/dist/checks/seo-basics.d.ts.map +1 -0
- package/dist/checks/seo-basics.js +222 -0
- package/dist/checks/seo-basics.js.map +1 -0
- package/dist/checks/sitemap.d.ts +12 -0
- package/dist/checks/sitemap.d.ts.map +1 -0
- package/dist/checks/sitemap.js +241 -0
- package/dist/checks/sitemap.js.map +1 -0
- package/dist/checks/structured-data.js +1 -1
- package/dist/checks/structured-data.js.map +1 -1
- package/dist/checks/tls-https.d.ts +13 -0
- package/dist/checks/tls-https.d.ts.map +1 -0
- package/dist/checks/tls-https.js +164 -0
- package/dist/checks/tls-https.js.map +1 -0
- package/dist/checks/utils.d.ts +13 -1
- package/dist/checks/utils.d.ts.map +1 -1
- package/dist/checks/utils.js +28 -0
- package/dist/checks/utils.js.map +1 -1
- package/dist/checks/well-known-ai.d.ts +17 -0
- package/dist/checks/well-known-ai.d.ts.map +1 -0
- package/dist/checks/well-known-ai.js +123 -0
- package/dist/checks/well-known-ai.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +47 -1
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +19 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +47 -10
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/reporter/html.d.ts +2 -2
- package/dist/reporter/html.d.ts.map +1 -1
- package/dist/reporter/html.js +63 -7
- package/dist/reporter/html.js.map +1 -1
- package/dist/reporter/index.d.ts +2 -2
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +4 -4
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/json.d.ts +2 -2
- package/dist/reporter/json.d.ts.map +1 -1
- package/dist/reporter/json.js +3 -2
- package/dist/reporter/json.js.map +1 -1
- package/dist/reporter/terminal.d.ts +2 -2
- package/dist/reporter/terminal.d.ts.map +1 -1
- package/dist/reporter/terminal.js +42 -3
- package/dist/reporter/terminal.js.map +1 -1
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -1
- 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
|
-
|
|
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
|
|
31
|
-
PASS
|
|
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
|
|
44
|
-
| **Robots.txt** | AI crawler configuration, wildcard detection, partial path restrictions |
|
|
45
|
-
| **
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
48
|
-
| **
|
|
49
|
-
| **
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
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 `
|
|
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
|
|
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
|
|
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
|
-
|
|
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"}
|
package/dist/baseline.js
ADDED
|
@@ -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,
|
|
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:
|
|
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":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
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"}
|