afdocs 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +48 -24
  2. package/dist/checks/authentication/auth-alternative-access.d.ts +2 -0
  3. package/dist/checks/authentication/auth-alternative-access.d.ts.map +1 -0
  4. package/dist/checks/authentication/auth-alternative-access.js +17 -0
  5. package/dist/checks/authentication/auth-alternative-access.js.map +1 -0
  6. package/dist/checks/authentication/auth-gate-detection.d.ts +2 -0
  7. package/dist/checks/authentication/auth-gate-detection.d.ts.map +1 -0
  8. package/dist/checks/authentication/auth-gate-detection.js +175 -0
  9. package/dist/checks/authentication/auth-gate-detection.js.map +1 -0
  10. package/dist/checks/content-structure/markdown-code-fence-validity.js +119 -5
  11. package/dist/checks/content-structure/markdown-code-fence-validity.js.map +1 -1
  12. package/dist/checks/index.d.ts +2 -0
  13. package/dist/checks/index.d.ts.map +1 -1
  14. package/dist/checks/index.js +3 -0
  15. package/dist/checks/index.js.map +1 -1
  16. package/dist/checks/llms-txt/llms-txt-exists.js +83 -6
  17. package/dist/checks/llms-txt/llms-txt-exists.js.map +1 -1
  18. package/dist/checks/llms-txt/llms-txt-links-markdown.js +46 -21
  19. package/dist/checks/llms-txt/llms-txt-links-markdown.js.map +1 -1
  20. package/dist/checks/llms-txt/llms-txt-links-resolve.js +10 -3
  21. package/dist/checks/llms-txt/llms-txt-links-resolve.js.map +1 -1
  22. package/dist/checks/markdown-availability/content-negotiation.js +101 -5
  23. package/dist/checks/markdown-availability/content-negotiation.js.map +1 -1
  24. package/dist/checks/markdown-availability/markdown-url-support.js +82 -5
  25. package/dist/checks/markdown-availability/markdown-url-support.js.map +1 -1
  26. package/dist/checks/observability/cache-header-hygiene.js +200 -5
  27. package/dist/checks/observability/cache-header-hygiene.js.map +1 -1
  28. package/dist/checks/page-size/content-start-position.js +161 -5
  29. package/dist/checks/page-size/content-start-position.js.map +1 -1
  30. package/dist/checks/page-size/page-size-html.js +127 -5
  31. package/dist/checks/page-size/page-size-html.js.map +1 -1
  32. package/dist/checks/page-size/page-size-markdown.js +100 -5
  33. package/dist/checks/page-size/page-size-markdown.js.map +1 -1
  34. package/dist/checks/url-stability/http-status-codes.js +106 -5
  35. package/dist/checks/url-stability/http-status-codes.js.map +1 -1
  36. package/dist/cli/commands/check.d.ts.map +1 -1
  37. package/dist/cli/commands/check.js +9 -1
  38. package/dist/cli/commands/check.js.map +1 -1
  39. package/dist/cli/formatters/text.d.ts +4 -1
  40. package/dist/cli/formatters/text.d.ts.map +1 -1
  41. package/dist/cli/formatters/text.js +94 -1
  42. package/dist/cli/formatters/text.js.map +1 -1
  43. package/dist/constants.d.ts +6 -0
  44. package/dist/constants.d.ts.map +1 -1
  45. package/dist/constants.js +3 -0
  46. package/dist/constants.js.map +1 -1
  47. package/dist/helpers/detect-markdown.d.ts +10 -0
  48. package/dist/helpers/detect-markdown.d.ts.map +1 -0
  49. package/dist/helpers/detect-markdown.js +29 -0
  50. package/dist/helpers/detect-markdown.js.map +1 -0
  51. package/dist/helpers/get-markdown-content.d.ts +23 -0
  52. package/dist/helpers/get-markdown-content.d.ts.map +1 -0
  53. package/dist/helpers/get-markdown-content.js +93 -0
  54. package/dist/helpers/get-markdown-content.js.map +1 -0
  55. package/dist/helpers/get-page-urls.d.ts +42 -0
  56. package/dist/helpers/get-page-urls.d.ts.map +1 -0
  57. package/dist/helpers/get-page-urls.js +236 -0
  58. package/dist/helpers/get-page-urls.js.map +1 -0
  59. package/dist/helpers/html-to-markdown.d.ts +7 -0
  60. package/dist/helpers/html-to-markdown.d.ts.map +1 -0
  61. package/dist/helpers/html-to-markdown.js +11 -0
  62. package/dist/helpers/html-to-markdown.js.map +1 -0
  63. package/dist/helpers/index.d.ts +5 -0
  64. package/dist/helpers/index.d.ts.map +1 -1
  65. package/dist/helpers/index.js +4 -0
  66. package/dist/helpers/index.js.map +1 -1
  67. package/dist/helpers/to-md-urls.d.ts +7 -0
  68. package/dist/helpers/to-md-urls.d.ts.map +1 -0
  69. package/dist/helpers/to-md-urls.js +24 -0
  70. package/dist/helpers/to-md-urls.js.map +1 -0
  71. package/dist/runner.d.ts.map +1 -1
  72. package/dist/runner.js +21 -14
  73. package/dist/runner.js.map +1 -1
  74. package/dist/types.d.ts +13 -1
  75. package/dist/types.d.ts.map +1 -1
  76. package/package.json +4 -2
