afdocs 0.3.4 → 0.4.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/README.md +8 -8
- package/dist/checks/agent-discoverability/llms-txt-directive.js +206 -5
- package/dist/checks/agent-discoverability/llms-txt-directive.js.map +1 -1
- package/dist/checks/url-stability/redirect-behavior.js +127 -5
- package/dist/checks/url-stability/redirect-behavior.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Test your documentation site against the [Agent-Friendly Documentation Spec](htt
|
|
|
7
7
|
|
|
8
8
|
Agents don't use docs like humans. They hit truncation limits, get walls of CSS instead of content, can't follow cross-host redirects, and don't know about quality-of-life improvements like `llms.txt` or `.md` docs pages that would make life swell. Maybe this is because the industry has lacked guidance - until now.
|
|
9
9
|
|
|
10
|
-
afdocs runs 21 checks across 8 categories to evaluate how well your docs serve agent consumers.
|
|
10
|
+
afdocs runs 21 checks across 8 categories to evaluate how well your docs serve agent consumers. 16 are fully implemented; the rest return `skip` until completed.
|
|
11
11
|
|
|
12
12
|
> **Status: Early development (0.x)**
|
|
13
13
|
> This project is under active development. Check IDs, CLI flags, and output formats may change between minor versions. Feel free to try it out, but don't build automation against specific output until 1.0.
|
|
@@ -181,16 +181,16 @@ describe('agent-friendliness', () => {
|
|
|
181
181
|
|
|
182
182
|
### Category 5: URL Stability and Redirects
|
|
183
183
|
|
|
184
|
-
| Check
|
|
185
|
-
|
|
|
186
|
-
| `http-status-codes`
|
|
187
|
-
| `redirect-behavior`
|
|
184
|
+
| Check | Description |
|
|
185
|
+
| ------------------- | ----------------------------------------------- |
|
|
186
|
+
| `http-status-codes` | Whether error pages return correct status codes |
|
|
187
|
+
| `redirect-behavior` | Whether redirects are same-host HTTP redirects |
|
|
188
188
|
|
|
189
189
|
### Category 6: Agent Discoverability Directives
|
|
190
190
|
|
|
191
|
-
| Check
|
|
192
|
-
|
|
|
193
|
-
| `llms-txt-directive`
|
|
191
|
+
| Check | Description |
|
|
192
|
+
| -------------------- | -------------------------------------------------------- |
|
|
193
|
+
| `llms-txt-directive` | Whether pages include a directive pointing to `llms.txt` |
|
|
194
194
|
|
|
195
195
|
### Category 7: Observability and Content Health
|
|
196
196
|
|
|
@@ -1,10 +1,211 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
3
|
+
/**
|
|
4
|
+
* Patterns that indicate an agent-facing directive pointing to llms.txt.
|
|
5
|
+
*
|
|
6
|
+
* HTML pattern matches:
|
|
7
|
+
* - Links whose href contains "llms.txt"
|
|
8
|
+
* - Text mentioning "llms.txt" in prose
|
|
9
|
+
*
|
|
10
|
+
* Markdown pattern matches:
|
|
11
|
+
* - Markdown links to llms.txt (e.g., [index](/llms.txt))
|
|
12
|
+
* - Plain text mentioning "llms.txt"
|
|
13
|
+
*/
|
|
14
|
+
const HTML_DIRECTIVE_PATTERN = /(?:<a\s[^>]*href\s*=\s*["'][^"']*llms\.txt[^"']*["'][^>]*>[\s\S]*?<\/a>|llms\.txt)/gi;
|
|
15
|
+
const MARKDOWN_DIRECTIVE_PATTERN = /llms\.txt/gi;
|
|
16
|
+
/** Percentage threshold: directive in the first 10% is "near the top". */
|
|
17
|
+
const TOP_THRESHOLD = 0.1;
|
|
18
|
+
/** Percentage threshold: directive past 50% is "buried deep". */
|
|
19
|
+
const DEEP_THRESHOLD = 0.5;
|
|
20
|
+
/**
|
|
21
|
+
* Extract the HTML body content (between <body> and </body>), or fall
|
|
22
|
+
* back to the full HTML if no body tags are found.
|
|
23
|
+
*/
|
|
24
|
+
function extractBody(html) {
|
|
25
|
+
const openMatch = /<body[\s>]/i.exec(html);
|
|
26
|
+
if (!openMatch)
|
|
27
|
+
return { body: html, offset: 0 };
|
|
28
|
+
const bodyStart = html.indexOf('>', openMatch.index + openMatch[0].length - 1) + 1;
|
|
29
|
+
const closeMatch = /<\/body\s*>/i.exec(html.slice(bodyStart));
|
|
30
|
+
const bodyEnd = closeMatch ? bodyStart + closeMatch.index : html.length;
|
|
31
|
+
return { body: html.slice(bodyStart, bodyEnd), offset: bodyStart };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert a markdown URL back to its HTML equivalent.
|
|
35
|
+
* Strips trailing `.md` extension or `/index.md` suffix.
|
|
36
|
+
*/
|
|
37
|
+
function toHtmlUrl(url) {
|
|
38
|
+
try {
|
|
39
|
+
const u = new URL(url);
|
|
40
|
+
if (u.pathname.endsWith('.md')) {
|
|
41
|
+
u.pathname = u.pathname.replace(/(?:\/index)?\.md$/, '') || '/';
|
|
42
|
+
// Ensure trailing slash for directory-style URLs
|
|
43
|
+
if (u.pathname !== '/' && !u.pathname.includes('.')) {
|
|
44
|
+
u.pathname = u.pathname.replace(/\/?$/, '/');
|
|
45
|
+
}
|
|
46
|
+
return u.toString();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Fall through to return original
|
|
51
|
+
}
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
function searchContent(content, pattern) {
|
|
55
|
+
const match = pattern.exec(content);
|
|
56
|
+
pattern.lastIndex = 0;
|
|
57
|
+
if (!match)
|
|
58
|
+
return null;
|
|
59
|
+
return { position: match.index, matchText: match[0].slice(0, 200) };
|
|
60
|
+
}
|
|
61
|
+
async function check(ctx) {
|
|
62
|
+
const id = 'llms-txt-directive';
|
|
63
|
+
const category = 'agent-discoverability';
|
|
64
|
+
const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
|
|
65
|
+
const results = [];
|
|
66
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
67
|
+
for (let i = 0; i < pageUrls.length; i += concurrency) {
|
|
68
|
+
const batch = pageUrls.slice(i, i + concurrency);
|
|
69
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
70
|
+
try {
|
|
71
|
+
// Try the HTML version of the page first
|
|
72
|
+
const htmlUrl = toHtmlUrl(url);
|
|
73
|
+
const response = await ctx.http.fetch(htmlUrl);
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
return { url: htmlUrl, found: false, error: `HTTP ${response.status}` };
|
|
76
|
+
}
|
|
77
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
78
|
+
const text = await response.text();
|
|
79
|
+
// Determine if we got HTML or markdown
|
|
80
|
+
const isHtml = contentType.includes('text/html') || text.trimStart().startsWith('<');
|
|
81
|
+
if (isHtml) {
|
|
82
|
+
const { body } = extractBody(text);
|
|
83
|
+
const hit = searchContent(body, HTML_DIRECTIVE_PATTERN);
|
|
84
|
+
if (hit) {
|
|
85
|
+
const positionPercent = body.length > 0 ? hit.position / body.length : 0;
|
|
86
|
+
return {
|
|
87
|
+
url: htmlUrl,
|
|
88
|
+
found: true,
|
|
89
|
+
source: 'html',
|
|
90
|
+
position: hit.position,
|
|
91
|
+
positionPercent,
|
|
92
|
+
matchText: hit.matchText,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Got markdown content; search it directly
|
|
98
|
+
const hit = searchContent(text, MARKDOWN_DIRECTIVE_PATTERN);
|
|
99
|
+
if (hit) {
|
|
100
|
+
const positionPercent = text.length > 0 ? hit.position / text.length : 0;
|
|
101
|
+
return {
|
|
102
|
+
url: htmlUrl,
|
|
103
|
+
found: true,
|
|
104
|
+
source: 'markdown',
|
|
105
|
+
position: hit.position,
|
|
106
|
+
positionPercent,
|
|
107
|
+
matchText: hit.matchText,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// If the original URL was different (a .md URL), also check it
|
|
112
|
+
if (url !== htmlUrl) {
|
|
113
|
+
try {
|
|
114
|
+
const mdResponse = await ctx.http.fetch(url);
|
|
115
|
+
if (mdResponse.ok) {
|
|
116
|
+
const mdText = await mdResponse.text();
|
|
117
|
+
const hit = searchContent(mdText, MARKDOWN_DIRECTIVE_PATTERN);
|
|
118
|
+
if (hit) {
|
|
119
|
+
const positionPercent = mdText.length > 0 ? hit.position / mdText.length : 0;
|
|
120
|
+
return {
|
|
121
|
+
url,
|
|
122
|
+
found: true,
|
|
123
|
+
source: 'markdown',
|
|
124
|
+
position: hit.position,
|
|
125
|
+
positionPercent,
|
|
126
|
+
matchText: hit.matchText,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Markdown fetch failed; that's fine, we already checked HTML
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { url: htmlUrl, found: false };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
return {
|
|
139
|
+
url,
|
|
140
|
+
found: false,
|
|
141
|
+
error: err instanceof Error ? err.message : String(err),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
results.push(...batchResults);
|
|
146
|
+
}
|
|
147
|
+
const tested = results.filter((r) => !r.error);
|
|
148
|
+
const fetchErrors = results.filter((r) => r.error).length;
|
|
149
|
+
const found = results.filter((r) => r.found);
|
|
150
|
+
const notFound = tested.filter((r) => !r.found);
|
|
151
|
+
if (tested.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
category,
|
|
155
|
+
status: 'fail',
|
|
156
|
+
message: `Could not test any pages${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
157
|
+
details: {
|
|
158
|
+
totalPages,
|
|
159
|
+
testedPages: results.length,
|
|
160
|
+
sampled,
|
|
161
|
+
fetchErrors,
|
|
162
|
+
pageResults: results,
|
|
163
|
+
discoveryWarnings: warnings,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Classify pages with directives by position
|
|
168
|
+
const nearTop = found.filter((r) => (r.positionPercent ?? 1) <= TOP_THRESHOLD);
|
|
169
|
+
const buried = found.filter((r) => (r.positionPercent ?? 0) > DEEP_THRESHOLD);
|
|
170
|
+
let status;
|
|
171
|
+
let message;
|
|
172
|
+
const pageLabel = sampled ? 'sampled pages' : 'pages';
|
|
173
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
174
|
+
if (found.length === 0) {
|
|
175
|
+
status = 'fail';
|
|
176
|
+
message = `No llms.txt directive found in any of ${tested.length} ${pageLabel}${suffix}`;
|
|
177
|
+
}
|
|
178
|
+
else if (buried.length > 0 && nearTop.length === 0) {
|
|
179
|
+
// All found directives are buried deep
|
|
180
|
+
status = 'warn';
|
|
181
|
+
message = `llms.txt directive found in ${found.length} of ${tested.length} ${pageLabel}, but buried deep in the page (past ${Math.round(DEEP_THRESHOLD * 100)}%)${suffix}`;
|
|
182
|
+
}
|
|
183
|
+
else if (notFound.length > 0) {
|
|
184
|
+
// Some pages have directives, some don't
|
|
185
|
+
status = 'warn';
|
|
186
|
+
message = `llms.txt directive found in ${found.length} of ${tested.length} ${pageLabel} (${notFound.length} missing)${suffix}`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
status = 'pass';
|
|
190
|
+
message = `llms.txt directive found in all ${tested.length} ${pageLabel}${nearTop.length > 0 ? ', near the top of content' : ''}${suffix}`;
|
|
191
|
+
}
|
|
3
192
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status
|
|
7
|
-
message
|
|
193
|
+
id,
|
|
194
|
+
category,
|
|
195
|
+
status,
|
|
196
|
+
message,
|
|
197
|
+
details: {
|
|
198
|
+
totalPages,
|
|
199
|
+
testedPages: tested.length,
|
|
200
|
+
sampled,
|
|
201
|
+
foundCount: found.length,
|
|
202
|
+
notFoundCount: notFound.length,
|
|
203
|
+
nearTopCount: nearTop.length,
|
|
204
|
+
buriedCount: buried.length,
|
|
205
|
+
fetchErrors,
|
|
206
|
+
pageResults: results,
|
|
207
|
+
discoveryWarnings: warnings,
|
|
208
|
+
},
|
|
8
209
|
};
|
|
9
210
|
}
|
|
10
211
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"llms-txt-directive.js","sourceRoot":"","sources":["../../../src/checks/agent-discoverability/llms-txt-directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"llms-txt-directive.js","sourceRoot":"","sources":["../../../src/checks/agent-discoverability/llms-txt-directive.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,132 @@
|
|
|
1
1
|
import { registerCheck } from '../registry.js';
|
|
2
|
-
|
|
2
|
+
import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
|
|
3
|
+
const JS_REDIRECT_PATTERNS = /window\.location\s*[=.]|document\.location\s*[=.]|location\.href\s*=|location\.replace\s*\(|<meta[^>]+http-equiv\s*=\s*["']?refresh["']?/i;
|
|
4
|
+
async function check(ctx) {
|
|
5
|
+
const id = 'redirect-behavior';
|
|
6
|
+
const category = 'url-stability';
|
|
7
|
+
const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
|
|
8
|
+
const results = [];
|
|
9
|
+
const concurrency = ctx.options.maxConcurrency;
|
|
10
|
+
for (let i = 0; i < pageUrls.length; i += concurrency) {
|
|
11
|
+
const batch = pageUrls.slice(i, i + concurrency);
|
|
12
|
+
const batchResults = await Promise.all(batch.map(async (url) => {
|
|
13
|
+
try {
|
|
14
|
+
// Use manual redirect to inspect the first response
|
|
15
|
+
const response = await ctx.http.fetch(url, { redirect: 'manual' });
|
|
16
|
+
const status = response.status;
|
|
17
|
+
// Not a redirect
|
|
18
|
+
if (status < 300 || status >= 400) {
|
|
19
|
+
// Check for JS-based redirects in the body
|
|
20
|
+
try {
|
|
21
|
+
const body = await response.text();
|
|
22
|
+
if (JS_REDIRECT_PATTERNS.test(body.slice(0, 10_000))) {
|
|
23
|
+
return { url, status, classification: 'js-redirect' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore body read errors
|
|
28
|
+
}
|
|
29
|
+
return { url, status, classification: 'no-redirect' };
|
|
30
|
+
}
|
|
31
|
+
// HTTP redirect — classify as same-host or cross-host
|
|
32
|
+
const location = response.headers.get('location');
|
|
33
|
+
if (!location) {
|
|
34
|
+
return { url, status, classification: 'no-redirect' };
|
|
35
|
+
}
|
|
36
|
+
const resolvedTarget = new URL(location, url).toString();
|
|
37
|
+
const sourceOrigin = new URL(url).origin;
|
|
38
|
+
const targetOrigin = new URL(resolvedTarget).origin;
|
|
39
|
+
if (sourceOrigin === targetOrigin) {
|
|
40
|
+
return { url, status, classification: 'same-host', redirectTarget: resolvedTarget };
|
|
41
|
+
}
|
|
42
|
+
return { url, status, classification: 'cross-host', redirectTarget: resolvedTarget };
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
url,
|
|
47
|
+
status: null,
|
|
48
|
+
classification: 'fetch-error',
|
|
49
|
+
error: err instanceof Error ? err.message : String(err),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}));
|
|
53
|
+
results.push(...batchResults);
|
|
54
|
+
}
|
|
55
|
+
const tested = results.filter((r) => r.classification !== 'fetch-error');
|
|
56
|
+
const fetchErrors = results.filter((r) => r.classification === 'fetch-error').length;
|
|
57
|
+
const noRedirects = results.filter((r) => r.classification === 'no-redirect');
|
|
58
|
+
const sameHost = results.filter((r) => r.classification === 'same-host');
|
|
59
|
+
const crossHost = results.filter((r) => r.classification === 'cross-host');
|
|
60
|
+
const jsRedirects = results.filter((r) => r.classification === 'js-redirect');
|
|
61
|
+
if (tested.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
category,
|
|
65
|
+
status: 'fail',
|
|
66
|
+
message: `Could not test any URLs${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
|
|
67
|
+
details: {
|
|
68
|
+
totalPages,
|
|
69
|
+
testedPages: results.length,
|
|
70
|
+
sampled,
|
|
71
|
+
fetchErrors,
|
|
72
|
+
pageResults: results,
|
|
73
|
+
discoveryWarnings: warnings,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Determine status: js-redirect → fail, cross-host → warn, otherwise pass
|
|
78
|
+
let status;
|
|
79
|
+
if (jsRedirects.length > 0) {
|
|
80
|
+
status = 'fail';
|
|
81
|
+
}
|
|
82
|
+
else if (crossHost.length > 0) {
|
|
83
|
+
status = 'warn';
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
status = 'pass';
|
|
87
|
+
}
|
|
88
|
+
const pageLabel = sampled ? 'sampled pages' : 'pages';
|
|
89
|
+
const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
|
|
90
|
+
let message;
|
|
91
|
+
if (status === 'pass') {
|
|
92
|
+
const redirectCount = sameHost.length;
|
|
93
|
+
if (redirectCount === 0) {
|
|
94
|
+
message = `No redirects detected across ${tested.length} ${pageLabel}${suffix}`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
message = `All ${redirectCount} redirect(s) across ${tested.length} ${pageLabel} are same-host HTTP redirects${suffix}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (status === 'warn') {
|
|
101
|
+
message = `${crossHost.length} of ${tested.length} ${pageLabel} use cross-host redirects${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const parts = [];
|
|
105
|
+
if (jsRedirects.length > 0) {
|
|
106
|
+
parts.push(`${jsRedirects.length} JavaScript redirect(s)`);
|
|
107
|
+
}
|
|
108
|
+
if (crossHost.length > 0) {
|
|
109
|
+
parts.push(`${crossHost.length} cross-host redirect(s)`);
|
|
110
|
+
}
|
|
111
|
+
message = `${parts.join(' and ')} detected across ${tested.length} ${pageLabel}${suffix}`;
|
|
112
|
+
}
|
|
3
113
|
return {
|
|
4
|
-
id
|
|
5
|
-
category
|
|
6
|
-
status
|
|
7
|
-
message
|
|
114
|
+
id,
|
|
115
|
+
category,
|
|
116
|
+
status,
|
|
117
|
+
message,
|
|
118
|
+
details: {
|
|
119
|
+
totalPages,
|
|
120
|
+
testedPages: tested.length,
|
|
121
|
+
sampled,
|
|
122
|
+
noRedirectCount: noRedirects.length,
|
|
123
|
+
sameHostCount: sameHost.length,
|
|
124
|
+
crossHostCount: crossHost.length,
|
|
125
|
+
jsRedirectCount: jsRedirects.length,
|
|
126
|
+
fetchErrors,
|
|
127
|
+
pageResults: results,
|
|
128
|
+
discoveryWarnings: warnings,
|
|
129
|
+
},
|
|
8
130
|
};
|
|
9
131
|
}
|
|
10
132
|
registerCheck({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirect-behavior.js","sourceRoot":"","sources":["../../../src/checks/url-stability/redirect-behavior.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"redirect-behavior.js","sourceRoot":"","sources":["../../../src/checks/url-stability/redirect-behavior.ts"],"names":[],"mappings":"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"}
|