@forkpoint/agent-lighthouse-core 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -41,6 +41,19 @@ interface FetchResult {
41
41
  contentLength: number;
42
42
  /** Raw response bytes. Present only when the request asked for `binary`. */
43
43
  bytes?: Uint8Array;
44
+ /**
45
+ * Every redirect hop the fetcher walked, in order. Absent when the caller
46
+ * disabled redirects or the response was not a redirect.
47
+ *
48
+ * `finalUrl` alone cannot say whether a host change was permanent. A scan
49
+ * has to tell a domain migration (301/308) from a temporary hop to somebody
50
+ * else's domain, so the status of each hop is kept.
51
+ */
52
+ redirectChain?: Array<{
53
+ status: number;
54
+ from: string;
55
+ to: string;
56
+ }>;
44
57
  error?: string;
45
58
  }
46
59
  /**
@@ -154,7 +167,25 @@ interface AuditMeta {
154
167
  tier?: AuditTier;
155
168
  /** Repo-relative path to the audit's evidence dossier. */
156
169
  dossier?: string;
170
+ /**
171
+ * Which classes of scan evidence this audit needs to say anything true.
172
+ *
173
+ * Declared per audit rather than inferred at runtime, and checked against
174
+ * what the source actually reads by `scripts/check-requires.mjs`. An audit
175
+ * that reads the sampled pages — directly or through a page-fed gatherer —
176
+ * needs all four keys; one that reads only root files needs the origin to
177
+ * have answered. The deliberate disagreements are the audits whose subject
178
+ * *is* the missing evidence, and they are listed in that script.
179
+ */
180
+ requires?: EvidenceKey[];
157
181
  }
