afdocs 0.2.0 → 0.3.1

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 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 are fully implemented; the rest return `skip` until completed.
10
+ afdocs runs 21 checks across 8 categories to evaluate how well your docs serve agent consumers. 14 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.
@@ -36,8 +36,14 @@ Markdown Availability
36
36
  ✗ content-negotiation: Server ignores Accept: text/markdown header (0/50 sampled pages return markdown)
37
37
  ✗ markdown-url-support: No sampled pages support .md URLs (0/50 tested)
38
38
 
39
+ URL Stability
40
+ ✓ http-status-codes: All 50 sampled pages return proper error codes for bad URLs
41
+
42
+ Authentication
43
+ ✓ auth-gate-detection: All 50 sampled pages are publicly accessible
44
+
39
45
  Summary
40
- 5 passed, 2 failed, 14 skipped (21 total)
46
+ 9 passed, 3 failed, 9 skipped (21 total)
41
47
  ```
42
48
 
43
49
  ## Install
@@ -138,7 +144,7 @@ describe('agent-friendliness', () => {
138
144
 
139
145
  ## Checks
140
146
 
141
- 21 checks across 8 categories. Checks marked with \* are stub implementations that return `skip`.
147
+ 21 checks across 8 categories. Checks marked with \* are not yet implemented and return `skip`.
142
148
 
143
149
  ### Category 1: llms.txt
144
150
 
@@ -171,13 +177,13 @@ describe('agent-friendliness', () => {
171
177
  | --------------------------------- | -------------------------------------------------- |
172
178
  | `tabbed-content-serialization` \* | Whether tabbed content creates oversized output |
173
179
  | `section-header-quality` \* | Whether headers in tabbed sections include context |
174
- | `markdown-code-fence-validity` \* | Whether markdown has unclosed code fences |
180
+ | `markdown-code-fence-validity` | Whether markdown has unclosed code fences |
175
181
 
176
182
  ### Category 5: URL Stability and Redirects
177
183
 
178
184
  | Check | Description |
179
185
  | ---------------------- | ----------------------------------------------- |
180
- | `http-status-codes` \* | Whether error pages return correct status codes |
186
+ | `http-status-codes` | Whether error pages return correct status codes |
181
187
  | `redirect-behavior` \* | Whether redirects are same-host HTTP redirects |
182
188
 
183
189
  ### Category 6: Agent Discoverability Directives
@@ -192,13 +198,13 @@ describe('agent-friendliness', () => {
192
198
  | ---------------------------- | ---------------------------------------------- |
193
199
  | `llms-txt-freshness` \* | Whether `llms.txt` reflects current site state |
194
200
  | `markdown-content-parity` \* | Whether markdown and HTML versions match |
195
- | `cache-header-hygiene` \* | Whether cache headers allow timely updates |
201
+ | `cache-header-hygiene` | Whether cache headers allow timely updates |
196
202
 
197
203
  ### Category 8: Authentication and Access
198
204
 
199
205
  | Check | Description |
200
206
  | ---------------------------- | -------------------------------------------------------------------- |
201
- | `auth-gate-detection` \* | Whether documentation pages require authentication to access content |
207
+ | `auth-gate-detection` | Whether documentation pages require authentication to access content |
202
208
  | `auth-alternative-access` \* | Whether auth-gated sites provide alternative access paths for agents |
203
209
 
204
210
  ## Check dependencies
@@ -1,10 +1,168 @@
1
1
  import { registerCheck } from '../registry.js';
2
- async function check(_ctx) {
2
+ import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
3
+ const SSO_DOMAINS = [
4
+ 'okta.com',
5
+ 'auth0.com',
6
+ 'login.microsoftonline.com',
7
+ 'accounts.google.com',
8
+ 'login.salesforce.com',
9
+ 'sso.',
10
+ 'idp.',
11
+ 'auth.',
12
+ 'login.',
13
+ ];
14
+ function isSsoDomain(url) {
15
+ try {
16
+ const hostname = new URL(url).hostname.toLowerCase();
17
+ return SSO_DOMAINS.find((domain) => hostname === domain || hostname.endsWith('.' + domain) || hostname.startsWith(domain));
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function detectLoginForm(body) {
24
+ const sample = body.slice(0, 50000).toLowerCase();
25
+ if (sample.includes('<input') && sample.includes('type="password"')) {
26
+ return 'Contains password input field';
27
+ }
28
+ // Check page title for login indicators
29
+ const titleMatch = /<title[^>]*>(.*?)<\/title>/i.exec(sample);
30
+ if (titleMatch) {
31
+ const title = titleMatch[1].toLowerCase();
32
+ if (/sign\s*in|log\s*in|authenticate/i.test(title)) {
33
+ return `Page title suggests login: "${titleMatch[1].trim()}"`;
34
+ }
35
+ }
36
+ // Check for SSO form actions
37
+ if (/<form[^>]*action\s*=\s*["'][^"']*(?:saml|oauth|openid|sso|auth)[^"']*["']/i.test(sample)) {
38
+ return 'Contains SSO-related form action';
39
+ }
40
+ return undefined;
41
+ }
42
+ async function check(ctx) {
43
+ const id = 'auth-gate-detection';
44
+ const category = 'authentication';
45
+ const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
46
+ const results = [];
47
+ const concurrency = ctx.options.maxConcurrency;
48
+ for (let i = 0; i < pageUrls.length; i += concurrency) {
49
+ const batch = pageUrls.slice(i, i + concurrency);
50
+ const batchResults = await Promise.all(batch.map(async (url) => {
51
+ try {
52
+ const response = await ctx.http.fetch(url, { redirect: 'manual' });
53
+ const status = response.status;
54
+ // Auth-required status codes
55
+ if (status === 401 || status === 403) {
56
+ return { url, classification: 'auth-required', status };
57
+ }
58
+ // Redirect — check if it's to an SSO domain
59
+ if (status >= 300 && status < 400) {
60
+ const location = response.headers.get('location');
61
+ if (location) {
62
+ const resolvedLocation = location.startsWith('http')
63
+ ? location
64
+ : new URL(location, url).toString();
65
+ const ssoDomain = isSsoDomain(resolvedLocation);
66
+ if (ssoDomain) {
67
+ return {
68
+ url,
69
+ classification: 'auth-redirect',
70
+ status,
71
+ redirectUrl: resolvedLocation,
72
+ ssoDomain,
73
+ };
74
+ }
75
+ }
76
+ // Non-SSO redirect — treat as accessible (normal redirect)
77
+ return { url, classification: 'accessible', status };
78
+ }
79
+ // 200 — check for soft auth gate (login form)
80
+ if (status === 200) {
81
+ let body;
82
+ try {
83
+ body = await response.text();
84
+ }
85
+ catch {
86
+ return { url, classification: 'accessible', status };
87
+ }
88
+ const loginHint = detectLoginForm(body);
89
+ if (loginHint) {
90
+ return { url, classification: 'soft-auth-gate', status, hint: loginHint };
91
+ }
92
+ return { url, classification: 'accessible', status };
93
+ }
94
+ // Other status codes — treat as accessible
95
+ return { url, classification: 'accessible', status };
96
+ }
97
+ catch (err) {
98
+ return {
99
+ url,
100
+ classification: 'accessible',
101
+ status: null,
102
+ error: err instanceof Error ? err.message : String(err),
103
+ };
104
+ }
105
+ }));
106
+ results.push(...batchResults);
107
+ }
108
+ const fetchErrors = results.filter((r) => r.error).length;
109
+ const tested = results.filter((r) => !r.error);
110
+ if (tested.length === 0) {
111
+ return {
112
+ id,
113
+ category,
114
+ status: 'fail',
115
+ message: `Could not fetch any pages to check authentication${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
116
+ details: {
117
+ totalPages,
118
+ testedPages: results.length,
119
+ sampled,
120
+ fetchErrors,
121
+ pageResults: results,
122
+ discoveryWarnings: warnings,
123
+ },
124
+ };
125
+ }
126
+ const accessible = tested.filter((r) => r.classification === 'accessible');
127
+ const authRequired = tested.filter((r) => r.classification === 'auth-required');
128
+ const softAuthGate = tested.filter((r) => r.classification === 'soft-auth-gate');
129
+ const authRedirect = tested.filter((r) => r.classification === 'auth-redirect');
130
+ const gatedCount = authRequired.length + softAuthGate.length + authRedirect.length;
131
+ const ssoDomains = [...new Set(authRedirect.map((r) => r.ssoDomain).filter(Boolean))];
132
+ let status;
133
+ let message;
134
+ const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
135
+ const pageLabel = sampled ? 'sampled pages' : 'pages';
136
+ if (gatedCount === 0) {
137
+ status = 'pass';
138
+ message = `All ${accessible.length} ${pageLabel} are publicly accessible${suffix}`;
139
+ }
140
+ else if (accessible.length > 0 && gatedCount > 0) {
141
+ status = 'warn';
142
+ message = `${gatedCount} of ${tested.length} ${pageLabel} require authentication (${accessible.length} accessible)${suffix}`;
143
+ }
144
+ else {
145
+ status = 'fail';
146
+ message = `All ${tested.length} ${pageLabel} require authentication${suffix}`;
147
+ }
3
148
  return {
4
- id: 'auth-gate-detection',
5
- category: 'authentication',
6
- status: 'skip',
7
- message: 'Not yet implemented',
149
+ id,
150
+ category,
151
+ status,
152
+ message,
153
+ details: {
154
+ totalPages,
155
+ testedPages: results.length,
156
+ sampled,
157
+ accessible: accessible.length,
158
+ authRequired: authRequired.length,
159
+ softAuthGate: softAuthGate.length,
160
+ authRedirect: authRedirect.length,
161
+ ssoDomains,
162
+ fetchErrors,
163
+ pageResults: results,
164
+ discoveryWarnings: warnings,
165
+ },
8
166
  };
