ax-audit 3.1.0 → 4.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 +198 -0
- package/LICENSE +1 -1
- package/README.md +89 -231
- package/dist/baseline.d.ts +2 -0
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +42 -4
- package/dist/baseline.js.map +1 -1
- package/dist/check-ids.d.ts +19 -0
- package/dist/check-ids.d.ts.map +1 -0
- package/dist/check-ids.js +53 -0
- package/dist/check-ids.js.map +1 -0
- package/dist/checks/agent-access.d.ts +33 -0
- package/dist/checks/agent-access.d.ts.map +1 -0
- package/dist/checks/agent-access.js +256 -0
- package/dist/checks/agent-access.js.map +1 -0
- package/dist/checks/agent-card.d.ts +37 -0
- package/dist/checks/agent-card.d.ts.map +1 -0
- package/dist/checks/agent-card.js +352 -0
- package/dist/checks/agent-card.js.map +1 -0
- package/dist/checks/agent-operability.d.ts +66 -0
- package/dist/checks/agent-operability.d.ts.map +1 -0
- package/dist/checks/agent-operability.js +383 -0
- package/dist/checks/agent-operability.js.map +1 -0
- package/dist/checks/agent-skills.d.ts +24 -0
- package/dist/checks/agent-skills.d.ts.map +1 -0
- package/dist/checks/agent-skills.js +316 -0
- package/dist/checks/agent-skills.js.map +1 -0
- package/dist/checks/ai-catalog.d.ts +28 -0
- package/dist/checks/ai-catalog.d.ts.map +1 -0
- package/dist/checks/ai-catalog.js +254 -0
- package/dist/checks/ai-catalog.js.map +1 -0
- package/dist/checks/ai-directives.d.ts +57 -0
- package/dist/checks/ai-directives.d.ts.map +1 -0
- package/dist/checks/ai-directives.js +263 -0
- package/dist/checks/ai-directives.js.map +1 -0
- package/dist/checks/api-discovery.d.ts +26 -0
- package/dist/checks/api-discovery.d.ts.map +1 -0
- package/dist/checks/api-discovery.js +432 -0
- package/dist/checks/api-discovery.js.map +1 -0
- package/dist/checks/auth-discovery.d.ts +28 -0
- package/dist/checks/auth-discovery.d.ts.map +1 -0
- package/dist/checks/auth-discovery.js +213 -0
- package/dist/checks/auth-discovery.js.map +1 -0
- package/dist/checks/commerce-discovery.d.ts +40 -0
- package/dist/checks/commerce-discovery.d.ts.map +1 -0
- package/dist/checks/commerce-discovery.js +295 -0
- package/dist/checks/commerce-discovery.js.map +1 -0
- package/dist/checks/content-negotiation.d.ts.map +1 -1
- package/dist/checks/content-negotiation.js +135 -20
- package/dist/checks/content-negotiation.js.map +1 -1
- package/dist/checks/crawl-efficiency.d.ts +16 -0
- package/dist/checks/crawl-efficiency.d.ts.map +1 -0
- package/dist/checks/crawl-efficiency.js +186 -0
- package/dist/checks/crawl-efficiency.js.map +1 -0
- package/dist/checks/frontmatter.d.ts +34 -0
- package/dist/checks/frontmatter.d.ts.map +1 -0
- package/dist/checks/frontmatter.js +100 -0
- package/dist/checks/frontmatter.js.map +1 -0
- package/dist/checks/html-rendering.d.ts.map +1 -1
- package/dist/checks/html-rendering.js +0 -1
- package/dist/checks/html-rendering.js.map +1 -1
- package/dist/checks/html-utils.d.ts +10 -0
- package/dist/checks/html-utils.d.ts.map +1 -1
- package/dist/checks/html-utils.js +19 -0
- package/dist/checks/html-utils.js.map +1 -1
- package/dist/checks/http-headers.d.ts.map +1 -1
- package/dist/checks/http-headers.js +82 -10
- package/dist/checks/http-headers.js.map +1 -1
- package/dist/checks/http-hygiene.d.ts +26 -0
- package/dist/checks/http-hygiene.d.ts.map +1 -0
- package/dist/checks/http-hygiene.js +257 -0
- package/dist/checks/http-hygiene.js.map +1 -0
- package/dist/checks/index.d.ts.map +1 -1
- package/dist/checks/index.js +30 -8
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/llms-txt.d.ts +15 -0
- package/dist/checks/llms-txt.d.ts.map +1 -1
- package/dist/checks/llms-txt.js +162 -2
- package/dist/checks/llms-txt.js.map +1 -1
- package/dist/checks/mcp-discovery.d.ts +30 -0
- package/dist/checks/mcp-discovery.d.ts.map +1 -0
- package/dist/checks/mcp-discovery.js +523 -0
- package/dist/checks/mcp-discovery.js.map +1 -0
- package/dist/checks/meta-tags.d.ts.map +1 -1
- package/dist/checks/meta-tags.js +6 -5
- package/dist/checks/meta-tags.js.map +1 -1
- package/dist/checks/robots-parser.d.ts +110 -0
- package/dist/checks/robots-parser.d.ts.map +1 -0
- package/dist/checks/robots-parser.js +277 -0
- package/dist/checks/robots-parser.js.map +1 -0
- package/dist/checks/robots-txt.d.ts +2 -0
- package/dist/checks/robots-txt.d.ts.map +1 -1
- package/dist/checks/robots-txt.js +252 -45
- package/dist/checks/robots-txt.js.map +1 -1
- package/dist/checks/{mcp.d.ts → rsl.d.ts} +1 -1
- package/dist/checks/rsl.d.ts.map +1 -0
- package/dist/checks/rsl.js +242 -0
- package/dist/checks/rsl.js.map +1 -0
- package/dist/checks/security-txt.d.ts.map +1 -1
- package/dist/checks/security-txt.js +0 -1
- package/dist/checks/security-txt.js.map +1 -1
- package/dist/checks/seo-basics.d.ts.map +1 -1
- package/dist/checks/seo-basics.js +0 -1
- package/dist/checks/seo-basics.js.map +1 -1
- package/dist/checks/sitemap.d.ts.map +1 -1
- package/dist/checks/sitemap.js +0 -1
- package/dist/checks/sitemap.js.map +1 -1
- package/dist/checks/structured-data.d.ts.map +1 -1
- package/dist/checks/structured-data.js +215 -4
- package/dist/checks/structured-data.js.map +1 -1
- package/dist/checks/structured-fields.d.ts +46 -0
- package/dist/checks/structured-fields.d.ts.map +1 -0
- package/dist/checks/structured-fields.js +112 -0
- package/dist/checks/structured-fields.js.map +1 -0
- package/dist/checks/surface.d.ts +59 -0
- package/dist/checks/surface.d.ts.map +1 -0
- package/dist/checks/surface.js +106 -0
- package/dist/checks/surface.js.map +1 -0
- package/dist/checks/tls-https.d.ts.map +1 -1
- package/dist/checks/tls-https.js +0 -1
- package/dist/checks/tls-https.js.map +1 -1
- package/dist/checks/usage-policy.d.ts +53 -0
- package/dist/checks/usage-policy.d.ts.map +1 -0
- package/dist/checks/usage-policy.js +339 -0
- package/dist/checks/usage-policy.js.map +1 -0
- package/dist/checks/utils.d.ts +25 -1
- package/dist/checks/utils.d.ts.map +1 -1
- package/dist/checks/utils.js +33 -1
- package/dist/checks/utils.js.map +1 -1
- package/dist/checks/waf.d.ts +75 -0
- package/dist/checks/waf.d.ts.map +1 -0
- package/dist/checks/waf.js +203 -0
- package/dist/checks/waf.js.map +1 -0
- package/dist/checks/webmcp.d.ts +55 -0
- package/dist/checks/webmcp.d.ts.map +1 -0
- package/dist/checks/webmcp.js +209 -0
- package/dist/checks/webmcp.js.map +1 -0
- package/dist/checks/well-known.d.ts +38 -0
- package/dist/checks/well-known.d.ts.map +1 -0
- package/dist/checks/well-known.js +202 -0
- package/dist/checks/well-known.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +144 -6
- package/dist/cli.js.map +1 -1
- package/dist/constants.d.ts +212 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +625 -59
- package/dist/constants.js.map +1 -1
- package/dist/fetcher.d.ts +5 -1
- package/dist/fetcher.d.ts.map +1 -1
- package/dist/fetcher.js +62 -27
- package/dist/fetcher.js.map +1 -1
- package/dist/guide-urls.js +1 -1
- package/dist/guide-urls.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +2 -2
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +18 -7
- package/dist/orchestrator.js.map +1 -1
- package/dist/reporter/html.d.ts +9 -0
- package/dist/reporter/html.d.ts.map +1 -1
- package/dist/reporter/html.js +49 -11
- package/dist/reporter/html.js.map +1 -1
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +7 -0
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/markdown.d.ts +8 -0
- package/dist/reporter/markdown.d.ts.map +1 -0
- package/dist/reporter/markdown.js +106 -0
- package/dist/reporter/markdown.js.map +1 -0
- package/dist/reporter/terminal.d.ts.map +1 -1
- package/dist/reporter/terminal.js +36 -1
- package/dist/reporter/terminal.js.map +1 -1
- package/dist/scorer.d.ts +10 -0
- package/dist/scorer.d.ts.map +1 -1
- package/dist/scorer.js +22 -5
- package/dist/scorer.js.map +1 -1
- package/dist/types.d.ts +96 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/api.md +200 -0
- package/docs/architecture.md +104 -0
- package/docs/checks.md +555 -0
- package/docs/ci.md +89 -0
- package/docs/cli.md +103 -0
- package/docs/concepts.md +101 -0
- package/docs/faq.md +89 -0
- package/docs/getting-started.md +108 -0
- package/docs/roadmap.md +367 -0
- package/package.json +14 -5
- package/dist/checks/agent-json.d.ts +0 -14
- package/dist/checks/agent-json.d.ts.map +0 -1
- package/dist/checks/agent-json.js +0 -167
- package/dist/checks/agent-json.js.map +0 -1
- package/dist/checks/mcp.d.ts.map +0 -1
- package/dist/checks/mcp.js +0 -162
- package/dist/checks/mcp.js.map +0 -1
- package/dist/checks/openapi.d.ts +0 -4
- package/dist/checks/openapi.d.ts.map +0 -1
- package/dist/checks/openapi.js +0 -121
- package/dist/checks/openapi.js.map +0 -1
- package/dist/checks/well-known-ai.d.ts +0 -17
- package/dist/checks/well-known-ai.d.ts.map +0 -1
- package/dist/checks/well-known-ai.js +0 -123
- package/dist/checks/well-known-ai.js.map +0 -1
package/docs/api.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Programmatic API
|
|
2
|
+
|
|
3
|
+
Full TypeScript support; every public type is exported from the package root.
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { audit, batchAudit } from 'ax-audit';
|
|
7
|
+
import type { AuditReport, BatchAuditReport } from 'ax-audit';
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The package is ESM-only (`"type": "module"`), targets Node 18+ (built-in `fetch`), and ships type declarations at `dist/index.d.ts`.
|
|
11
|
+
|
|
12
|
+
## `audit(options): Promise<AuditReport>`
|
|
13
|
+
|
|
14
|
+
Runs all (or selected) checks against one URL.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
function audit(options: AuditOptions): Promise<AuditReport>;
|
|
18
|
+
|
|
19
|
+
interface AuditOptions {
|
|
20
|
+
url: string; // required, fully qualified (scheme included)
|
|
21
|
+
checks?: string[]; // default: all. Unknown IDs are silently ignored here
|
|
22
|
+
// (the CLI validates them; the API does not)
|
|
23
|
+
timeout?: number; // ms per request, default 10000
|
|
24
|
+
retries?: number; // transient-failure retries, default 2
|
|
25
|
+
verbose?: boolean; // log to stderr, default false
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Behavior:
|
|
30
|
+
|
|
31
|
+
- Checks execute in parallel via `Promise.allSettled`. A check that **throws** becomes a score-0 `CheckResult` whose findings contain the error — `audit` itself never rejects for a check failure.
|
|
32
|
+
- All HTTP requests in a run share an in-memory cache keyed on URL + normalized request headers (`Vary`-aware).
|
|
33
|
+
- The homepage is fetched once and passed to every check as `ctx.html` / `ctx.headers`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const report = await audit({ url: 'https://example.com' });
|
|
37
|
+
report.overallScore; // number 0–100, weighted
|
|
38
|
+
report.grade; // { min, label, color }
|
|
39
|
+
report.results; // CheckResult[]
|
|
40
|
+
report.duration; // ms
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`audit` **rejects** only for an unrecoverable setup error (e.g. an invalid `url` that can't be parsed). Network failure on the homepage fetch does not reject — it surfaces as failing checks.
|
|
44
|
+
|
|
45
|
+
## `batchAudit(urls, options?): Promise<BatchAuditReport>`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
function batchAudit(urls: string[], options?: BatchOptions): Promise<BatchAuditReport>;
|
|
49
|
+
|
|
50
|
+
interface BatchOptions extends Omit<AuditOptions, 'url'> {
|
|
51
|
+
concurrency?: number; // max parallel audits, default 1 (sequential)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Report order always matches input order, regardless of `concurrency`. `concurrency < 1` is treated as 1.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const batch = await batchAudit(urls, { concurrency: 4, retries: 2 });
|
|
59
|
+
batch.reports; // AuditReport[], input order
|
|
60
|
+
batch.summary.total; // number
|
|
61
|
+
batch.summary.passed; // count scoring >= 70
|
|
62
|
+
batch.summary.failed; // count scoring < 70
|
|
63
|
+
batch.summary.averageScore;
|
|
64
|
+
batch.summary.grade; // Grade for the average
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Result shapes
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface AuditReport {
|
|
71
|
+
url: string;
|
|
72
|
+
timestamp: string; // ISO 8601
|
|
73
|
+
overallScore: number; // 0–100
|
|
74
|
+
grade: Grade;
|
|
75
|
+
results: CheckResult[];
|
|
76
|
+
duration: number; // ms
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface CheckResult {
|
|
80
|
+
id: string; // e.g. 'llms-txt'
|
|
81
|
+
name: string; // e.g. 'LLMs.txt'
|
|
82
|
+
description: string;
|
|
83
|
+
score: number; // 0–100, clamped
|
|
84
|
+
findings: Finding[];
|
|
85
|
+
duration: number; // ms
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface Finding {
|
|
89
|
+
status: 'pass' | 'warn' | 'fail';
|
|
90
|
+
message: string;
|
|
91
|
+
detail?: string;
|
|
92
|
+
hint?: string; // remediation advice (warn/fail)
|
|
93
|
+
learnMoreUrl?: string; // link to the matching remediation guide
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface Grade { min: number; label: string; color: string; }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Scoring
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
function calculateOverallScore(results: CheckResult[], metas: CheckMeta[]): number;
|
|
103
|
+
function getGrade(score: number): Grade;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`calculateOverallScore` computes the weighted average; it falls back to a plain average when all selected checks have weight 0, and returns 0 for empty input. `getGrade` maps a score to its `Grade`.
|
|
107
|
+
|
|
108
|
+
## Baselines
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
function toBaselineData(report: AuditReport): BaselineData;
|
|
112
|
+
function saveBaseline(path: string, report: AuditReport): void; // writes JSON
|
|
113
|
+
function loadBaseline(path: string): BaselineData; // throws on missing/invalid file
|
|
114
|
+
function diffBaseline(baseline: BaselineData, report: AuditReport): BaselineDiff;
|
|
115
|
+
|
|
116
|
+
interface BaselineData {
|
|
117
|
+
url: string;
|
|
118
|
+
timestamp: string;
|
|
119
|
+
overallScore: number;
|
|
120
|
+
checks: Record<string, number>; // checkId → score
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface BaselineDiff {
|
|
124
|
+
url: string;
|
|
125
|
+
baselineTimestamp: string;
|
|
126
|
+
currentTimestamp: string;
|
|
127
|
+
overallPrevious: number;
|
|
128
|
+
overallCurrent: number;
|
|
129
|
+
overallDelta: number;
|
|
130
|
+
checks: CheckDiff[];
|
|
131
|
+
regressions: CheckDiff[]; // delta < 0
|
|
132
|
+
improvements: CheckDiff[]; // delta > 0
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`loadBaseline` throws (does not return null) on a missing file or invalid JSON — wrap it in try/catch. Checks present now but absent from the baseline appear as new (no delta); checks removed since are ignored.
|
|
137
|
+
|
|
138
|
+
## Reporters
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
function renderMarkdown(report: AuditReport, diff?: BaselineDiff): string;
|
|
142
|
+
function renderBatchMarkdown(batch: BatchAuditReport): string;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Both return a Markdown string (summary table + findings with status emoji; baseline deltas when a diff is passed). Terminal and HTML rendering are CLI-internal; for other formats, consume the `AuditReport` JSON directly.
|
|
146
|
+
|
|
147
|
+
## Checks registry
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { checks } from 'ax-audit';
|
|
151
|
+
|
|
152
|
+
interface CheckModule {
|
|
153
|
+
run: (ctx: CheckContext) => Promise<CheckResult>;
|
|
154
|
+
meta: CheckMeta; // { id, name, description, weight }
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run an individual check with a custom context:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const llms = checks.find((c) => c.meta.id === 'llms-txt')!;
|
|
162
|
+
const result = await llms.run({
|
|
163
|
+
url: 'https://example.com', // no trailing slash
|
|
164
|
+
html: homepageHtml,
|
|
165
|
+
headers: homepageHeaders, // lowercased keys
|
|
166
|
+
fetch: myFetchImpl,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
interface CheckContext {
|
|
170
|
+
url: string;
|
|
171
|
+
html: string;
|
|
172
|
+
headers: Record<string, string>;
|
|
173
|
+
fetch: (url: string, options?: FetchOptions) => Promise<FetchResponse>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface FetchOptions { headers?: Record<string, string>; }
|
|
177
|
+
|
|
178
|
+
interface FetchResponse {
|
|
179
|
+
status: number; // 0 on network error
|
|
180
|
+
headers: Record<string, string>; // lowercased keys
|
|
181
|
+
body: string;
|
|
182
|
+
ok: boolean;
|
|
183
|
+
url: string; // final URL after redirects
|
|
184
|
+
error?: string; // set when status === 0
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ctx.fetch` custom headers merge case-insensitively over the defaults (a custom `Accept` or `User-Agent` replaces the default), and responses are cached per URL + normalized headers — mirroring HTTP `Vary`. Your `fetch` implementation should never throw; model failures as `{ status: 0, ok: false, error }`.
|
|
189
|
+
|
|
190
|
+
## API stability
|
|
191
|
+
|
|
192
|
+
Within a major version, the exported function signatures and the `AuditReport` JSON shape are stable. Specifically:
|
|
193
|
+
|
|
194
|
+
- **Stable:** `audit`, `batchAudit`, `calculateOverallScore`, `getGrade`, the baseline functions, the reporter functions, and all exported type *shapes*.
|
|
195
|
+
- **May change in minor versions:** the *set* of checks (new checks are added), individual check `score`/`findings` content, and check `weight` values (new checks start at 0; reweighting is reserved for majors). Treat `results` as a list to iterate, not a fixed-length tuple.
|
|
196
|
+
- **Internal:** anything not exported from `src/index.ts`, including terminal/HTML reporters and individual check modules' internals.
|
|
197
|
+
|
|
198
|
+
## Exported types
|
|
199
|
+
|
|
200
|
+
`AuditOptions`, `BatchOptions`, `AuditReport`, `BatchAuditReport`, `CheckResult`, `CheckMeta`, `CheckContext`, `CheckModule`, `Finding`, `FindingStatus`, `FetchOptions`, `FetchResponse`, `Grade`, `OutputFormat`, `BaselineData`, `BaselineDiff`, `CheckDiff`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
ax-audit is a dependency-light TypeScript codebase: two runtime dependencies (`chalk`, `commander`), Node 18+ built-in `fetch`, no HTTP libraries, no XML/HTML parser dependencies (regex-based primitives), and the built-in `node:test` runner.
|
|
4
|
+
|
|
5
|
+
## Pipeline
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
cli.ts ──► orchestrator.ts ──► checks/* (Promise.allSettled, parallel)
|
|
9
|
+
│ │
|
|
10
|
+
▼ ▼
|
|
11
|
+
fetcher.ts scorer.ts ──► reporter/{terminal,json,html,markdown}
|
|
12
|
+
(cache + retries) │
|
|
13
|
+
▲ baseline.ts (save / load / diff)
|
|
14
|
+
└── shared by every check via CheckContext.fetch
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
1. **cli.ts** parses and validates flags, loads the baseline if requested, and dispatches to single or batch mode.
|
|
18
|
+
2. **orchestrator.ts** (`audit`) creates one fetcher per run, fetches the homepage once, builds the `CheckContext` (`url`, `html`, `headers`, `fetch`), and runs all selected checks in parallel. A check that throws is converted into a score-0 result with the error as a finding — one bad check never kills the audit. `batchAudit` runs `audit` per URL through an order-preserving work queue with configurable `concurrency`.
|
|
19
|
+
3. **fetcher.ts** wraps `fetch` with: per-run in-memory caching keyed on URL + normalized (lowercased, sorted) custom headers — mirroring HTTP `Vary` semantics so a `text/markdown` probe never collides with the HTML fetch; case-insensitive header merging over defaults; timeouts via `AbortController`; and retries with exponential backoff for transient failures (status 0, 408, 425, 429, 5xx). Errors never throw — they become `{ status: 0, ok: false, error }` results, also cached.
|
|
20
|
+
4. **checks/** — one module per check (26). Each exports `default` (async check function) and `meta` (`{ id, name, description, weight, category?, aliases? }`).
|
|
21
|
+
5. **scorer.ts** computes the weighted average over the checks that ran *and apply*; a check reporting `applicable: false` is excluded from both numerator and denominator. When every applicable check has weight 0 it falls back to a plain average.
|
|
22
|
+
6. **reporter/** renders to terminal (chalk), JSON, self-contained HTML, or Markdown, grouping checks by category and showing n/a where a check does not apply.
|
|
23
|
+
7. **baseline.ts** persists minimal score snapshots and computes per-check diffs for regression gating.
|
|
24
|
+
|
|
25
|
+
## Anatomy of a check
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { guideUrl } from '../guide-urls.js';
|
|
29
|
+
import type { CheckContext, CheckResult, CheckMeta, Finding } from '../types.js';
|
|
30
|
+
import { buildResult } from './utils.js';
|
|
31
|
+
|
|
32
|
+
export const meta: CheckMeta = {
|
|
33
|
+
id: 'my-check',
|
|
34
|
+
name: 'My Check',
|
|
35
|
+
description: 'One-line description shown in reports',
|
|
36
|
+
category: 'discovery',
|
|
37
|
+
// No `weight` here: weights live in CHECK_WEIGHTS, and only there.
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default async function check(ctx: CheckContext): Promise<CheckResult> {
|
|
41
|
+
const start = performance.now();
|
|
42
|
+
const findings: Finding[] = [];
|
|
43
|
+
let score = 100;
|
|
44
|
+
|
|
45
|
+
const res = await ctx.fetch(`${ctx.url}/something`, { headers: { Accept: 'application/json' } });
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
findings.push({
|
|
48
|
+
status: 'fail',
|
|
49
|
+
message: '/something not found',
|
|
50
|
+
hint: 'Actionable, copy-pasteable advice.',
|
|
51
|
+
learnMoreUrl: guideUrl(meta.id, 'not-found'),
|
|
52
|
+
});
|
|
53
|
+
return buildResult(meta, 0, findings, start);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ... validations, each pushing a pass/warn/fail Finding and adjusting score
|
|
57
|
+
|
|
58
|
+
return buildResult(meta, score, findings, start);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Conventions:
|
|
63
|
+
|
|
64
|
+
- **Findings are actionable.** Every `warn`/`fail` carries a `hint` with concrete remediation and a `learnMoreUrl` pointing to `axrush.com/guides/<check-id>#<anchor>`. Every anchor must have a section in that guide.
|
|
65
|
+
- **Scores are clamped** to [0, 100] by `buildResult`.
|
|
66
|
+
- **Shared HTML primitives** live in `checks/html-utils.ts` (`getMetaContent`, `findLinkTags`, `getAttribute`, `extractVisibleText`, …) — no per-check regex duplication.
|
|
67
|
+
- **robots.txt is parsed once**, by `checks/robots-parser.ts`. It returns User-agent groups with their rules plus `Content-Signal`, `Content-Usage`, `License`, `Sitemap` and `Agentmap` directives. `robots-txt`, `rsl` and `agent-access` all consume it, so the grouping rules have one definition.
|
|
68
|
+
- **Responses are classified**, not just status-checked. `checks/waf.ts` turns a response into ok / challenge / paywall / needs-signature / license-required / rate-limited / blocked, with the evidence that produced it and an `inconclusive` flag for what an unsigned probe cannot settle.
|
|
69
|
+
- **Probed paths carry their standing.** `checks/well-known.ts` records every path as IANA-registered, vendor convention, draft or legacy, so a missing draft file is never reported like a missing registered one.
|
|
70
|
+
- **An HTML body means absent, not broken.** `isHtmlDocument` in `checks/utils.ts` gates speculative probes: an SPA catch-all returns its index shell for every unknown path, and reporting that as a malformed document sends operators hunting for a bug in a file they never wrote.
|
|
71
|
+
- **A check that does not apply reports N/A**, via `notApplicable()`, rather than scoring 0. Commerce discovery on a blog, OAuth metadata where nothing needs authorizing, WebMCP on a page with no forms: scoring these zero would say something false about the site. Everything counted against a site must be something the site could have done.
|
|
72
|
+
- **Check ids are a public interface** — they appear in `--checks` flags and in saved baselines. A rename declares the old id in `meta.aliases`; `src/check-ids.ts` resolves aliases for selection and for baseline diffing.
|
|
73
|
+
- **Content-Type validation** uses `checkContentType` from `checks/utils.ts` (−5 convention for mismatches).
|
|
74
|
+
- **Network goes through `ctx.fetch`** — never raw `fetch` — so caching, retries, timeouts, and `--verbose` logging apply uniformly.
|
|
75
|
+
|
|
76
|
+
## Adding a new check
|
|
77
|
+
|
|
78
|
+
1. Create `src/checks/your-check.ts` exporting `default` + `meta` (weight 0 — see scoring policy below).
|
|
79
|
+
2. Register it in `src/checks/index.ts`.
|
|
80
|
+
3. Add its weight to `CHECK_WEIGHTS` in `src/constants.ts`.
|
|
81
|
+
4. Add a test suite in `test/checks/your-check.test.js` using `mockContext` / `mockResponse` from `test/helpers.js`. Route values can be functions `(url, fetchOptions) => response` when the response must vary by request headers.
|
|
82
|
+
5. Document it in `docs/checks.md` and the README table.
|
|
83
|
+
6. Write the remediation guide covering every `learnMoreUrl` anchor you emit.
|
|
84
|
+
|
|
85
|
+
## Scoring policy
|
|
86
|
+
|
|
87
|
+
Score deltas on the same site are treated as **breaking**. Within a major version:
|
|
88
|
+
|
|
89
|
+
- New checks ship with **weight 0**: full findings, no effect on the overall score or baselines.
|
|
90
|
+
- New findings inside weighted checks must be informational, with no deduction.
|
|
91
|
+
- Weight redistribution happens in a major version, and the baseline schema version is bumped with it so an existing baseline is not read as a regression.
|
|
92
|
+
|
|
93
|
+
Weights live in `CHECK_WEIGHTS` in `src/constants.ts`, and only there. Checks used to declare their own `meta.weight` alongside the map; the two drifted, and a redistribution silently did nothing. `CheckMeta.weight` remains as an override that nothing uses, and a test asserts no check declares one.
|
|
94
|
+
|
|
95
|
+
Two check states exist beyond a score:
|
|
96
|
+
|
|
97
|
+
- **Weight 0** means the check runs and reports but rests on something too unsettled to score: a draft specification that may be renamed.
|
|
98
|
+
- **Not applicable** means the question does not arise for this site. It leaves the denominator entirely. `--profile` overrides the detection.
|
|
99
|
+
|
|
100
|
+
## Testing
|
|
101
|
+
|
|
102
|
+
`npm test` builds (`tsc`) and runs `node --test`. The suite (964 tests) covers every check, the scorer, baseline logic, the Markdown reporter, plus integration tests that spin up real local HTTP servers for the fetcher (per-header caching, retries, HEAD and manual redirects) and the batch orchestrator (ordering, concurrency caps). No test dependencies beyond Node.
|
|
103
|
+
|
|
104
|
+
Two classes of test exist specifically to keep the 3.x promise that no score goes down: **score-stability tests** assert that a configuration which scored 100 in 3.6 still scores 100, and that findings added inside a weighted check leave the score untouched. When those fail, the change belongs in the next major, not the current minor.
|