@@ -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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"}
@@ -1,10 +1,166 @@
1
1
  import { registerCheck } from '../registry.js';
2
- async function check(_ctx) {
2
+ import { looksLikeHtml } from '../../helpers/detect-markdown.js';
3
+ import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
4
+ import { htmlToMarkdown } from '../../helpers/html-to-markdown.js';
5
+ const CSS_PATTERN = /[{}\s]*[a-z-]+\s*:\s*[^;]+;/;
6
+ const JS_PATTERNS = [/^\s*(function|var|const|let|import|export)\b/, /^\s*\/\//, /[{};]\s*$/];
7
+ const NAV_MAX_LENGTH = 40;
8
+ /**
9
+ * Find the character position where meaningful content begins in converted markdown.
10
+ * Meaningful content is a heading or a prose paragraph (not CSS, JS, or short nav text).
11
+ */
12
+ function findContentStart(markdown) {
13
+ const lines = markdown.split('\n');
14
+ let charPos = 0;
15
+ for (let idx = 0; idx < lines.length; idx++) {
16
+ const line = lines[idx];
17
+ const trimmed = line.trim();
18
+ if (trimmed.length === 0) {
19
+ charPos += line.length + 1;
20
+ continue;
21
+ }
22
+ // ATX heading: starts with # at beginning of line
23
+ if (/^#{1,6}\s+\S/.test(trimmed)) {
24
+ return charPos;
25
+ }
26
+ // Setext heading: current line is text, next line is === or ---
27
+ const nextLine = idx + 1 < lines.length ? lines[idx + 1].trim() : '';
28
+ if (/^[=-]+$/.test(nextLine) && nextLine.length >= 2 && trimmed.length > 0) {
29
+ return charPos;
30
+ }
31
+ // Skip CSS-like lines
32
+ if (CSS_PATTERN.test(trimmed)) {
33
+ charPos += line.length + 1;
34
+ continue;
35
+ }
36
+ // Skip JS-like lines
37
+ if (JS_PATTERNS.some((p) => p.test(trimmed))) {
38
+ charPos += line.length + 1;
39
+ continue;
40
+ }
41
+ // Skip very short nav-like tokens (e.g., "Home", "Docs", "API")
42
+ if (trimmed.length <= NAV_MAX_LENGTH && !/[.!?]/.test(trimmed) && !trimmed.includes(' ')) {
43
+ charPos += line.length + 1;
44
+ continue;
45
+ }
46
+ // Prose-like paragraph: contains spaces (multiple words) and is reasonably long
47
+ if (trimmed.length > NAV_MAX_LENGTH || (trimmed.includes(' ') && trimmed.length > 20)) {
48
+ return charPos;
49
+ }
50
+ charPos += line.length + 1;
51
+ }
52
+ // If nothing matched, content starts at the end (all boilerplate)
53
+ return charPos;
54
+ }
55
+ function positionStatus(percent) {
56
+ if (percent <= 10)
57
+ return 'pass';
58
+ if (percent <= 50)
59
+ return 'warn';
60
+ return 'fail';
61
+ }
62
+ function worstStatus(statuses) {
63
+ if (statuses.includes('fail'))
64
+ return 'fail';
65
+ if (statuses.includes('warn'))
66
+ return 'warn';
67
+ return 'pass';
68
+ }
69
+ async function check(ctx) {
70
+ const id = 'content-start-position';
71
+ const category = 'page-size';
72
+ const { urls: pageUrls, totalPages, sampled: wasSampled, warnings, } = await discoverAndSamplePages(ctx);
73
+ const results = [];
74
+ const concurrency = ctx.options.maxConcurrency;
75
+ for (let i = 0; i < pageUrls.length; i += concurrency) {
76
+ const batch = pageUrls.slice(i, i + concurrency);
77
+ const batchResults = await Promise.all(batch.map(async (url) => {
78
+ try {
79
+ const response = await ctx.http.fetch(url);
80
+ const body = await response.text();
81
+ const contentType = response.headers.get('content-type') ?? '';
82
+ const isMarkdownType = contentType.includes('text/markdown') || contentType.includes('text/plain');
83
+ const isHtml = !isMarkdownType && (contentType.includes('text/html') || looksLikeHtml(body));
84
+ const markdown = isHtml ? htmlToMarkdown(body) : body;
85
+ const totalChars = markdown.length;
86
+ const contentStartChar = findContentStart(markdown);
87
+ const contentStartPercent = totalChars > 0 ? Math.round((contentStartChar / totalChars) * 100) : 0;
88
+ return {
89
+ url,
90
+ contentStartChar,
91
+ totalChars,
92
+ contentStartPercent,
93
+ status: positionStatus(contentStartPercent),
94
+ };
95
+ }
96
+ catch (err) {
97
+ return {
98
+ url,
99
+ contentStartChar: 0,
100
+ totalChars: 0,
101
+ contentStartPercent: 100,
102
+ status: 'fail',
103
+ error: err instanceof Error ? err.message : String(err),
104
+ };
105
+ }
106
+ }));
107
+ results.push(...batchResults);
108
+ }
109
+ const successful = results.filter((r) => !r.error);
110
+ const fetchErrors = results.filter((r) => r.error).length;
111
+ if (successful.length === 0) {
112
+ return {
113
+ id,
114
+ category,
115
+ status: 'fail',
116
+ message: `Could not fetch any pages to analyze${fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : ''}`,
117
+ details: {
118
+ totalPages,
119
+ testedPages: results.length,
120
+ sampled: wasSampled,
121
+ fetchErrors,
122
+ pageResults: results,
123
+ discoveryWarnings: warnings,
124
+ },
125
+ };
126
+ }
127
+ const percents = successful.map((r) => r.contentStartPercent);
128
+ const medianPercent = percents.sort((a, b) => a - b)[Math.floor(percents.length / 2)];
129
+ const maxPercent = Math.max(...percents);
130
+ const overallStatus = worstStatus(successful.map((r) => r.status));
131
+ const pageLabel = wasSampled ? 'sampled pages' : 'pages';
132
+ const passBucket = successful.filter((r) => r.status === 'pass').length;
133
+ const warnBucket = successful.filter((r) => r.status === 'warn').length;
134
+ const failBucket = successful.filter((r) => r.status === 'fail').length;
135
+ const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
136
+ let message;
137
+ if (overallStatus === 'pass') {
138
+ message = `Content starts within first 10% on all ${successful.length} ${pageLabel} (median ${medianPercent}%)${suffix}`;
139
+ }
140
+ else if (overallStatus === 'warn') {
141
+ message = `${warnBucket} of ${successful.length} ${pageLabel} have content starting at 10–50% (worst ${maxPercent}%)${suffix}`;
142
+ }
143
+ else {
144
+ message = `${failBucket} of ${successful.length} ${pageLabel} have content starting past 50% (worst ${maxPercent}%)${suffix}`;
145
+ }
3
146
  return {
4
- id: 'content-start-position',
5
- category: 'page-size',
6
- status: 'skip',
7
- message: 'Not yet implemented',
147
+ id,
148
+ category,
149
+ status: overallStatus,
150
+ message,
151
+ details: {
152
+ totalPages,
153
+ testedPages: results.length,
154
+ sampled: wasSampled,
155
+ medianPercent,
156
+ maxPercent,
157
+ passBucket,
158
+ warnBucket,
159
+ failBucket,
160
+ fetchErrors,
161
+ pageResults: results,
162
+ discoveryWarnings: warnings,
163
+ },
8
164
  };
9
165
  }
10
166
  registerCheck({
@@ -1 +1 @@
1
- {"version":3,"file":"content-start-position.js","sourceRoot":"","sources":["../../../src/checks/page-size/content-start-position.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,KAAK,UAAU,KAAK,CAAC,IAAkB;IACrC,OAAO;QACL,EAAE,EAAE,wBAAwB;QAC5B,QAAQ,EAAE,WAAW;QACrB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,qBAAqB;KAC/B,CAAC;AACJ,CAAC;AAED,aAAa,CAAC;IACZ,EAAE,EAAE,wBAAwB;IAC5B,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,sDAAsD;IACnE,SAAS,EAAE,EAAE;IACb,GAAG,EAAE,KAAK;CACX,CAAC,CAAC"}
1
+ {"version":3,"file":"content-start-position.js","sourceRoot":"","sources":["../../../src/checks/page-size/content-start-position.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,132 @@
1
1
  import { registerCheck } from '../registry.js';
2
- async function check(_ctx) {
2
+ import { looksLikeHtml } from '../../helpers/detect-markdown.js';
3
+ import { discoverAndSamplePages } from '../../helpers/get-page-urls.js';
4
+ import { htmlToMarkdown } from '../../helpers/html-to-markdown.js';
5
+ function sizeStatus(chars, pass, fail) {
6
+ if (chars <= pass)
7
+ return 'pass';
8
+ if (chars <= fail)
9
+ return 'warn';
10
+ return 'fail';
11
+ }
12
+ function worstStatus(statuses) {
13
+ if (statuses.includes('fail'))
14
+ return 'fail';
15
+ if (statuses.includes('warn'))
16
+ return 'warn';
17
+ return 'pass';
18
+ }
19
+ function formatSize(chars) {
20
+ if (chars >= 1000)
21
+ return `${Math.round(chars / 1000)}K`;
22
+ return String(chars);
23
+ }
24
+ async function check(ctx) {
25
+ const id = 'page-size-html';
26
+ const category = 'page-size';
27
+ const { pass: passThreshold, fail: failThreshold } = ctx.options.thresholds;
28
+ const { urls: pageUrls, totalPages, sampled: wasSampled, warnings, } = await discoverAndSamplePages(ctx);
29
+ const results = [];
30
+ const concurrency = ctx.options.maxConcurrency;
31
+ for (let i = 0; i < pageUrls.length; i += concurrency) {
32
+ const batch = pageUrls.slice(i, i + concurrency);
33
+ const batchResults = await Promise.all(batch.map(async (url) => {
34
+ try {
35
+ const response = await ctx.http.fetch(url);
36
+ const body = await response.text();
37
+ const contentType = response.headers.get('content-type') ?? '';
38
+ const isMarkdownType = contentType.includes('text/markdown') || contentType.includes('text/plain');
39
+ const isHtml = !isMarkdownType && (contentType.includes('text/html') || looksLikeHtml(body));
40
+ // Skip conversion if the response is already markdown
41
+ const html = isHtml ? body : '';
42
+ const htmlChars = html.length;
43
+ const converted = isHtml ? htmlToMarkdown(body) : body;
44
+ const convertedChars = converted.length;
45
+ const ratio = htmlChars > 0 ? Math.round((1 - convertedChars / htmlChars) * 100) : 0;
46
+ return {
47
+ url,
48
+ htmlCharacters: htmlChars,
49
+ convertedCharacters: convertedChars,
50
+ conversionRatio: ratio,
51
+ status: sizeStatus(convertedChars, passThreshold, failThreshold),
52
+ };
53
+ }
54
+ catch (err) {
55
+ return {
56
+ url,
57
+ htmlCharacters: 0,
58
+ convertedCharacters: 0,
59
+ conversionRatio: 0,
60
+ status: 'fail',
61
+ error: err instanceof Error ? err.message : String(err),
62
+ };
63
+ }
64
+ }));
65
+ results.push(...batchResults);
66
+ }
67
+ const successful = results.filter((r) => !r.error);
68
+ const fetchErrors = results.filter((r) => r.error).length;
69
+ const rateLimited = results.filter((r) => r.error && r.error.includes('429')).length;
70
+ if (successful.length === 0) {
71
+ const suffix = fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '';
72
+ return {
73
+ id,
74
+ category,
75
+ status: 'fail',
76
+ message: `Could not fetch any pages to measure${suffix}`,
77
+ details: {
78
+ totalPages,
79
+ testedPages: results.length,
80
+ sampled: wasSampled,
81
+ fetchErrors,
82
+ rateLimited,
83
+ pageResults: results,
84
+ discoveryWarnings: warnings,
85
+ },
86
+ };
87
+ }
88
+ const convertedSizes = successful.map((r) => r.convertedCharacters).sort((a, b) => a - b);
89
+ const median = convertedSizes[Math.floor(convertedSizes.length / 2)];
90
+ const max = convertedSizes[convertedSizes.length - 1];
91
+ const avgRatio = Math.round(successful.reduce((sum, r) => sum + r.conversionRatio, 0) / successful.length);
92
+ const overallStatus = worstStatus(successful.map((r) => r.status));
93
+ const pageLabel = wasSampled ? 'sampled pages' : 'pages';
94
+ const passBucket = successful.filter((r) => r.status === 'pass').length;
95
+ const warnBucket = successful.filter((r) => r.status === 'warn').length;
96
+ const failBucket = successful.filter((r) => r.status === 'fail').length;
97
+ const suffix = (fetchErrors > 0 ? `; ${fetchErrors} failed to fetch` : '') +
98
+ (rateLimited > 0 ? `; ${rateLimited} rate-limited (HTTP 429)` : '');
99
+ let message;
100
+ if (overallStatus === 'pass') {
101
+ message = `All ${successful.length} ${pageLabel} convert under ${formatSize(passThreshold)} chars (median ${formatSize(median)}, ${avgRatio}% boilerplate)${suffix}`;
102
+ }
103
+ else if (overallStatus === 'warn') {
104
+ message = `${warnBucket} of ${successful.length} ${pageLabel} convert to ${formatSize(passThreshold)}–${formatSize(failThreshold)} chars (max ${formatSize(max)}, ${avgRatio}% boilerplate)${suffix}`;
105
+ }
106
+ else {
107
+ message = `${failBucket} of ${successful.length} ${pageLabel} convert to over ${formatSize(failThreshold)} chars (max ${formatSize(max)}, ${avgRatio}% boilerplate)${suffix}`;
108
+ }
3
109
  return {
4
- id: 'page-size-html',
5
- category: 'page-size',
6
- status: 'skip',
7
- message: 'Not yet implemented',
110
+ id,
111
+ category,
112
+ status: overallStatus,
113
+ message,
114
+ details: {
115
+ totalPages,
116
+ testedPages: results.length,
117
+ sampled: wasSampled,
118
+ median,
119
+ max,
120
+ avgBoilerplatePercent: avgRatio,
121
+ passBucket,
122
+ warnBucket,
123
+ failBucket,
124
+ fetchErrors,
125
+ rateLimited,
126
+ thresholds: { pass: passThreshold, fail: failThreshold },
127
+ pageResults: results,
128
+ discoveryWarnings: warnings,
129
+ },
8
130
  };
9
131
  }
10
132
  registerCheck({
@@ -1 +1 @@
1
- {"version":3,"file":"page-size-html.js","sourceRoot":"","sources":["../../../src/checks/page-size/page-size-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,KAAK,UAAU,KAAK,CAAC,IAAkB;IACrC,OAAO;QACL,EAAE,EAAE,gBAAgB;QACpB,QAAQ,EAAE,WAAW;QACrB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,qBAAqB;KAC/B,CAAC;AACJ,CAAC;AAED,aAAa,CAAC;IACZ,EAAE,EAAE,gBAAgB;IACpB,QAAQ,EAAE,WAAW;IACrB,WAAW,EAAE,2DAA2D;IACxE,SAAS,EAAE,EAAE;IACb,GAAG,EAAE,KAAK;CACX,CAAC,CAAC"}
1
+ {"version":3,"file":"page-size-html.js","sourceRoot":"","sources":["../../../src/checks/page-size/page-size-html.ts"],"names":[],"mappings":"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"}