9
167
  }
10
168
  registerCheck({
@@ -1 +1 @@
1
- {"version":3,"file":"auth-gate-detection.js","sourceRoot":"","sources":["../../../src/checks/authentication/auth-gate-detection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,KAAK,UAAU,KAAK,CAAC,IAAkB;IACrC,OAAO;QACL,EAAE,EAAE,qBAAqB;QACzB,QAAQ,EAAE,gBAAgB;QAC1B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,qBAAqB;KAC/B,CAAC;AACJ,CAAC;AAED,aAAa,CAAC;IACZ,EAAE,EAAE,qBAAqB;IACzB,QAAQ,EAAE,gBAAgB;IAC1B,WAAW,EAAE,sEAAsE;IACnF,SAAS,EAAE,EAAE;IACb,GAAG,EAAE,KAAK;CACX,CAAC,CAAC"}
1
+ {"version":3,"file":"auth-gate-detection.js","sourceRoot":"","sources":["../../../src/checks/authentication/auth-gate-detection.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,124 @@
1
1
  import { registerCheck } from '../registry.js';
2
- async function check(_ctx) {
2
+ import { getMarkdownContent } from '../../helpers/get-markdown-content.js';
3
+ /**
4
+ * Strip blockquote prefixes (`> `) from a line so that fences inside
5
+ * blockquotes are detected the same way a CommonMark parser would
6
+ * handle them. Supports nested blockquotes (`> > `).
7
+ */
8
+ function stripBlockquotePrefix(line) {
9
+ return line.replace(/^(?:\s{0,3}> ?)+/, '');
10
+ }
11
+ /** Match a line that opens or closes a code fence (after blockquote stripping). */
12
+ const FENCE_RE = /^( {0,3})((`{3,})|(~{3,}))(.*)?$/;
13
+ function analyzeFences(content) {
14
+ const lines = content.split('\n');
15
+ const issues = [];
16
+ let fenceCount = 0;
17
+ let openFence = null;
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const stripped = stripBlockquotePrefix(lines[i]);
20
+ const match = FENCE_RE.exec(stripped);
21
+ if (!match)
22
+ continue;
23
+ const char = match[3] ? '`' : '~';
24
+ const length = (match[3] || match[4]).length;
25
+ if (!openFence) {
26
+ // Opening fence
27
+ openFence = { line: i + 1, char, length };
28
+ fenceCount++;
29
+ }
30
+ else {
31
+ // Potential closing fence: must use same char and be at least as long
32
+ if (char === openFence.char && length >= openFence.length) {
33
+ // Proper close
34
+ openFence = null;
35
+ }
36
+ else if (char !== openFence.char && length >= openFence.length) {
37
+ // Inconsistent close: different delimiter type
38
+ issues.push({
39
+ line: i + 1,
40
+ type: 'inconsistent-close',
41
+ opener: openFence.char.repeat(openFence.length),
42
+ closer: char.repeat(length),
43
+ });
44
+ openFence = null;
45
+ }
46
+ // Otherwise: different char + shorter length = not a close, just content inside the fence
47
+ }
48
+ }
49
+ if (openFence) {
50
+ issues.push({
51
+ line: openFence.line,
52
+ type: 'unclosed',
53
+ opener: openFence.char.repeat(openFence.length),
54
+ });
55
+ }
56
+ return { fenceCount, issues };
57
+ }
58
+ function worstStatus(statuses) {
59
+ if (statuses.includes('fail'))
60
+ return 'fail';
61
+ if (statuses.includes('warn'))
62
+ return 'warn';
63
+ return 'pass';
64
+ }
65
+ async function check(ctx) {
66
+ const id = 'markdown-code-fence-validity';
67
+ const category = 'content-structure';
68
+ const mdResult = await getMarkdownContent(ctx);
69
+ if (mdResult.pages.length === 0) {
70
+ if (mdResult.mode === 'cached' && !mdResult.depPassed) {
71
+ return {
72
+ id,
73
+ category,
74
+ status: 'skip',
75
+ message: 'Site does not serve markdown content; nothing to analyze',
76
+ };
77
+ }
78
+ const hint = mdResult.mode === 'standalone'
79
+ ? '; try running with markdown-url-support or content-negotiation checks'
80
+ : '';
81
+ return { id, category, status: 'skip', message: `No markdown content found${hint}` };
82
+ }
83
+ const results = mdResult.pages.map(({ url, content, source }) => {
84
+ const { fenceCount, issues } = analyzeFences(content);
85
+ const hasUnclosed = issues.some((i) => i.type === 'unclosed');
86
+ const hasInconsistent = issues.some((i) => i.type === 'inconsistent-close');
87
+ let status;
88
+ if (hasUnclosed)
89
+ status = 'fail';
90
+ else if (hasInconsistent)
91
+ status = 'warn';
92
+ else
93
+ status = 'pass';
94
+ return { url, source, fenceCount, issues, status };
95
+ });
96
+ const overallStatus = worstStatus(results.map((r) => r.status));
97
+ const totalFences = results.reduce((sum, r) => sum + r.fenceCount, 0);
98
+ const unclosedCount = results.reduce((sum, r) => sum + r.issues.filter((i) => i.type === 'unclosed').length, 0);
99
+ const inconsistentCount = results.reduce((sum, r) => sum + r.issues.filter((i) => i.type === 'inconsistent-close').length, 0);
100
+ let message;
101
+ if (overallStatus === 'pass') {
102
+ message = `All ${totalFences} code fences properly closed across ${results.length} pages`;
103
+ }
104
+ else if (overallStatus === 'warn') {
105
+ message = `${inconsistentCount} code fences use inconsistent delimiters across ${results.length} pages`;
106
+ }
107
+ else {
108
+ message = `${unclosedCount} unclosed code fences found across ${results.length} pages`;
109
+ }
3
110
  return {
4
- id: 'markdown-code-fence-validity',
5
- category: 'content-structure',
6
- status: 'skip',
7
- message: 'Not yet implemented',
111
+ id,
112
+ category,
113
+ status: overallStatus,
114
+ message,
115
+ details: {
116
+ pagesAnalyzed: results.length,
117
+ totalFences,
118
+ unclosedCount,
119
+ inconsistentCount,
120
+ pageResults: results,
121
+ },
8
122
  };
9
123
  }
10
124
  registerCheck({
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-code-fence-validity.js","sourceRoot":"","sources":["../../../src/checks/content-structure/markdown-code-fence-validity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,KAAK,UAAU,KAAK,CAAC,IAAkB;IACrC,OAAO;QACL,EAAE,EAAE,8BAA8B;QAClC,QAAQ,EAAE,mBAAmB;QAC7B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,qBAAqB;KAC/B,CAAC;AACJ,CAAC;AAED,aAAa,CAAC;IACZ,EAAE,EAAE,8BAA8B;IAClC,QAAQ,EAAE,mBAAmB;IAC7B,WAAW,EAAE,gDAAgD;IAC7D,SAAS,EAAE,CAAC,CAAC,sBAAsB,EAAE,qBAAqB,CAAC,CAAC;IAC5D,GAAG,EAAE,KAAK;CACX,CAAC,CAAC"}
1
+ {"version":3,"file":"markdown-code-fence-validity.js","sourceRoot":"","sources":["../../../src/checks/content-structure/markdown-code-fence-validity.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,205 @@
1
1
  import { registerCheck } from '../registry.js';
2
- async function check(_ctx) {
2
+ import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
3
+ function parseCacheControl(header) {
4
+ if (!header)
5
+ return { maxAge: null, sMaxAge: null, mustRevalidate: false, noCache: false, noStore: false };
6
+ const directives = header
7
+ .toLowerCase()
8
+ .split(',')
9
+ .map((d) => d.trim());
10
+ let maxAge = null;
11
+ let sMaxAge = null;
12
+ let mustRevalidate = false;
13
+ let noCache = false;
14
+ let noStore = false;
15
+ for (const d of directives) {
16
+ if (d.startsWith('max-age=')) {
17
+ maxAge = parseInt(d.split('=')[1], 10);
18
+ if (isNaN(maxAge))
19
+ maxAge = null;
20
+ }
21
+ else if (d.startsWith('s-maxage=')) {
22
+ sMaxAge = parseInt(d.split('=')[1], 10);
23
+ if (isNaN(sMaxAge))
24
+ sMaxAge = null;
25
+ }
26
+ else if (d === 'must-revalidate') {
27
+ mustRevalidate = true;
28
+ }
29
+ else if (d === 'no-cache') {
30
+ noCache = true;
31
+ }
32
+ else if (d === 'no-store') {
33
+ noStore = true;
34
+ }
35
+ }
36
+ return { maxAge, sMaxAge, mustRevalidate, noCache, noStore };
37
+ }
38
+ function classifyCache(result) {
39
+ // no-cache or no-store = always fresh
40
+ if (result.noCache || result.noStore)
41
+ return 'pass';
42
+ // must-revalidate with a revalidation mechanism is good
43
+ if (result.mustRevalidate && (result.etag || result.lastModified))
44
+ return 'pass';
45
+ const effective = result.effectiveMaxAge;
46
+ if (effective === null) {
47
+ // No cache-related headers at all
48
+ if (!result.cacheControl && !result.expires && !result.etag && !result.lastModified) {
49
+ return 'fail';
50
+ }
51
+ // Has ETag/Last-Modified but no max-age — still ok (browser will revalidate)
52
+ if (result.etag || result.lastModified)
53
+ return 'pass';
54
+ return 'fail';
55
+ }
56
+ if (effective <= 3600)
57
+ return 'pass';
58
+ if (effective <= 86400)
59
+ return 'warn';
60
+ return 'fail';
61
+ }
62
+ function getEffectiveMaxAge(parsed, expires) {
63
+ // s-maxage takes precedence over max-age
64
+ if (parsed.sMaxAge !== null)
65
+ return parsed.sMaxAge;
66
+ if (parsed.maxAge !== null)
67
+ return parsed.maxAge;
68
+ // Fall back to Expires header
69
+ if (expires) {
70
+ try {
71
+ const expiresMs = new Date(expires).getTime();
72
+ const nowMs = Date.now();
73
+ if (!isNaN(expiresMs)) {
74
+ return Math.max(0, Math.round((expiresMs - nowMs) / 1000));
75
+ }
76
+ }
77
+ catch {
78
+ // invalid date
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ function worstStatus(statuses) {
84
+ if (statuses.includes('fail'))
85
+ return 'fail';
86
+ if (statuses.includes('warn'))
87
+ return 'warn';
88
+ return 'pass';
89
+ }
90
+ async function check(ctx) {
91
+ const id = 'cache-header-hygiene';
92
+ const category = 'observability';
93
+ // Collect URLs to check: llms.txt files + sampled page URLs
94
+ const urlsToCheck = [];
95
+ // llms.txt URLs
96
+ const existsResult = ctx.previousResults.get('llms-txt-exists');
97
+ const discovered = (existsResult?.details?.discoveredFiles ?? []);
98
+ for (const file of discovered) {
99
+ urlsToCheck.push(file.url);
100
+ }
101
+ // Page URLs
102
+ const { urls: pageUrls, totalPages, sampled, warnings } = await discoverAndSamplePages(ctx);
103
+ for (const url of pageUrls) {
104
+ if (!urlsToCheck.includes(url)) {
105
+ urlsToCheck.push(url);
106
+ }
107
+ }
108
+ const results = [];
109
+ const concurrency = ctx.options.maxConcurrency;
110
+ for (let i = 0; i < urlsToCheck.length; i += concurrency) {
111
+ const batch = urlsToCheck.slice(i, i + concurrency);
112
+ const batchResults = await Promise.all(batch.map(async (url) => {
113
+ try {
114
+ const response = await ctx.http.fetch(url);
115
+ const ccHeader = response.headers.get('cache-control');
116
+ const parsed = parseCacheControl(ccHeader);
117
+ const etag = response.headers.get('etag');
118
+ const lastModified = response.headers.get('last-modified');
119
+ const expires = response.headers.get('expires');
120
+ const effectiveMaxAge = getEffectiveMaxAge(parsed, expires);
121
+ const partial = {
122
+ url,
123
+ cacheControl: ccHeader,
124
+ maxAge: parsed.maxAge,
125
+ sMaxAge: parsed.sMaxAge,
126
+ mustRevalidate: parsed.mustRevalidate,
127
+ noCache: parsed.noCache,
128
+ noStore: parsed.noStore,
129
+ etag,
130
+ lastModified,
131
+ expires,
132
+ effectiveMaxAge,
133
+ };
134
+ return { ...partial, status: classifyCache(partial) };
135
+ }
136
+ catch (err) {
137
+ return {
138
+ url,
139
+ cacheControl: null,
140
+ maxAge: null,
141
+ sMaxAge: null,
142
+ mustRevalidate: false,
143
+ noCache: false,
144
+ noStore: false,
145
+ etag: null,
146
+ lastModified: null,
147
+ expires: null,
148
+ effectiveMaxAge: null,
149
+ status: 'fail',
150
+ error: err instanceof Error ? err.message : String(err),
151
+ };
152
+ }
153
+ }));
154
+ results.push(...batchResults);
155
+ }
156
+ const successful = results.filter((r) => !r.error);
157
+ const fetchErrors = results.filter((r) => r.error).length;
158
+ if (successful.length === 0) {
159
+ return {
160
+ id,
161
+ category,
162
+ status: 'fail',
163
+ message: `Could not fetch any endpoints to check cache headers${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
164
+ details: {
165
+ testedEndpoints: results.length,
166
+ fetchErrors,
167
+ endpointResults: results,
168
+ discoveryWarnings: warnings,
169
+ },
170
+ };
171
+ }
172
+ const overallStatus = worstStatus(successful.map((r) => r.status));
173
+ const passBucket = successful.filter((r) => r.status === 'pass').length;
174
+ const warnBucket = successful.filter((r) => r.status === 'warn').length;
175
+ const failBucket = successful.filter((r) => r.status === 'fail').length;
176
+ const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
177
+ let message;
178
+ if (overallStatus === 'pass') {
179
+ message = `All ${successful.length} endpoints have appropriate cache headers${suffix}`;
180
+ }
181
+ else if (overallStatus === 'warn') {
182
+ message = `${warnBucket} of ${successful.length} endpoints have moderate cache lifetimes (1–24 hours)${suffix}`;
183
+ }
184
+ else {
185
+ message = `${failBucket} of ${successful.length} endpoints have aggressive caching or missing cache headers${suffix}`;
186
+ }
3
187
  return {
4
- id: 'cache-header-hygiene',
5
- category: 'observability',
6
- status: 'skip',
7
- message: 'Not yet implemented',
188
+ id,
189
+ category,
190
+ status: overallStatus,
191
+ message,
192
+ details: {
193
+ totalPages,
194
+ testedEndpoints: results.length,
195
+ sampled,
196
+ passBucket,
197
+ warnBucket,
198
+ failBucket,
199
+ fetchErrors,
200
+ endpointResults: results,
201
+ discoveryWarnings: warnings,
202
+ },
8
203
  };
9
204
  }
10
205
  registerCheck({
@@ -1 +1 @@
1
- {"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,KAAK,UAAU,KAAK,CAAC,IAAkB;IACrC,OAAO;QACL,EAAE,EAAE,sBAAsB;QAC1B,QAAQ,EAAE,eAAe;QACzB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,qBAAqB;KAC/B,CAAC;AACJ,CAAC;AAED,aAAa,CAAC;IACZ,EAAE,EAAE,sBAAsB;IAC1B,QAAQ,EAAE,eAAe;IACzB,WAAW,EAAE,4CAA4C;IACzD,SAAS,EAAE,EAAE;IACb,GAAG,EAAE,KAAK;CACX,CAAC,CAAC"}
1
+ {"version":3,"file":"cache-header-hygiene.js","sourceRoot":"","sources":["../../../src/checks/observability/cache-header-hygiene.ts"],"names":[],"mappings":"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"}