182
+ /**
183
+ * A class of evidence a scan either obtained or did not.
184
+ *
185
+ * Declared here rather than in `scan-evidence.ts` so `AuditMeta` can name it
186
+ * without the audit layer importing the scan layer.
187
+ */
188
+ type EvidenceKey = 'origin-reachable' | 'unblocked-fetches' | 'rendered-body' | 'sample-adequate';
158
189
  interface AuditResult {
159
190
  status: CheckStatus;
160
191
  score: number;
@@ -231,12 +262,31 @@ interface CategoryResult {
231
262
  failCount: number;
232
263
  }
233
264
  type ScoreTier = 'agent-ready' | 'partially-ready' | 'needs-work' | 'not-ready';
265
+ /**
266
+ * Whether a verdict about this site can mean anything, and what is missing.
267
+ *
268
+ * A scan that fetched nothing still produced a number before this existed:
269
+ * `ridge.com` scored 43 and `westontable.com` 37 on scans that read no page,
270
+ * against a real store's 51. The numbers overlap, so a reader could not
271
+ * separate "mediocre site" from "we never saw this site". When `judgeable` is
272
+ * false the report carries no score at all — not a zero, not a low number.
273
+ */
274
+ interface ScanValidity {
275
+ judgeable: boolean;
276
+ evidence: Record<EvidenceKey, boolean>;
277
+ reasons: Partial<Record<EvidenceKey, string>>;
278
+ /** Why the score was suppressed, when it was. */
279
+ unscoredReason?: string;
280
+ }
234
281
  interface ScanReport {
235
282
  scanId: string;
236
283
  url: string;
237
284
  domain: string;
238
- overallScore: number;
239
- scoreTier: ScoreTier;
285
+ /** Null when the scan obtained too little evidence to judge the site. */
286
+ overallScore: number | null;
287
+ /** Null exactly when `overallScore` is. */
288
+ scoreTier: ScoreTier | null;
289
+ scanValidity?: ScanValidity;
240
290
  summary?: string;
241
291
  categories: CategoryResult[];
242
292
  topPasses: CheckResult[];
@@ -269,6 +319,39 @@ interface ReadinessVitals {
269
319
  technical: number;
270
320
  }
271
321
 
322
+ /**
323
+ * What a scan actually obtained, decided once, before any audit runs.
324
+ *
325
+ * An audit that runs on evidence the scan never got reports a verdict about
326
+ * the scanner, not about the site: "no structured data" when the fetch was
327
+ * refused, "0 words" when the origin never answered. This module names the
328
+ * classes of evidence a scan can be missing so those audits can be skipped
329
+ * with the reason attached instead of answering blind.
330
+ *
331
+ * Pure: no network, no IO. It reads what the orchestrator already has.
332
+ */
333
+
334
+ interface ScanEvidence {
335
+ met: Record<EvidenceKey, boolean>;
336
+ /** One sentence per unmet key, shown in the `na` stub and the trace. */
337
+ reasons: Partial<Record<EvidenceKey, string>>;
338
+ /** Page URL to "the served HTML carried text a non-JS consumer can read". */
339
+ renderedByPage: Record<string, boolean>;
340
+ usablePageTypes: Set<PageType>;
341
+ /** False when no verdict about this site can mean anything. */
342
+ judgeable: boolean;
343
+ }
344
+ interface ScanEvidenceInput {
345
+ requestedUrl: string;
346
+ homepageResult: FetchResult;
347
+ pages: PageContext[];
348
+ rootFiles: Record<string, FetchResult>;
349
+ wafProtection: WafProtection | null;
350
+ }
351
+ declare function buildScanEvidence(input: ScanEvidenceInput): ScanEvidence;
352
+ /** All requirements met. For test harnesses not exercising the gate. */
353
+ declare function allEvidenceMet(): ScanEvidence;
354
+
272
355
  interface A11yNodeFinding {
273
356
  target: string;
274
357
  summary: string;
@@ -316,6 +399,14 @@ interface CheckContext {
316
399
  baseUrl: string;
317
400
  fetch: (options: FetchOptions) => Promise<FetchResult>;
318
401
  wafProtection?: WafProtection;
402
+ /**
403
+ * What the scan actually obtained, decided once before any audit ran.
404
+ *
405
+ * Required, not optional. An optional field fails open, and a caller that
406
+ * forgets is exactly the silent-nothing verdict this exists to remove. Test
407
+ * harnesses that do not exercise the gate pass `allEvidenceMet()`.
408
+ */
409
+ evidence: ScanEvidence;
319
410
  }
320
411
  type CheckFn = (ctx: CheckContext) => CheckResult | Promise<CheckResult>;
321
412
 
@@ -441,9 +532,10 @@ interface AuditTrace {
441
532
  /**
442
533
  * `ran` — the audit executed and returned.
443
534
  * `skipped` — no scanned page matched its `applicablePageTypes`.
535
+ * `gated` — the scan never obtained evidence the audit needs.
444
536
  * `error` — it threw, or its result was rejected by the schema.
445
537
  */
446
- outcome: 'ran' | 'skipped' | 'error';
538
+ outcome: 'ran' | 'skipped' | 'gated' | 'error';
447
539
  status: CheckStatus;
448
540
  score: number;
449
541
  weight: number;
@@ -457,7 +549,7 @@ interface AuditTrace {
457
549
  /** The structured evidence behind the verdict, as the report carries it. */
458
550
  details?: CheckResult['details'];
459
551
  }
460
- /** Which of the three outcomes a finished check represents. */
552
+ /** Which outcome a finished check represents. */
461
553
  declare function outcomeOf(check: CheckResult): AuditTrace['outcome'];
462
554
  /** Build a trace record from a finished check. */
463
555
  declare function traceFromCheck(check: CheckResult, durationMs: number): AuditTrace;
@@ -496,12 +588,23 @@ interface AuditPlan {
496
588
  }>;
497
589
  skipped: CheckResult[];
498
590
  }
591
+ /** How `planAudits` should treat audits the scan cannot feed. */
592
+ interface PlanOptions {
593
+ /**
594
+ * Skip audits whose `requires` the scan did not obtain.
595
+ *
596
+ * Off by default while the gate is calibrated: turning it on changes what
597
+ * every blocked or client-rendered scan reports, and that lands as one
598
+ * deliberate change rather than as a side effect of this one.
599
+ */
600
+ enforceEvidence?: boolean;
601
+ }
499
602
  /**
500
603
  * Split a scan config into the audits that will actually execute and the
501
- * page-type-skipped `na` stubs. Exported so the orchestrator can size the
604
+ * `na` stubs for those it will not. Exported so the orchestrator can size the
502
605
  * audits progress phase before running it.
503
606
  */
504
- declare function planAudits(ctx: CheckContext, config: ScanConfig): AuditPlan;
607
+ declare function planAudits(ctx: CheckContext, config: ScanConfig, options?: PlanOptions): AuditPlan;
505
608
  /**
506
609
  * Execute all audits registered in the config against the scan context.
507
610
  * Returns check results grouped into categories with weighted scores.
@@ -545,7 +648,8 @@ type ScanEvent = {
545
648
  } | {
546
649
  type: 'scan:done';
547
650
  durationMs: number;
548
- score: number;
651
+ /** Null when the scan obtained too little evidence to judge the site. */
652
+ score: number | null;
549
653
  fraction: number;
550
654
  elapsedMs: number;
551
655
  };
@@ -583,7 +687,7 @@ declare class ProgressTracker {
583
687
  /** A failed unit still counts as settled work so the phase can complete. */
584
688
  unitFail(label: string, error: string): void;
585
689
  phaseDone(): void;
586
- scanDone(score: number): void;
690
+ scanDone(score: number | null): void;
587
691
  }
588
692
 
589
693
  interface ScanOptions {
@@ -607,6 +711,15 @@ interface ScanOptions {
607
711
  * evidence it was drawn from; see {@link AuditTrace}.
608
712
  */
609
713
  onAuditTrace?: AuditTraceHandler;
714
+ /**
715
+ * Skip audits the scan could not feed, instead of running them blind.
716
+ *
717
+ * On by default. An audit whose required evidence the scan never obtained
718
+ * reports `na` tagged `skipped:no-evidence`, with the reason attached, and
719
+ * is never constructed. Set it to `false` to run every audit regardless —
720
+ * useful when measuring what the gate removes.
721
+ */
722
+ enforceEvidenceGate?: boolean;
610
723
  }
611
724
  declare function runScan(url: string, options?: ScanOptions): Promise<ScanReport>;
612
725
 
@@ -910,7 +1023,28 @@ declare function extractHeadings($: CheerioAPI): Array<{
910
1023
  level: number;
911
1024
  text: string;
912
1025
  }>;
1026
+ /**
1027
+ * The text of the page's main content region.
1028
+ *
1029
+ * Answers what the main content says, so page chrome — navigation, headers,
1030
+ * footers, cookie banners — stays out of it. Among several <main> elements the
1031
+ * one holding the most text wins: themes ship empty or fragmented <main>
1032
+ * wrappers, and the first one is often a short stub. Falls back to the whole
1033
+ * <body> only when no <main> holds any text.
1034
+ *
1035
+ * For shell detection — did the server render any readable text at all — use
1036
+ * `getRenderedText` instead.
1037
+ */
913
1038
  declare function getMainContentText($: CheerioAPI): string;
1039
+ /**
1040
+ * The text of the served HTML body, minus script/style/noscript/template.
1041
+ *
1042
+ * Answers whether the server rendered any readable text at all, so page chrome
1043
+ * counts: a shell that ships only a nav and a footer is exactly what this has
1044
+ * to see. Use `getMainContentText` when the question is what the main content
1045
+ * says rather than whether anything was served.
1046
+ */
1047
+ declare function getRenderedText($: CheerioAPI): string;
914
1048
  declare function getWordCount($: CheerioAPI): number;
915
1049
  declare function extractImages($: CheerioAPI): Array<{
916
1050
  src: string;
@@ -963,6 +1097,8 @@ declare const MAX_CONCURRENT_REQUESTS = 10;
963
1097
  declare const SCANNER_USER_AGENT = "AgentLighthouse/1.0 (+https://github.com/ForkPoint/agent-lighthouse)";
964
1098
  declare const TAG_SKIPPED_PAGE_TYPE = "skipped:page-type";
965
1099
  declare const TAG_SCAN_ERROR = "scan-error";
1100
+ /** An audit the scan could not feed: it needs evidence this scan never got. */
1101
+ declare const TAG_SKIPPED_NO_EVIDENCE = "skipped:no-evidence";
966
1102
  /** Display names for the 8 v2 categories. */
967
1103
  declare const CATEGORY_NAMES: Record<string, string>;
968
1104
  declare const READINESS_WEIGHTS: {
@@ -1113,6 +1249,7 @@ declare const EvidenceGradeSchema: z.ZodEnum<["A", "B", "C", "D"]>;
1113
1249
  declare const AuditTierSchema: z.ZodEnum<["scored", "informative", "experimental"]>;
1114
1250
  /** v2 audit identity: `category/slug`, stable across releases (spec §6). */
1115
1251
  declare const AUDIT_ID_PATTERN: RegExp;
1252
+ declare const EvidenceKeySchema: z.ZodEnum<["origin-reachable", "unblocked-fetches", "rendered-body", "sample-adequate"]>;
1116
1253
  declare const AuditMetaSchema: z.ZodObject<{
1117
1254
  id: z.ZodString;
1118
1255
  category: z.ZodString;
@@ -1158,6 +1295,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1158
1295
  evidenceGrade: z.ZodEnum<["A", "B", "C", "D"]>;
1159
1296
  tier: z.ZodEnum<["scored", "informative", "experimental"]>;
1160
1297
  dossier: z.ZodString;
1298
+ requires: z.ZodOptional<z.ZodArray<z.ZodEnum<["origin-reachable", "unblocked-fetches", "rendered-body", "sample-adequate"]>, "many">>;
1161
1299
  }, "strip", z.ZodTypeAny, {
1162
1300
  category: string;
1163
1301
  title: string;
@@ -1183,6 +1321,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1183
1321
  link: string;
1184
1322
  notice: string;
1185
1323
  } | undefined;
1324
+ requires?: ("origin-reachable" | "unblocked-fetches" | "rendered-body" | "sample-adequate")[] | undefined;
1186
1325
  }, {
1187
1326
  category: string;
1188
1327
  title: string;
@@ -1208,6 +1347,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1208
1347
  link: string;
1209
1348
  notice: string;
1210
1349
  } | undefined;
1350
+ requires?: ("origin-reachable" | "unblocked-fetches" | "rendered-body" | "sample-adequate")[] | undefined;
1211
1351
  }>;
1212
1352
  declare const CheckResultSchema: z.ZodObject<{
1213
1353
  id: z.ZodString;
@@ -1376,4 +1516,4 @@ type AcpLinkType = (typeof ACP_LINK_TYPES)[number];
1376
1516
  */
1377
1517
  declare function resolvePolicyLinks(ctx: CheckContext): Map<AcpLinkType, string>;
1378
1518
 
1379
- export { ACP_LINK_TYPES, AI_CRAWLER_UAS, AUDIT_ID_PATTERN, type AcpLinkType, type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, type AuditTier, AuditTierSchema, type AuditTrace, type AuditTraceHandler, BASELINE_UA, type BlockClass, CATEGORY_IDS, CATEGORY_MASS, CATEGORY_NAMES, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, type CssRule, DEFAULT_SCAN_LIMIT, type DeprecationNotice, DeprecationNoticeSchema, type EvidenceGrade, EvidenceGradeSchema, type ExpectedKind, type FetchClass, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageCss, type PageJudgement, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, type RobotsFile, type RobotsGroup, type RobotsRule, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanOptions, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, type SitemapEntry, type SitemapTree, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type UaProbe, type WafProtection, allJsonLdNodes, buildCategoryResult, calculateCategoryScore, calculateOverallScore, classifyFetch, classifyResponse, collectPageCss, collectSitemapEntries, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, evidenceUrl, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, filterConfig, flattenJsonLd, formatTrace, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, groupsForBot, hasNamedGroup, isBlanketBlocked, isInformative, isPathAllowed, isPrivateIp, isRealFile, isSafeUrl, isW3CDateTime, joinUrl, judgePages, loadConfigFile, logger, matchesUserAgent, normalizeNewlines, normalizeUrl, outcomeOf, pagesOfType, parseCssRules, parseHtml, parseRobots, parseRobotsFile, planAudits, probeUaParity, resolvePolicyLinks, runAudits, runScan, sampleEntries, stripBom, topLevelJsonLd, traceFromCheck, weightForGrade };
1519
+ export { ACP_LINK_TYPES, AI_CRAWLER_UAS, AUDIT_ID_PATTERN, type AcpLinkType, type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, type AuditTier, AuditTierSchema, type AuditTrace, type AuditTraceHandler, BASELINE_UA, type BlockClass, CATEGORY_IDS, CATEGORY_MASS, CATEGORY_NAMES, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, type CssRule, DEFAULT_SCAN_LIMIT, type DeprecationNotice, DeprecationNoticeSchema, type EvidenceGrade, EvidenceGradeSchema, type EvidenceKey, EvidenceKeySchema, type ExpectedKind, type FetchClass, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageCss, type PageJudgement, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, type RobotsFile, type RobotsGroup, type RobotsRule, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanEvidence, type ScanOptions, type ScanReport, type ScanValidity, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, type SitemapEntry, type SitemapTree, TAG_SCAN_ERROR, TAG_SKIPPED_NO_EVIDENCE, TAG_SKIPPED_PAGE_TYPE, type UaProbe, type WafProtection, allEvidenceMet, allJsonLdNodes, buildCategoryResult, buildScanEvidence, calculateCategoryScore, calculateOverallScore, classifyFetch, classifyResponse, collectPageCss, collectSitemapEntries, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, evidenceUrl, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, filterConfig, flattenJsonLd, formatTrace, getMainContentText, getPreset, getRenderedText, getScoreTier, getTierColor, getTierLabel, getWordCount, groupsForBot, hasNamedGroup, isBlanketBlocked, isInformative, isPathAllowed, isPrivateIp, isRealFile, isSafeUrl, isW3CDateTime, joinUrl, judgePages, loadConfigFile, logger, matchesUserAgent, normalizeNewlines, normalizeUrl, outcomeOf, pagesOfType, parseCssRules, parseHtml, parseRobots, parseRobotsFile, planAudits, probeUaParity, resolvePolicyLinks, runAudits, runScan, sampleEntries, stripBom, topLevelJsonLd, traceFromCheck, weightForGrade };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,19 @@ interface FetchResult {
41
41
  contentLength: number;
42
42
  /** Raw response bytes. Present only when the request asked for `binary`. */
43
43
  bytes?: Uint8Array;
44
+ /**
45
+ * Every redirect hop the fetcher walked, in order. Absent when the caller
46
+ * disabled redirects or the response was not a redirect.
47
+ *
48
+ * `finalUrl` alone cannot say whether a host change was permanent. A scan
49
+ * has to tell a domain migration (301/308) from a temporary hop to somebody
50
+ * else's domain, so the status of each hop is kept.
51
+ */
52
+ redirectChain?: Array<{
53
+ status: number;
54
+ from: string;
55
+ to: string;
56
+ }>;
44
57
  error?: string;
45
58
  }
46
59
  /**
@@ -154,7 +167,25 @@ interface AuditMeta {
154
167
  tier?: AuditTier;
155
168
  /** Repo-relative path to the audit's evidence dossier. */
156
169
  dossier?: string;
170
+ /**
171
+ * Which classes of scan evidence this audit needs to say anything true.
172
+ *
173
+ * Declared per audit rather than inferred at runtime, and checked against
174
+ * what the source actually reads by `scripts/check-requires.mjs`. An audit
175
+ * that reads the sampled pages — directly or through a page-fed gatherer —
176
+ * needs all four keys; one that reads only root files needs the origin to
177
+ * have answered. The deliberate disagreements are the audits whose subject
178
+ * *is* the missing evidence, and they are listed in that script.
179
+ */
180
+ requires?: EvidenceKey[];
157
181
  }
182
+ /**
183
+ * A class of evidence a scan either obtained or did not.
184
+ *
185
+ * Declared here rather than in `scan-evidence.ts` so `AuditMeta` can name it
186
+ * without the audit layer importing the scan layer.
187
+ */
188
+ type EvidenceKey = 'origin-reachable' | 'unblocked-fetches' | 'rendered-body' | 'sample-adequate';
158
189
  interface AuditResult {
159
190
  status: CheckStatus;
160
191
  score: number;
@@ -231,12 +262,31 @@ interface CategoryResult {
231
262
  failCount: number;
232
263
  }
233
264
  type ScoreTier = 'agent-ready' | 'partially-ready' | 'needs-work' | 'not-ready';
265
+ /**
266
+ * Whether a verdict about this site can mean anything, and what is missing.
267
+ *
268
+ * A scan that fetched nothing still produced a number before this existed:
269
+ * `ridge.com` scored 43 and `westontable.com` 37 on scans that read no page,
270
+ * against a real store's 51. The numbers overlap, so a reader could not
271
+ * separate "mediocre site" from "we never saw this site". When `judgeable` is
272
+ * false the report carries no score at all — not a zero, not a low number.
273
+ */
274
+ interface ScanValidity {
275
+ judgeable: boolean;
276
+ evidence: Record<EvidenceKey, boolean>;
277
+ reasons: Partial<Record<EvidenceKey, string>>;
278
+ /** Why the score was suppressed, when it was. */
279
+ unscoredReason?: string;
280
+ }
234
281
  interface ScanReport {
235
282
  scanId: string;
236
283
  url: string;
237
284
  domain: string;
238
- overallScore: number;
239
- scoreTier: ScoreTier;
285
+ /** Null when the scan obtained too little evidence to judge the site. */
286
+ overallScore: number | null;
287
+ /** Null exactly when `overallScore` is. */
288
+ scoreTier: ScoreTier | null;
289
+ scanValidity?: ScanValidity;
240
290
  summary?: string;
241
291
  categories: CategoryResult[];
242
292
  topPasses: CheckResult[];
@@ -269,6 +319,39 @@ interface ReadinessVitals {
269
319
  technical: number;
270
320
  }
271
321
 
322
+ /**
323
+ * What a scan actually obtained, decided once, before any audit runs.
324
+ *
325
+ * An audit that runs on evidence the scan never got reports a verdict about
326
+ * the scanner, not about the site: "no structured data" when the fetch was
327
+ * refused, "0 words" when the origin never answered. This module names the
328
+ * classes of evidence a scan can be missing so those audits can be skipped
329
+ * with the reason attached instead of answering blind.
330
+ *
331
+ * Pure: no network, no IO. It reads what the orchestrator already has.
332
+ */
333
+
334
+ interface ScanEvidence {
335
+ met: Record<EvidenceKey, boolean>;
336
+ /** One sentence per unmet key, shown in the `na` stub and the trace. */
337
+ reasons: Partial<Record<EvidenceKey, string>>;
338
+ /** Page URL to "the served HTML carried text a non-JS consumer can read". */
339
+ renderedByPage: Record<string, boolean>;
340
+ usablePageTypes: Set<PageType>;
341
+ /** False when no verdict about this site can mean anything. */
342
+ judgeable: boolean;
343
+ }
344
+ interface ScanEvidenceInput {
345
+ requestedUrl: string;
346
+ homepageResult: FetchResult;
347
+ pages: PageContext[];
348
+ rootFiles: Record<string, FetchResult>;
349
+ wafProtection: WafProtection | null;
350
+ }
351
+ declare function buildScanEvidence(input: ScanEvidenceInput): ScanEvidence;
352
+ /** All requirements met. For test harnesses not exercising the gate. */
353
+ declare function allEvidenceMet(): ScanEvidence;
354
+
272
355
  interface A11yNodeFinding {
273
356
  target: string;
274
357
  summary: string;
@@ -316,6 +399,14 @@ interface CheckContext {
316
399
  baseUrl: string;
317
400
  fetch: (options: FetchOptions) => Promise<FetchResult>;
318
401
  wafProtection?: WafProtection;
402
+ /**
403
+ * What the scan actually obtained, decided once before any audit ran.
404
+ *
405
+ * Required, not optional. An optional field fails open, and a caller that
406
+ * forgets is exactly the silent-nothing verdict this exists to remove. Test
407
+ * harnesses that do not exercise the gate pass `allEvidenceMet()`.
408
+ */
409
+ evidence: ScanEvidence;
319
410
  }
320
411
  type CheckFn = (ctx: CheckContext) => CheckResult | Promise<CheckResult>;
321
412
 
@@ -441,9 +532,10 @@ interface AuditTrace {
441
532
  /**
442
533
  * `ran` — the audit executed and returned.
443
534
  * `skipped` — no scanned page matched its `applicablePageTypes`.
535
+ * `gated` — the scan never obtained evidence the audit needs.
444
536
  * `error` — it threw, or its result was rejected by the schema.
445
537
  */
446
- outcome: 'ran' | 'skipped' | 'error';
538
+ outcome: 'ran' | 'skipped' | 'gated' | 'error';
447
539
  status: CheckStatus;
448
540
  score: number;
449
541
  weight: number;
@@ -457,7 +549,7 @@ interface AuditTrace {
457
549
  /** The structured evidence behind the verdict, as the report carries it. */
458
550
  details?: CheckResult['details'];
459
551
  }
460
- /** Which of the three outcomes a finished check represents. */
552
+ /** Which outcome a finished check represents. */
461
553
  declare function outcomeOf(check: CheckResult): AuditTrace['outcome'];
462
554
  /** Build a trace record from a finished check. */
463
555
  declare function traceFromCheck(check: CheckResult, durationMs: number): AuditTrace;
@@ -496,12 +588,23 @@ interface AuditPlan {
496
588
  }>;
497
589
  skipped: CheckResult[];
498
590
  }
591
+ /** How `planAudits` should treat audits the scan cannot feed. */
592
+ interface PlanOptions {
593
+ /**
594
+ * Skip audits whose `requires` the scan did not obtain.
595
+ *
596
+ * Off by default while the gate is calibrated: turning it on changes what
597
+ * every blocked or client-rendered scan reports, and that lands as one
598
+ * deliberate change rather than as a side effect of this one.
599
+ */
600
+ enforceEvidence?: boolean;
601
+ }
499
602
  /**
500
603
  * Split a scan config into the audits that will actually execute and the
501
- * page-type-skipped `na` stubs. Exported so the orchestrator can size the
604
+ * `na` stubs for those it will not. Exported so the orchestrator can size the
502
605
  * audits progress phase before running it.
503
606
  */
504
- declare function planAudits(ctx: CheckContext, config: ScanConfig): AuditPlan;
607
+ declare function planAudits(ctx: CheckContext, config: ScanConfig, options?: PlanOptions): AuditPlan;
505
608
  /**
506
609
  * Execute all audits registered in the config against the scan context.
507
610
  * Returns check results grouped into categories with weighted scores.
@@ -545,7 +648,8 @@ type ScanEvent = {
545
648
  } | {
546
649
  type: 'scan:done';
547
650
  durationMs: number;
548
- score: number;
651
+ /** Null when the scan obtained too little evidence to judge the site. */
652
+ score: number | null;
549
653
  fraction: number;
550
654
  elapsedMs: number;
551
655
  };
@@ -583,7 +687,7 @@ declare class ProgressTracker {
583
687
  /** A failed unit still counts as settled work so the phase can complete. */
584
688
  unitFail(label: string, error: string): void;
585
689
  phaseDone(): void;
586
- scanDone(score: number): void;
690
+ scanDone(score: number | null): void;
587
691
  }
588
692
 
589
693
  interface ScanOptions {
@@ -607,6 +711,15 @@ interface ScanOptions {
607
711
  * evidence it was drawn from; see {@link AuditTrace}.
608
712
  */
609
713
  onAuditTrace?: AuditTraceHandler;
714
+ /**
715
+ * Skip audits the scan could not feed, instead of running them blind.
716
+ *
717
+ * On by default. An audit whose required evidence the scan never obtained
718
+ * reports `na` tagged `skipped:no-evidence`, with the reason attached, and
719
+ * is never constructed. Set it to `false` to run every audit regardless —
720
+ * useful when measuring what the gate removes.
721
+ */
722
+ enforceEvidenceGate?: boolean;
610
723
  }
611
724
  declare function runScan(url: string, options?: ScanOptions): Promise<ScanReport>;
612
725
 
@@ -910,7 +1023,28 @@ declare function extractHeadings($: CheerioAPI): Array<{
910
1023
  level: number;
911
1024
  text: string;
912
1025
  }>;
1026
+ /**
1027
+ * The text of the page's main content region.
1028
+ *
1029
+ * Answers what the main content says, so page chrome — navigation, headers,
1030
+ * footers, cookie banners — stays out of it. Among several <main> elements the
1031
+ * one holding the most text wins: themes ship empty or fragmented <main>
1032
+ * wrappers, and the first one is often a short stub. Falls back to the whole
1033
+ * <body> only when no <main> holds any text.
1034
+ *
1035
+ * For shell detection — did the server render any readable text at all — use
1036
+ * `getRenderedText` instead.
1037
+ */
913
1038
  declare function getMainContentText($: CheerioAPI): string;
1039
+ /**
1040
+ * The text of the served HTML body, minus script/style/noscript/template.
1041
+ *
1042
+ * Answers whether the server rendered any readable text at all, so page chrome
1043
+ * counts: a shell that ships only a nav and a footer is exactly what this has
1044
+ * to see. Use `getMainContentText` when the question is what the main content
1045
+ * says rather than whether anything was served.
1046
+ */
1047
+ declare function getRenderedText($: CheerioAPI): string;
914
1048
  declare function getWordCount($: CheerioAPI): number;
915
1049
  declare function extractImages($: CheerioAPI): Array<{
916
1050
  src: string;
@@ -963,6 +1097,8 @@ declare const MAX_CONCURRENT_REQUESTS = 10;
963
1097
  declare const SCANNER_USER_AGENT = "AgentLighthouse/1.0 (+https://github.com/ForkPoint/agent-lighthouse)";
964
1098
  declare const TAG_SKIPPED_PAGE_TYPE = "skipped:page-type";
965
1099
  declare const TAG_SCAN_ERROR = "scan-error";
1100
+ /** An audit the scan could not feed: it needs evidence this scan never got. */
1101
+ declare const TAG_SKIPPED_NO_EVIDENCE = "skipped:no-evidence";
966
1102
  /** Display names for the 8 v2 categories. */
967
1103
  declare const CATEGORY_NAMES: Record<string, string>;
968
1104
  declare const READINESS_WEIGHTS: {
@@ -1113,6 +1249,7 @@ declare const EvidenceGradeSchema: z.ZodEnum<["A", "B", "C", "D"]>;
1113
1249
  declare const AuditTierSchema: z.ZodEnum<["scored", "informative", "experimental"]>;
1114
1250
  /** v2 audit identity: `category/slug`, stable across releases (spec §6). */
1115
1251
  declare const AUDIT_ID_PATTERN: RegExp;
1252
+ declare const EvidenceKeySchema: z.ZodEnum<["origin-reachable", "unblocked-fetches", "rendered-body", "sample-adequate"]>;
1116
1253
  declare const AuditMetaSchema: z.ZodObject<{
1117
1254
  id: z.ZodString;
1118
1255
  category: z.ZodString;
@@ -1158,6 +1295,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1158
1295
  evidenceGrade: z.ZodEnum<["A", "B", "C", "D"]>;
1159
1296
  tier: z.ZodEnum<["scored", "informative", "experimental"]>;
1160
1297
  dossier: z.ZodString;
1298
+ requires: z.ZodOptional<z.ZodArray<z.ZodEnum<["origin-reachable", "unblocked-fetches", "rendered-body", "sample-adequate"]>, "many">>;
1161
1299
  }, "strip", z.ZodTypeAny, {
1162
1300
  category: string;
1163
1301
  title: string;
@@ -1183,6 +1321,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1183
1321
  link: string;
1184
1322
  notice: string;
1185
1323
  } | undefined;
1324
+ requires?: ("origin-reachable" | "unblocked-fetches" | "rendered-body" | "sample-adequate")[] | undefined;
1186
1325
  }, {
1187
1326
  category: string;
1188
1327
  title: string;
@@ -1208,6 +1347,7 @@ declare const AuditMetaSchema: z.ZodObject<{
1208
1347
  link: string;
1209
1348
  notice: string;
1210
1349
  } | undefined;
1350
+ requires?: ("origin-reachable" | "unblocked-fetches" | "rendered-body" | "sample-adequate")[] | undefined;
1211
1351
  }>;
1212
1352
  declare const CheckResultSchema: z.ZodObject<{
1213
1353
  id: z.ZodString;
@@ -1376,4 +1516,4 @@ type AcpLinkType = (typeof ACP_LINK_TYPES)[number];
1376
1516
  */
1377
1517
  declare function resolvePolicyLinks(ctx: CheckContext): Map<AcpLinkType, string>;
1378
1518
 
1379
- export { ACP_LINK_TYPES, AI_CRAWLER_UAS, AUDIT_ID_PATTERN, type AcpLinkType, type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, type AuditTier, AuditTierSchema, type AuditTrace, type AuditTraceHandler, BASELINE_UA, type BlockClass, CATEGORY_IDS, CATEGORY_MASS, CATEGORY_NAMES, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, type CssRule, DEFAULT_SCAN_LIMIT, type DeprecationNotice, DeprecationNoticeSchema, type EvidenceGrade, EvidenceGradeSchema, type ExpectedKind, type FetchClass, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageCss, type PageJudgement, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, type RobotsFile, type RobotsGroup, type RobotsRule, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanOptions, type ScanReport, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, type SitemapEntry, type SitemapTree, TAG_SCAN_ERROR, TAG_SKIPPED_PAGE_TYPE, type UaProbe, type WafProtection, allJsonLdNodes, buildCategoryResult, calculateCategoryScore, calculateOverallScore, classifyFetch, classifyResponse, collectPageCss, collectSitemapEntries, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, evidenceUrl, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, filterConfig, flattenJsonLd, formatTrace, getMainContentText, getPreset, getScoreTier, getTierColor, getTierLabel, getWordCount, groupsForBot, hasNamedGroup, isBlanketBlocked, isInformative, isPathAllowed, isPrivateIp, isRealFile, isSafeUrl, isW3CDateTime, joinUrl, judgePages, loadConfigFile, logger, matchesUserAgent, normalizeNewlines, normalizeUrl, outcomeOf, pagesOfType, parseCssRules, parseHtml, parseRobots, parseRobotsFile, planAudits, probeUaParity, resolvePolicyLinks, runAudits, runScan, sampleEntries, stripBom, topLevelJsonLd, traceFromCheck, weightForGrade };
1519
+ export { ACP_LINK_TYPES, AI_CRAWLER_UAS, AUDIT_ID_PATTERN, type AcpLinkType, type AgentLighthouseConfig, Audit, type AuditGuidance, AuditGuidanceSchema, type AuditMeta, AuditMetaSchema, type AuditPlan, type AuditProgressEvent, type AuditRegistration, type AuditResult, AuditResultSchema, type AuditRunResult, type AuditTier, AuditTierSchema, type AuditTrace, type AuditTraceHandler, BASELINE_UA, type BlockClass, CATEGORY_IDS, CATEGORY_MASS, CATEGORY_NAMES, type CategoryConfig, type CategoryResult, type CheckContext, type CheckFn, type CheckPriority, CheckPrioritySchema, type CheckRecommendation, type CheckResult, CheckResultSchema, type CheckStatus, CheckStatusSchema, type CssRule, DEFAULT_SCAN_LIMIT, type DeprecationNotice, DeprecationNoticeSchema, type EvidenceGrade, EvidenceGradeSchema, type EvidenceKey, EvidenceKeySchema, type ExpectedKind, type FetchClass, type FetchOptions, type FetchResult, type FieldStatus, type FixEffort, FixEffortSchema, MAX_CONCURRENT_REQUESTS, MAX_PAGES_PER_SCAN, MAX_RESPONSE_BODY_BYTES, PAGE_TYPE_LABELS, PHASE_WEIGHTS, PRESETS, type PageContext, type PageCss, type PageJudgement, type PageOverride, type PageType, type PhaseId, type PresetName, type PresetOptions, type ProductFieldVerification, ProgressTracker, READINESS_WEIGHTS, REQUEST_TIMEOUT_MS, type ReadinessVitals, type RobotsFile, type RobotsGroup, type RobotsRule, SCANNER_USER_AGENT, SCAN_TIMEOUT_MS, SCORE_TIER_LABELS, type ScanConfig, type ScanEvent, type ScanEvidence, type ScanOptions, type ScanReport, type ScanValidity, type ScoreDisplayMode, ScoreDisplayModeSchema, type ScoreTier, type SitemapEntry, type SitemapTree, TAG_SCAN_ERROR, TAG_SKIPPED_NO_EVIDENCE, TAG_SKIPPED_PAGE_TYPE, type UaProbe, type WafProtection, allEvidenceMet, allJsonLdNodes, buildCategoryResult, buildScanEvidence, calculateCategoryScore, calculateOverallScore, classifyFetch, classifyResponse, collectPageCss, collectSitemapEntries, createFetcher, defaultConfig, defineConfig, detectPageType, detectWafProtection, evidenceUrl, extractDomain, extractForms, extractHeadLinks, extractHeadings, extractImages, extractInternalLinks, extractJsonLd, extractMarkdownLinks, extractMetaTags, extractNavLinks, extractProductFieldVerification, extractScripts, extractStylesheetUrls, filterConfig, flattenJsonLd, formatTrace, getMainContentText, getPreset, getRenderedText, getScoreTier, getTierColor, getTierLabel, getWordCount, groupsForBot, hasNamedGroup, isBlanketBlocked, isInformative, isPathAllowed, isPrivateIp, isRealFile, isSafeUrl, isW3CDateTime, joinUrl, judgePages, loadConfigFile, logger, matchesUserAgent, normalizeNewlines, normalizeUrl, outcomeOf, pagesOfType, parseCssRules, parseHtml, parseRobots, parseRobotsFile, planAudits, probeUaParity, resolvePolicyLinks, runAudits, runScan, sampleEntries, stripBom, topLevelJsonLd, traceFromCheck, weightForGrade };