@nebulacomponents/citable 1.18.2 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +12 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +19 -13
- package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +19 -13
- package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +19 -13
- package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +19 -13
- package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +11 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +19 -13
- package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +19 -13
- package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +19 -13
- package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +19 -13
- package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +19 -13
- package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +19 -13
- package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +19 -13
- package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +19 -13
- package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/manifest.json +232 -160
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/browser-evidence-plan.schema.json +186 -3
- package/schemas/browser-network-events.schema.json +142 -0
- package/schemas/browser-runtime-events.schema.json +112 -0
- package/schemas/browser-state-observations.schema.json +97 -0
- package/schemas/collection-result.schema.json +35 -0
- package/schemas/config.schema.json +2 -0
- package/schemas/cro-report.schema.json +8 -1
- package/schemas/finding.schema.json +105 -2
- package/schemas/remediation-verification.schema.json +12 -1
- package/schemas/run.schema.json +15 -2
- package/schemas/search-report.schema.json +8 -1
- package/schemas/summary.schema.json +41 -0
- package/skill/SKILL.md +11 -2
- package/src/acceptance/reproducibility.js +5 -25
- package/src/analysis/croRoadmap.js +3 -3
- package/src/analysis/strategicRoadmap.js +3 -3
- package/src/artifacts/interchange.js +2 -6
- package/src/cli/index.js +15 -3
- package/src/commands/actionPlan.js +46 -5
- package/src/commands/applyRemediation.js +5 -5
- package/src/commands/audit.js +75 -8
- package/src/commands/ciWorkflow.js +7 -2
- package/src/commands/compareSnapshots.js +155 -47
- package/src/commands/connect.js +24 -4
- package/src/commands/context.js +41 -2
- package/src/commands/croSuite.js +51 -13
- package/src/commands/delivery.js +7 -7
- package/src/commands/governance.js +9 -10
- package/src/commands/implementationKit.js +2 -1
- package/src/commands/init.js +8 -1
- package/src/commands/previewCro.js +1 -1
- package/src/commands/prioritize.js +3 -2
- package/src/commands/reviews.js +31 -5
- package/src/commands/roadmapCmd.js +7 -13
- package/src/commands/sweep.js +15 -2
- package/src/commands/verifyRemediation.js +94 -19
- package/src/connectors/collectionResult.js +80 -0
- package/src/connectors/ga4.js +46 -11
- package/src/connectors/gsc.js +41 -6
- package/src/connectors/index.js +1 -1
- package/src/connectors/webflow.js +140 -10
- package/src/connectors/wordpress.js +92 -8
- package/src/crawler/fetch.js +220 -24
- package/src/crawler/resourceValidity.js +119 -0
- package/src/crawler/sitemap.js +435 -16
- package/src/crawler/sitemapCollector.js +364 -0
- package/src/crawler/urlIdentity.js +97 -0
- package/src/detectors/agent.js +11 -0
- package/src/detectors/ans.js +16 -1
- package/src/detectors/arch.js +6 -0
- package/src/detectors/claim.js +9 -0
- package/src/detectors/crawl.js +7 -0
- package/src/detectors/cro.js +21 -0
- package/src/detectors/cwv.js +4 -0
- package/src/detectors/entity.js +7 -0
- package/src/detectors/evd.js +9 -0
- package/src/detectors/framework.js +28 -2
- package/src/detectors/geoReco.js +16 -0
- package/src/detectors/hreflang.js +3 -0
- package/src/detectors/lifeMeas.js +9 -0
- package/src/detectors/link.js +6 -1
- package/src/detectors/page.js +11 -0
- package/src/detectors/schemaData.js +16 -1
- package/src/detectors/tech.js +34 -3
- package/src/evidence/coverage.js +448 -0
- package/src/evidence/determination.js +157 -0
- package/src/evidence/downstream.js +110 -0
- package/src/evidence/hashes.js +109 -0
- package/src/evidence/run.js +68 -8
- package/src/extractor/page.js +29 -2
- package/src/extractor/site.js +298 -50
- package/src/observations/browser/correlation.js +83 -0
- package/src/observations/browser/networkCollector.js +413 -0
- package/src/observations/browser/runtimeEventCollector.js +337 -0
- package/src/observations/browser/sanitizer.js +322 -0
- package/src/observations/browser/stateCollector.js +386 -0
- package/src/observations/browserJourney.js +131 -3
- package/src/reporting/executiveCroReport.js +37 -14
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +57 -23
- package/src/reporting/report.js +56 -9
- package/src/shared/epistemicLanguage.js +86 -0
- package/src/shared/evidenceSourceResolver.js +48 -17
- package/src/shared/htmlEscape.js +8 -4
- package/src/shared/runPackageVerifier.js +99 -8
- package/src/shared/verifiedRunLoader.js +129 -0
- package/src/sow/generateSow.js +1 -1
package/src/detectors/tech.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isAllowed } from '../crawler/robots.js';
|
|
|
5
5
|
const D = [];
|
|
6
6
|
|
|
7
7
|
D.push(defineDetector({
|
|
8
|
+
coverage_requirement: 'evaluated_subset',
|
|
8
9
|
id: 'TECH-001', name: 'Non-200 index-target URL', namespace: 'TECH',
|
|
9
10
|
description: 'A page intended for indexing does not return HTTP 200.',
|
|
10
11
|
discipline: ['seo', 'aeo', 'geo'], severity: 'critical', deterministic: true, requires: ['site'],
|
|
@@ -23,6 +24,7 @@ D.push(defineDetector({
|
|
|
23
24
|
}));
|
|
24
25
|
|
|
25
26
|
D.push(defineDetector({
|
|
27
|
+
coverage_requirement: 'evaluated_subset',
|
|
26
28
|
id: 'TECH-002', name: 'Accidental noindex on index-target page', namespace: 'TECH',
|
|
27
29
|
description: 'A page whose registry intent is "index" carries a noindex directive (meta robots or X-Robots-Tag).',
|
|
28
30
|
discipline: ['seo', 'aeo', 'geo'], severity: 'critical', deterministic: true, requires: ['site'],
|
|
@@ -41,6 +43,7 @@ D.push(defineDetector({
|
|
|
41
43
|
}));
|
|
42
44
|
|
|
43
45
|
D.push(defineDetector({
|
|
46
|
+
coverage_requirement: 'evaluated_subset',
|
|
44
47
|
id: 'TECH-003', name: 'robots.txt blocks index-target page', namespace: 'TECH',
|
|
45
48
|
description: 'robots.txt disallows a search crawler from fetching a page that is intended to be indexed and cited.',
|
|
46
49
|
discipline: ['seo', 'aeo', 'geo'], severity: 'critical', deterministic: true, requires: ['site'],
|
|
@@ -67,6 +70,7 @@ D.push(defineDetector({
|
|
|
67
70
|
}));
|
|
68
71
|
|
|
69
72
|
D.push(defineDetector({
|
|
73
|
+
coverage_requirement: 'evaluated_subset',
|
|
70
74
|
id: 'TECH-004', name: 'Multiple conflicting canonical declarations', namespace: 'TECH',
|
|
71
75
|
description: 'A document declares more than one rel=canonical with differing targets.',
|
|
72
76
|
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -85,6 +89,7 @@ D.push(defineDetector({
|
|
|
85
89
|
}));
|
|
86
90
|
|
|
87
91
|
D.push(defineDetector({
|
|
92
|
+
coverage_requirement: 'evaluated_subset',
|
|
88
93
|
id: 'TECH-005', name: 'Canonical points to redirect', namespace: 'TECH',
|
|
89
94
|
description: 'rel=canonical targets a URL that itself redirects.',
|
|
90
95
|
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -98,6 +103,7 @@ D.push(defineDetector({
|
|
|
98
103
|
}));
|
|
99
104
|
|
|
100
105
|
D.push(defineDetector({
|
|
106
|
+
coverage_requirement: 'evaluated_subset',
|
|
101
107
|
id: 'TECH-006', name: 'Canonical points to error page', namespace: 'TECH',
|
|
102
108
|
description: 'rel=canonical targets a URL returning 4xx/5xx.',
|
|
103
109
|
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -111,6 +117,7 @@ D.push(defineDetector({
|
|
|
111
117
|
}));
|
|
112
118
|
|
|
113
119
|
D.push(defineDetector({
|
|
120
|
+
coverage_requirement: 'evaluated_subset',
|
|
114
121
|
id: 'TECH-007', name: 'Canonical points to noindex page', namespace: 'TECH',
|
|
115
122
|
description: 'rel=canonical consolidates signals into a URL that is itself noindexed.',
|
|
116
123
|
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -124,6 +131,7 @@ D.push(defineDetector({
|
|
|
124
131
|
}));
|
|
125
132
|
|
|
126
133
|
D.push(defineDetector({
|
|
134
|
+
coverage_requirement: 'evaluated_subset',
|
|
127
135
|
id: 'TECH-008', name: 'Sitemap contains redirecting URL', namespace: 'TECH',
|
|
128
136
|
description: 'An XML sitemap lists a URL that does not resolve directly with HTTP 200 (redirects).',
|
|
129
137
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -137,6 +145,7 @@ D.push(defineDetector({
|
|
|
137
145
|
}));
|
|
138
146
|
|
|
139
147
|
D.push(defineDetector({
|
|
148
|
+
coverage_requirement: 'evaluated_subset',
|
|
140
149
|
id: 'TECH-009', name: 'Sitemap contains non-canonical URL', namespace: 'TECH',
|
|
141
150
|
description: 'A sitemap lists a URL whose page declares a different canonical.',
|
|
142
151
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -166,6 +175,7 @@ D.push(defineDetector({
|
|
|
166
175
|
}));
|
|
167
176
|
|
|
168
177
|
D.push(defineDetector({
|
|
178
|
+
coverage_requirement: 'evaluated_subset',
|
|
169
179
|
id: 'TECH-010', name: 'Sitemap URL unresolvable or errors', namespace: 'TECH',
|
|
170
180
|
description: 'A sitemap lists a URL that is missing from the built output or returns an error.',
|
|
171
181
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -178,13 +188,19 @@ D.push(defineDetector({
|
|
|
178
188
|
for (const sm of ctx.site.sitemaps) {
|
|
179
189
|
for (const u of sm.parsed.urls) {
|
|
180
190
|
const page = ctx.site.byUrl.get(ctx.site.normalize(u.loc));
|
|
181
|
-
|
|
191
|
+
const crawlState = ctx.site.mode === 'url' ? ctx.site.coverageLedger?.stateFor(u.loc) : null;
|
|
192
|
+
if (!page && ctx.site.mode === 'url' && crawlState !== 'failed') {
|
|
193
|
+
// An unvisited or merely queued sitemap URL is not evidence of an
|
|
194
|
+
// invalid resource. Coverage records it for an aggregate
|
|
195
|
+
// determination instead of manufacturing a TECH-010 absence claim.
|
|
182
196
|
continue;
|
|
183
197
|
} else if (!page) {
|
|
184
198
|
hits.push({
|
|
185
199
|
subject: { type: 'url', identifier: u.loc, url: u.loc, source_location: sm.source },
|
|
186
|
-
summary: 'Sitemap URL not present in audited output',
|
|
187
|
-
evidence: [
|
|
200
|
+
summary: crawlState === 'failed' ? 'Sitemap URL fetch failed' : 'Sitemap URL not present in audited output',
|
|
201
|
+
evidence: [crawlState === 'failed'
|
|
202
|
+
? `sitemap ${sm.source} lists ${u.loc}; retrieval failed after attempted observation`
|
|
203
|
+
: `sitemap ${sm.source} lists ${u.loc}; no matching page found`],
|
|
188
204
|
});
|
|
189
205
|
} else if (page.status >= 400) {
|
|
190
206
|
hits.push({
|
|
@@ -200,6 +216,7 @@ D.push(defineDetector({
|
|
|
200
216
|
}));
|
|
201
217
|
|
|
202
218
|
D.push(defineDetector({
|
|
219
|
+
coverage_requirement: 'evaluated_subset',
|
|
203
220
|
id: 'TECH-011', name: 'Render-dependent primary content', namespace: 'TECH',
|
|
204
221
|
description: 'Initial HTML contains almost no primary text relative to script payload; principal content likely requires client-side rendering.',
|
|
205
222
|
discipline: ['seo', 'aeo', 'geo'], severity: 'high', deterministic: false, requires: ['site'],
|
|
@@ -221,6 +238,7 @@ D.push(defineDetector({
|
|
|
221
238
|
}));
|
|
222
239
|
|
|
223
240
|
D.push(defineDetector({
|
|
241
|
+
coverage_requirement: 'evaluated_subset',
|
|
224
242
|
id: 'TECH-012', name: 'Invalid MIME type for HTML page', namespace: 'TECH',
|
|
225
243
|
description: 'An index-target page is served with a non-HTML content type.',
|
|
226
244
|
discipline: ['seo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -241,6 +259,7 @@ D.push(defineDetector({
|
|
|
241
259
|
}));
|
|
242
260
|
|
|
243
261
|
D.push(defineDetector({
|
|
262
|
+
coverage_requirement: 'evaluated_subset',
|
|
244
263
|
id: 'TECH-013', name: 'Redirect chain', namespace: 'TECH',
|
|
245
264
|
description: 'Reaching the final URL required more than one redirect hop.',
|
|
246
265
|
discipline: ['seo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -258,6 +277,7 @@ D.push(defineDetector({
|
|
|
258
277
|
}));
|
|
259
278
|
|
|
260
279
|
D.push(defineDetector({
|
|
280
|
+
coverage_requirement: 'evaluated_subset',
|
|
261
281
|
id: 'TECH-014', name: 'Soft-404 behaviour', namespace: 'TECH',
|
|
262
282
|
description: 'A page returns 200 but its content indicates the resource does not exist.',
|
|
263
283
|
discipline: ['seo'], severity: 'medium', deterministic: false, requires: ['site'],
|
|
@@ -280,6 +300,7 @@ D.push(defineDetector({
|
|
|
280
300
|
}));
|
|
281
301
|
|
|
282
302
|
D.push(defineDetector({
|
|
303
|
+
coverage_requirement: 'evaluated_subset',
|
|
283
304
|
id: 'TECH-015', name: 'Staging or preview host exposed as indexable', namespace: 'TECH',
|
|
284
305
|
description: 'The audited base URL looks like a staging/preview environment yet pages are indexable.',
|
|
285
306
|
discipline: ['seo'], severity: 'critical', deterministic: false, requires: ['site'],
|
|
@@ -305,6 +326,7 @@ D.push(defineDetector({
|
|
|
305
326
|
}));
|
|
306
327
|
|
|
307
328
|
D.push(defineDetector({
|
|
329
|
+
coverage_requirement: 'exhaustive_requested_scope',
|
|
308
330
|
id: 'TECH-016', name: 'Missing sitemap', namespace: 'TECH',
|
|
309
331
|
description: 'No XML sitemap was found for the audited output.',
|
|
310
332
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -323,6 +345,7 @@ D.push(defineDetector({
|
|
|
323
345
|
}));
|
|
324
346
|
|
|
325
347
|
D.push(defineDetector({
|
|
348
|
+
coverage_requirement: 'evaluated_subset',
|
|
326
349
|
id: 'TECH-017', name: 'Snippet suppression on citation-target page', namespace: 'TECH',
|
|
327
350
|
description: 'A page targeted for answer citation carries nosnippet or max-snippet:0, which removes snippet eligibility that generative search features depend on.',
|
|
328
351
|
discipline: ['aeo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -340,6 +363,7 @@ D.push(defineDetector({
|
|
|
340
363
|
}));
|
|
341
364
|
|
|
342
365
|
D.push(defineDetector({
|
|
366
|
+
coverage_requirement: 'evaluated_subset',
|
|
343
367
|
id: 'TECH-018', name: 'Missing viewport (mobile readiness)', namespace: 'TECH',
|
|
344
368
|
description: 'Page lacks a viewport meta tag, a minimum condition for mobile parity.',
|
|
345
369
|
discipline: ['seo'], severity: 'low', deterministic: true, requires: ['site'],
|
|
@@ -360,6 +384,7 @@ D.push(defineDetector({
|
|
|
360
384
|
}));
|
|
361
385
|
|
|
362
386
|
D.push(defineDetector({
|
|
387
|
+
coverage_requirement: 'evaluated_subset',
|
|
363
388
|
id: 'TECH-019', name: 'Open Graph URL conflicts with canonical', namespace: 'TECH',
|
|
364
389
|
description: 'Open Graph URL (og:url) disagrees with the declared canonical URL, sending conflicting canonicalization and entity identity signals.',
|
|
365
390
|
discipline: ['seo', 'aeo', 'geo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -395,6 +420,7 @@ D.push(defineDetector({
|
|
|
395
420
|
}));
|
|
396
421
|
|
|
397
422
|
D.push(defineDetector({
|
|
423
|
+
coverage_requirement: 'evaluated_subset',
|
|
398
424
|
id: 'TECH-020', name: 'Missing reciprocal hreflang return link', namespace: 'TECH',
|
|
399
425
|
description: 'An alternate hreflang URL targets an internal page, but that target page does not reciprocate with a matching hreflang link back to the source page.',
|
|
400
426
|
discipline: ['seo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -445,6 +471,7 @@ D.push(defineDetector({
|
|
|
445
471
|
}));
|
|
446
472
|
|
|
447
473
|
D.push(defineDetector({
|
|
474
|
+
coverage_requirement: 'evaluated_subset',
|
|
448
475
|
id: 'TECH-021', name: 'Invalid or non-standard BCP 47 hreflang syntax', namespace: 'TECH',
|
|
449
476
|
description: 'An hreflang annotation uses an invalid language code, malformed region subtag, deprecated territory, or relative URL.',
|
|
450
477
|
discipline: ['seo', 'geo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -493,6 +520,7 @@ D.push(defineDetector({
|
|
|
493
520
|
}));
|
|
494
521
|
|
|
495
522
|
D.push(defineDetector({
|
|
523
|
+
coverage_requirement: 'evaluated_subset',
|
|
496
524
|
id: 'TECH-022', name: 'Hreflang alternate target conflicts with rel=canonical', namespace: 'TECH',
|
|
497
525
|
description: 'An alternate hreflang URL points to a page that declares a different rel=canonical target, breaking the alternate cluster.',
|
|
498
526
|
discipline: ['seo', 'geo'], severity: 'critical', deterministic: true, requires: ['site'],
|
|
@@ -539,6 +567,7 @@ D.push(defineDetector({
|
|
|
539
567
|
}));
|
|
540
568
|
|
|
541
569
|
D.push(defineDetector({
|
|
570
|
+
coverage_requirement: 'evaluated_subset',
|
|
542
571
|
id: 'TECH-023', name: 'Hydration gap: critical metadata or schema missing from initial server HTML response', namespace: 'TECH',
|
|
543
572
|
description: 'A single-page application or client-rendered document returns empty content mounting shells with virtually no static text (<20 words) in initial server HTML, breaking crawler and answer engine extraction without full headless JS rendering.',
|
|
544
573
|
discipline: ['seo', 'aeo'], severity: 'high', deterministic: true, requires: ['site'],
|
|
@@ -574,6 +603,7 @@ D.push(defineDetector({
|
|
|
574
603
|
}));
|
|
575
604
|
|
|
576
605
|
D.push(defineDetector({
|
|
606
|
+
coverage_requirement: 'evaluated_subset',
|
|
577
607
|
id: 'TECH-024', name: 'Excessive render-blocking script payload exceeding crawler execution budget', namespace: 'TECH',
|
|
578
608
|
description: 'The page delivers excessive inline/head script payloads (>1.2MB) or an excessive number of render-blocking head scripts (>12 without defer/async), exhausting crawler execution budgets and delaying indexing.',
|
|
579
609
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -619,6 +649,7 @@ D.push(defineDetector({
|
|
|
619
649
|
}));
|
|
620
650
|
|
|
621
651
|
D.push(defineDetector({
|
|
652
|
+
coverage_requirement: 'evaluated_subset',
|
|
622
653
|
id: 'TECH-025', name: 'Oversized uncompressed image asset or heavy inline image payload', namespace: 'TECH',
|
|
623
654
|
description: 'The page embeds an excessively large inline image data URI (>100KB) or references raw uncompressed image payloads exceeding 1MB, delaying Largest Contentful Paint (LCP) and exhausting mobile data budgets.',
|
|
624
655
|
discipline: ['seo', 'aeo'], severity: 'medium', deterministic: true, requires: ['site'],
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { validateAgainst } from '../shared/schemaValidator.js';
|
|
2
|
+
|
|
3
|
+
export const COVERAGE_SCHEMA_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
const STOP_REASONS = new Set([
|
|
6
|
+
'frontier_exhausted',
|
|
7
|
+
'page_budget_exhausted',
|
|
8
|
+
'time_budget_exhausted',
|
|
9
|
+
'fatal_collection_failure',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const DISCOVERY_STATUSES = new Set(['complete', 'truncated', 'indeterminate']);
|
|
13
|
+
const TERMINAL_STATES = new Set([
|
|
14
|
+
'evaluated',
|
|
15
|
+
'valid_but_unevaluated',
|
|
16
|
+
'failed',
|
|
17
|
+
'indeterminate',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function assertNonEmptyString(value, name) {
|
|
21
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
22
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertIntegerInRange(value, name, minimum, maximum) {
|
|
27
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
28
|
+
throw new RangeError(`${name} must be an integer from ${minimum} through ${maximum}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertUrl(value, name) {
|
|
33
|
+
assertNonEmptyString(value, name);
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(value);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new TypeError(`${name} must be an absolute URL`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
41
|
+
throw new TypeError(`${name} must use http or https`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizedUrl(url) {
|
|
47
|
+
assertUrl(url, 'url');
|
|
48
|
+
const fragmentIndex = url.indexOf('#');
|
|
49
|
+
return fragmentIndex === -1 ? url : url.slice(0, fragmentIndex);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canonicalValue(value, path = 'metadata') {
|
|
53
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
54
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
55
|
+
if (Array.isArray(value)) return value.map((item, index) => canonicalValue(item, `${path}[${index}]`));
|
|
56
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.keys(value)
|
|
59
|
+
.sort()
|
|
60
|
+
.map((key) => [key, canonicalValue(value[key], `${path}.${key}`)]),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
throw new TypeError(`${path} must contain only JSON-compatible values`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalMetadata(metadata, name) {
|
|
67
|
+
if (metadata === undefined) return {};
|
|
68
|
+
if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
69
|
+
throw new TypeError(`${name} must be an object`);
|
|
70
|
+
}
|
|
71
|
+
return canonicalValue(metadata, name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stableString(value) {
|
|
75
|
+
return JSON.stringify(canonicalValue(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compareStrings(a, b) {
|
|
79
|
+
if (a < b) return -1;
|
|
80
|
+
if (a > b) return 1;
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sortedUniqueStrings(values, name) {
|
|
85
|
+
if (!Array.isArray(values)) throw new TypeError(`${name} must be an array`);
|
|
86
|
+
for (const value of values) assertNonEmptyString(value, `${name} entry`);
|
|
87
|
+
return [...new Set(values)].sort(compareStrings);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stateOf(resource) {
|
|
91
|
+
if (resource.exclusion) return 'excluded';
|
|
92
|
+
if (resource.evaluation) return 'evaluated';
|
|
93
|
+
if (resource.validButUnevaluated) return 'valid_but_unevaluated';
|
|
94
|
+
if (resource.failure) return 'failed';
|
|
95
|
+
if (resource.indeterminate) return 'indeterminate';
|
|
96
|
+
if (resource.classification?.state === 'valid_resource') return 'valid_resource';
|
|
97
|
+
if (resource.retrieval) return 'retrieved';
|
|
98
|
+
if (resource.attemptHistory.length > 0) return 'attempted';
|
|
99
|
+
return 'eligible';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertNotTerminal(resource, operation) {
|
|
103
|
+
const state = stateOf(resource);
|
|
104
|
+
if (TERMINAL_STATES.has(state) || state === 'excluded') {
|
|
105
|
+
throw new Error(`cannot ${operation} resource in terminal state ${state}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function equationError(label, left, right) {
|
|
110
|
+
return `${label} (${left} != ${right})`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function reconcileCoverage(coverage) {
|
|
114
|
+
const populations = coverage?.populations;
|
|
115
|
+
if (!populations || typeof populations !== 'object') {
|
|
116
|
+
return { valid: false, errors: ['coverage populations are missing'] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const errors = [];
|
|
120
|
+
const discoveredParts = populations.excluded + populations.eligible;
|
|
121
|
+
if (populations.discovered !== discoveredParts) {
|
|
122
|
+
errors.push(equationError('discovered = excluded + eligible', populations.discovered, discoveredParts));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const eligibleParts = populations.evaluated
|
|
126
|
+
+ populations.valid_but_unevaluated
|
|
127
|
+
+ populations.failed
|
|
128
|
+
+ populations.indeterminate
|
|
129
|
+
+ populations.unvisited;
|
|
130
|
+
if (populations.eligible !== eligibleParts) {
|
|
131
|
+
errors.push(equationError(
|
|
132
|
+
'eligible = evaluated + valid_but_unevaluated + failed + indeterminate + unvisited',
|
|
133
|
+
populations.eligible,
|
|
134
|
+
eligibleParts,
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const attemptedParts = populations.retrieved + populations.failed;
|
|
139
|
+
if (populations.attempted !== attemptedParts) {
|
|
140
|
+
errors.push(equationError('attempted = retrieved + failed', populations.attempted, attemptedParts));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const retrievedParts = populations.valid_resource + populations.indeterminate;
|
|
144
|
+
if (populations.retrieved !== retrievedParts) {
|
|
145
|
+
errors.push(equationError('retrieved = valid_resource + indeterminate', populations.retrieved, retrievedParts));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const validResourceParts = populations.evaluated + populations.valid_but_unevaluated;
|
|
149
|
+
if (populations.valid_resource !== validResourceParts) {
|
|
150
|
+
errors.push(equationError(
|
|
151
|
+
'valid_resource = evaluated + valid_but_unevaluated',
|
|
152
|
+
populations.valid_resource,
|
|
153
|
+
validResourceParts,
|
|
154
|
+
));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { valid: errors.length === 0, errors };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function coverageStatusFor({ stopReason, discoveryStatus = 'complete', populations }) {
|
|
161
|
+
if (!STOP_REASONS.has(stopReason)) throw new Error(`unknown stop reason: ${stopReason}`);
|
|
162
|
+
if (!DISCOVERY_STATUSES.has(discoveryStatus)) {
|
|
163
|
+
throw new Error(`unknown discovery status: ${discoveryStatus}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (stopReason === 'page_budget_exhausted' || stopReason === 'time_budget_exhausted') {
|
|
167
|
+
return 'truncated';
|
|
168
|
+
}
|
|
169
|
+
if (stopReason === 'fatal_collection_failure') return 'indeterminate';
|
|
170
|
+
|
|
171
|
+
const incomplete = populations.failed
|
|
172
|
+
+ populations.indeterminate
|
|
173
|
+
+ populations.valid_but_unevaluated
|
|
174
|
+
+ populations.unvisited;
|
|
175
|
+
return discoveryStatus === 'complete' && incomplete === 0 ? 'complete' : 'indeterminate';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createCoverageLedger({
|
|
179
|
+
startUrl,
|
|
180
|
+
maxPages,
|
|
181
|
+
timeBudgetSeconds,
|
|
182
|
+
normalizationVersion = 'url-identity-v1',
|
|
183
|
+
}) {
|
|
184
|
+
const parsedStartUrl = assertUrl(startUrl, 'startUrl');
|
|
185
|
+
assertIntegerInRange(maxPages, 'maxPages', 1, 10000);
|
|
186
|
+
assertIntegerInRange(timeBudgetSeconds, 'timeBudgetSeconds', 1, 86400);
|
|
187
|
+
assertNonEmptyString(normalizationVersion, 'normalizationVersion');
|
|
188
|
+
|
|
189
|
+
const resources = new Map();
|
|
190
|
+
let finalized = false;
|
|
191
|
+
|
|
192
|
+
function assertMutable() {
|
|
193
|
+
if (finalized) throw new Error('coverage ledger is already finalized');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function requireResource(url) {
|
|
197
|
+
const normalized = normalizedUrl(url);
|
|
198
|
+
const resource = resources.get(normalized);
|
|
199
|
+
if (!resource) throw new Error(`resource must be discovered before transition: ${url}`);
|
|
200
|
+
return resource;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function discover(url, source, metadata = {}) {
|
|
204
|
+
assertMutable();
|
|
205
|
+
const normalized = normalizedUrl(url);
|
|
206
|
+
assertNonEmptyString(source, 'source');
|
|
207
|
+
const canonical = canonicalMetadata(metadata, 'discovery metadata');
|
|
208
|
+
let resource = resources.get(normalized);
|
|
209
|
+
if (!resource) {
|
|
210
|
+
resource = {
|
|
211
|
+
normalizedUrl: normalized,
|
|
212
|
+
originalUrls: new Set(),
|
|
213
|
+
discoverySources: new Map(),
|
|
214
|
+
exclusion: null,
|
|
215
|
+
attemptHistory: [],
|
|
216
|
+
retrieval: null,
|
|
217
|
+
classification: null,
|
|
218
|
+
evaluation: null,
|
|
219
|
+
failure: null,
|
|
220
|
+
indeterminate: null,
|
|
221
|
+
validButUnevaluated: null,
|
|
222
|
+
};
|
|
223
|
+
resources.set(normalized, resource);
|
|
224
|
+
}
|
|
225
|
+
resource.originalUrls.add(url);
|
|
226
|
+
const discovery = { source, url, ...(Object.keys(canonical).length > 0 ? { metadata: canonical } : {}) };
|
|
227
|
+
resource.discoverySources.set(stableString(discovery), discovery);
|
|
228
|
+
return normalized;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function exclude(url, reason) {
|
|
232
|
+
assertMutable();
|
|
233
|
+
assertNonEmptyString(reason, 'exclusion reason');
|
|
234
|
+
const resource = requireResource(url);
|
|
235
|
+
if (resource.attemptHistory.length > 0) throw new Error('cannot exclude a resource after an attempt');
|
|
236
|
+
if (resource.exclusion) throw new Error('resource is already excluded');
|
|
237
|
+
resource.exclusion = { reason };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function attempt(url, attemptMetadata = {}) {
|
|
241
|
+
assertMutable();
|
|
242
|
+
const resource = requireResource(url);
|
|
243
|
+
assertNotTerminal(resource, 'attempt');
|
|
244
|
+
if (resource.retrieval) throw new Error('cannot attempt a resource after retrieval');
|
|
245
|
+
resource.attemptHistory.push(canonicalMetadata(attemptMetadata, 'attempt metadata'));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function retrieve(url, responseMetadata = {}) {
|
|
249
|
+
assertMutable();
|
|
250
|
+
const resource = requireResource(url);
|
|
251
|
+
assertNotTerminal(resource, 'retrieve');
|
|
252
|
+
if (resource.attemptHistory.length === 0) throw new Error('resource must have an attempt before retrieval');
|
|
253
|
+
if (resource.retrieval) throw new Error('resource is already retrieved');
|
|
254
|
+
resource.retrieval = canonicalMetadata(responseMetadata, 'response metadata');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function classify(url, classification) {
|
|
258
|
+
assertMutable();
|
|
259
|
+
const resource = requireResource(url);
|
|
260
|
+
assertNotTerminal(resource, 'classify');
|
|
261
|
+
if (!resource.retrieval) throw new Error('resource must be retrieved before classification');
|
|
262
|
+
if (resource.classification) throw new Error('resource is already classified');
|
|
263
|
+
|
|
264
|
+
const canonical = typeof classification === 'string'
|
|
265
|
+
? { state: classification }
|
|
266
|
+
: canonicalMetadata(classification, 'classification');
|
|
267
|
+
if (canonical.state !== 'valid_resource' && canonical.state !== 'indeterminate') {
|
|
268
|
+
throw new Error('classification state must be valid_resource or indeterminate');
|
|
269
|
+
}
|
|
270
|
+
if (canonical.state === 'indeterminate') {
|
|
271
|
+
resource.indeterminate = canonical;
|
|
272
|
+
} else {
|
|
273
|
+
resource.classification = canonical;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function evaluate(url, evaluationMetadata = {}) {
|
|
278
|
+
assertMutable();
|
|
279
|
+
const resource = requireResource(url);
|
|
280
|
+
assertNotTerminal(resource, 'evaluate');
|
|
281
|
+
if (resource.classification?.state !== 'valid_resource') {
|
|
282
|
+
throw new Error('resource must be classified as a valid resource before evaluation');
|
|
283
|
+
}
|
|
284
|
+
resource.evaluation = canonicalMetadata(evaluationMetadata, 'evaluation metadata');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function fail(url, failureMetadata = {}) {
|
|
288
|
+
assertMutable();
|
|
289
|
+
const resource = requireResource(url);
|
|
290
|
+
assertNotTerminal(resource, 'fail');
|
|
291
|
+
if (resource.attemptHistory.length === 0) throw new Error('resource must have an attempt before failure');
|
|
292
|
+
if (resource.retrieval) throw new Error('retrieved resources cannot enter retrieval failure');
|
|
293
|
+
resource.failure = canonicalMetadata(failureMetadata, 'failure metadata');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function markIndeterminate(url, metadata = {}) {
|
|
297
|
+
assertMutable();
|
|
298
|
+
const resource = requireResource(url);
|
|
299
|
+
assertNotTerminal(resource, 'mark indeterminate');
|
|
300
|
+
if (!resource.retrieval) throw new Error('resource must be retrieved before it can be indeterminate');
|
|
301
|
+
if (resource.classification?.state === 'valid_resource') {
|
|
302
|
+
throw new Error('a valid resource must be evaluated or marked valid-but-unevaluated');
|
|
303
|
+
}
|
|
304
|
+
resource.indeterminate = { ...canonicalMetadata(metadata, 'indeterminate metadata'), state: 'indeterminate' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function markValidButUnevaluated(url, metadata = {}) {
|
|
308
|
+
assertMutable();
|
|
309
|
+
const resource = requireResource(url);
|
|
310
|
+
assertNotTerminal(resource, 'mark valid-but-unevaluated');
|
|
311
|
+
if (resource.classification?.state !== 'valid_resource') {
|
|
312
|
+
throw new Error('resource must be classified as a valid resource before evaluator failure');
|
|
313
|
+
}
|
|
314
|
+
resource.validButUnevaluated = canonicalMetadata(metadata, 'evaluator failure metadata');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function finalizedState(resource) {
|
|
318
|
+
const state = stateOf(resource);
|
|
319
|
+
if (state === 'eligible') return 'unvisited';
|
|
320
|
+
if (state === 'attempted' || state === 'retrieved' || state === 'valid_resource') {
|
|
321
|
+
throw new Error(`resource ${resource.normalizedUrl} has unresolved state ${state}`);
|
|
322
|
+
}
|
|
323
|
+
return state;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resourceOutput(resource) {
|
|
327
|
+
const state = finalizedState(resource);
|
|
328
|
+
const output = {
|
|
329
|
+
normalized_url: resource.normalizedUrl,
|
|
330
|
+
original_urls: [...resource.originalUrls].sort(compareStrings),
|
|
331
|
+
discovery_sources: [...resource.discoverySources.values()].sort((a, b) => (
|
|
332
|
+
compareStrings(a.source, b.source)
|
|
333
|
+
|| compareStrings(a.url, b.url)
|
|
334
|
+
|| compareStrings(stableString(a), stableString(b))
|
|
335
|
+
)),
|
|
336
|
+
state,
|
|
337
|
+
};
|
|
338
|
+
if (resource.exclusion) output.exclusion = resource.exclusion;
|
|
339
|
+
if (resource.attemptHistory.length > 0) output.attempt_history = resource.attemptHistory;
|
|
340
|
+
if (resource.retrieval) output.retrieval = resource.retrieval;
|
|
341
|
+
if (resource.classification) output.classification = resource.classification;
|
|
342
|
+
if (resource.evaluation) output.evaluation = resource.evaluation;
|
|
343
|
+
if (resource.failure) output.failure = resource.failure;
|
|
344
|
+
if (resource.indeterminate) output.indeterminate = resource.indeterminate;
|
|
345
|
+
if (resource.validButUnevaluated) output.evaluator_failure = resource.validButUnevaluated;
|
|
346
|
+
return output;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function finalize(stopReason, options = {}) {
|
|
350
|
+
assertMutable();
|
|
351
|
+
if (!STOP_REASONS.has(stopReason)) throw new Error(`unknown stop reason: ${stopReason}`);
|
|
352
|
+
const {
|
|
353
|
+
discoveryStatus = 'complete',
|
|
354
|
+
discoveryMethods,
|
|
355
|
+
discoveryLimitations = [],
|
|
356
|
+
limitations = [],
|
|
357
|
+
preview = false,
|
|
358
|
+
} = options;
|
|
359
|
+
if (!DISCOVERY_STATUSES.has(discoveryStatus)) {
|
|
360
|
+
throw new Error(`unknown discovery status: ${discoveryStatus}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const provisional = [];
|
|
364
|
+
if (preview) {
|
|
365
|
+
for (const resource of resources.values()) {
|
|
366
|
+
if (resource.classification?.state === 'valid_resource' && !resource.evaluation && !resource.validButUnevaluated) {
|
|
367
|
+
resource.validButUnevaluated = { reason: 'evaluation_pending' };
|
|
368
|
+
provisional.push(resource);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const outputResources = [...resources.values()]
|
|
373
|
+
.map(resourceOutput)
|
|
374
|
+
.sort((a, b) => compareStrings(a.normalized_url, b.normalized_url));
|
|
375
|
+
const stateCount = (state) => outputResources.filter((resource) => resource.state === state).length;
|
|
376
|
+
const populations = {
|
|
377
|
+
discovered: outputResources.length,
|
|
378
|
+
eligible: outputResources.length - stateCount('excluded'),
|
|
379
|
+
excluded: stateCount('excluded'),
|
|
380
|
+
attempted: outputResources.filter((resource) => resource.attempt_history).length,
|
|
381
|
+
retrieved: outputResources.filter((resource) => resource.retrieval).length,
|
|
382
|
+
valid_resource: outputResources.filter((resource) => resource.classification?.state === 'valid_resource').length,
|
|
383
|
+
evaluated: stateCount('evaluated'),
|
|
384
|
+
valid_but_unevaluated: stateCount('valid_but_unevaluated'),
|
|
385
|
+
failed: stateCount('failed'),
|
|
386
|
+
indeterminate: stateCount('indeterminate'),
|
|
387
|
+
unvisited: stateCount('unvisited'),
|
|
388
|
+
};
|
|
389
|
+
const methods = discoveryMethods === undefined
|
|
390
|
+
? [...new Set(outputResources.flatMap((resource) => resource.discovery_sources.map(({ source }) => source)))].sort(compareStrings)
|
|
391
|
+
: sortedUniqueStrings(discoveryMethods, 'discoveryMethods');
|
|
392
|
+
const coverage = {
|
|
393
|
+
schema_version: COVERAGE_SCHEMA_VERSION,
|
|
394
|
+
normalization_version: normalizationVersion,
|
|
395
|
+
requested_scope: {
|
|
396
|
+
start_url: startUrl,
|
|
397
|
+
origin: parsedStartUrl.origin,
|
|
398
|
+
max_pages: maxPages,
|
|
399
|
+
time_budget_seconds: timeBudgetSeconds,
|
|
400
|
+
},
|
|
401
|
+
discovery: {
|
|
402
|
+
status: discoveryStatus,
|
|
403
|
+
methods,
|
|
404
|
+
limitations: sortedUniqueStrings(discoveryLimitations, 'discoveryLimitations'),
|
|
405
|
+
},
|
|
406
|
+
populations,
|
|
407
|
+
resources: outputResources,
|
|
408
|
+
stop_reason: stopReason,
|
|
409
|
+
coverage_status: coverageStatusFor({ stopReason, discoveryStatus, populations }),
|
|
410
|
+
reconciliation: { valid: false, errors: ['not reconciled'] },
|
|
411
|
+
limitations: sortedUniqueStrings(limitations, 'limitations'),
|
|
412
|
+
};
|
|
413
|
+
coverage.reconciliation = reconcileCoverage(coverage);
|
|
414
|
+
if (!coverage.reconciliation.valid) {
|
|
415
|
+
throw new Error(`coverage reconciliation failed: ${coverage.reconciliation.errors.join('; ')}`);
|
|
416
|
+
}
|
|
417
|
+
const schemaValidation = validateAgainst('audit-coverage.schema.json', coverage);
|
|
418
|
+
if (!schemaValidation.valid) {
|
|
419
|
+
throw new Error(`audit coverage invalid: ${schemaValidation.errors.join('; ')}`);
|
|
420
|
+
}
|
|
421
|
+
for (const resource of provisional) resource.validButUnevaluated = null;
|
|
422
|
+
if (!preview) finalized = true;
|
|
423
|
+
return coverage;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Read-only state inspection for detectors that must distinguish an
|
|
427
|
+
// unvisited sitemap URL from an observed retrieval failure. This does not
|
|
428
|
+
// expose mutable ledger internals or alter reconciliation.
|
|
429
|
+
function stateFor(url) {
|
|
430
|
+
const resource = resources.get(normalizedUrl(url));
|
|
431
|
+
return resource ? stateOf(resource) : null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
discover,
|
|
436
|
+
exclude,
|
|
437
|
+
attempt,
|
|
438
|
+
retrieve,
|
|
439
|
+
classify,
|
|
440
|
+
evaluate,
|
|
441
|
+
fail,
|
|
442
|
+
markIndeterminate,
|
|
443
|
+
markValidButUnevaluated,
|
|
444
|
+
stateFor,
|
|
445
|
+
preview: (stopReason, options = {}) => finalize(stopReason, { ...options, preview: true }),
|
|
446
|
+
finalize,
|
|
447
|
+
};
|
|
448
|
+
}
|