@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.
Files changed (298) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +12 -1
  3. package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +19 -13
  6. package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
  7. package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
  8. package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  9. package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
  10. package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  11. package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  12. package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
  13. package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
  14. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
  15. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
  16. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
  17. package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
  18. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
  19. package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
  20. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  21. package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
  22. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  23. package/dist/universal/.claude/skills/citable/manifest.json +19 -13
  24. package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
  25. package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
  26. package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  27. package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
  28. package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  29. package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  30. package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
  31. package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
  32. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
  33. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
  34. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
  35. package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
  36. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
  37. package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
  38. package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
  39. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  40. package/dist/universal/.cursor/skills/citable/manifest.json +19 -13
  41. package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
  42. package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
  43. package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  44. package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
  45. package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
  48. package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
  49. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
  50. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
  51. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
  52. package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
  53. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
  54. package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
  55. package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
  56. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  57. package/dist/universal/.gemini/skills/citable/manifest.json +19 -13
  58. package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
  59. package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
  60. package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  61. package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
  62. package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  63. package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  64. package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
  65. package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
  66. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
  67. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
  68. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
  69. package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
  70. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
  71. package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
  72. package/dist/universal/.github/skills/citable/SKILL.md +11 -2
  73. package/dist/universal/.github/skills/citable/VERSION +1 -1
  74. package/dist/universal/.github/skills/citable/manifest.json +19 -13
  75. package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
  76. package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
  77. package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  78. package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
  79. package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  80. package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  81. package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
  82. package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
  83. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
  84. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
  85. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
  86. package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
  87. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
  88. package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
  89. package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
  90. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  91. package/dist/universal/.kiro/skills/citable/manifest.json +19 -13
  92. package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
  93. package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
  94. package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  95. package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
  96. package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  97. package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  98. package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
  99. package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
  100. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
  101. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
  102. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
  103. package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
  104. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
  105. package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
  106. package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
  107. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  108. package/dist/universal/.opencode/skills/citable/manifest.json +19 -13
  109. package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
  110. package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
  111. package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  112. package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
  113. package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  114. package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  115. package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
  116. package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
  117. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
  118. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
  119. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
  120. package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
  121. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
  122. package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
  123. package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
  124. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  125. package/dist/universal/.pi/agent/skills/citable/manifest.json +19 -13
  126. package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
  127. package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
  128. package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  129. package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
  130. package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  131. package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  132. package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
  133. package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
  134. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
  135. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
  136. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
  137. package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
  138. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
  139. package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
  140. package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
  141. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  142. package/dist/universal/.qoder/skills/citable/manifest.json +19 -13
  143. package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
  144. package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
  145. package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  146. package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
  147. package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  148. package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  149. package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
  150. package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
  151. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
  152. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
  153. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
  154. package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
  155. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
  156. package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
  157. package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
  158. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  159. package/dist/universal/.rovodev/skills/citable/manifest.json +19 -13
  160. package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
  161. package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
  162. package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  163. package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
  164. package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  165. package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  166. package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
  167. package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
  168. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
  169. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
  170. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
  171. package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
  172. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
  173. package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
  174. package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
  175. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  176. package/dist/universal/.trae/skills/citable/manifest.json +19 -13
  177. package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
  178. package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
  179. package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  180. package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
  181. package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  182. package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  183. package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
  184. package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
  185. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
  186. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
  187. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
  188. package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
  189. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
  190. package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
  191. package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
  192. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  193. package/dist/universal/.trae-cn/skills/citable/manifest.json +19 -13
  194. package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
  195. package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
  196. package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  197. package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
  198. package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  199. package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  200. package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
  201. package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
  202. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
  203. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
  204. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
  205. package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
  206. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
  207. package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
  208. package/dist/universal/manifest.json +232 -160
  209. package/package.json +1 -1
  210. package/schemas/action-plan.schema.json +38 -1
  211. package/schemas/audit-coverage.schema.json +118 -0
  212. package/schemas/browser-evidence-plan.schema.json +186 -3
  213. package/schemas/browser-network-events.schema.json +142 -0
  214. package/schemas/browser-runtime-events.schema.json +112 -0
  215. package/schemas/browser-state-observations.schema.json +97 -0
  216. package/schemas/collection-result.schema.json +35 -0
  217. package/schemas/config.schema.json +2 -0
  218. package/schemas/cro-report.schema.json +8 -1
  219. package/schemas/finding.schema.json +105 -2
  220. package/schemas/remediation-verification.schema.json +12 -1
  221. package/schemas/run.schema.json +15 -2
  222. package/schemas/search-report.schema.json +8 -1
  223. package/schemas/summary.schema.json +41 -0
  224. package/skill/SKILL.md +11 -2
  225. package/src/acceptance/reproducibility.js +5 -25
  226. package/src/analysis/croRoadmap.js +3 -3
  227. package/src/analysis/strategicRoadmap.js +3 -3
  228. package/src/artifacts/interchange.js +2 -6
  229. package/src/cli/index.js +15 -3
  230. package/src/commands/actionPlan.js +46 -5
  231. package/src/commands/applyRemediation.js +5 -5
  232. package/src/commands/audit.js +75 -8
  233. package/src/commands/ciWorkflow.js +7 -2
  234. package/src/commands/compareSnapshots.js +155 -47
  235. package/src/commands/connect.js +24 -4
  236. package/src/commands/context.js +41 -2
  237. package/src/commands/croSuite.js +51 -13
  238. package/src/commands/delivery.js +7 -7
  239. package/src/commands/governance.js +9 -10
  240. package/src/commands/implementationKit.js +2 -1
  241. package/src/commands/init.js +8 -1
  242. package/src/commands/previewCro.js +1 -1
  243. package/src/commands/prioritize.js +3 -2
  244. package/src/commands/reviews.js +31 -5
  245. package/src/commands/roadmapCmd.js +7 -13
  246. package/src/commands/sweep.js +15 -2
  247. package/src/commands/verifyRemediation.js +94 -19
  248. package/src/connectors/collectionResult.js +80 -0
  249. package/src/connectors/ga4.js +46 -11
  250. package/src/connectors/gsc.js +41 -6
  251. package/src/connectors/index.js +1 -1
  252. package/src/connectors/webflow.js +140 -10
  253. package/src/connectors/wordpress.js +92 -8
  254. package/src/crawler/fetch.js +220 -24
  255. package/src/crawler/resourceValidity.js +119 -0
  256. package/src/crawler/sitemap.js +435 -16
  257. package/src/crawler/sitemapCollector.js +364 -0
  258. package/src/crawler/urlIdentity.js +97 -0
  259. package/src/detectors/agent.js +11 -0
  260. package/src/detectors/ans.js +16 -1
  261. package/src/detectors/arch.js +6 -0
  262. package/src/detectors/claim.js +9 -0
  263. package/src/detectors/crawl.js +7 -0
  264. package/src/detectors/cro.js +21 -0
  265. package/src/detectors/cwv.js +4 -0
  266. package/src/detectors/entity.js +7 -0
  267. package/src/detectors/evd.js +9 -0
  268. package/src/detectors/framework.js +28 -2
  269. package/src/detectors/geoReco.js +16 -0
  270. package/src/detectors/hreflang.js +3 -0
  271. package/src/detectors/lifeMeas.js +9 -0
  272. package/src/detectors/link.js +6 -1
  273. package/src/detectors/page.js +11 -0
  274. package/src/detectors/schemaData.js +16 -1
  275. package/src/detectors/tech.js +34 -3
  276. package/src/evidence/coverage.js +448 -0
  277. package/src/evidence/determination.js +157 -0
  278. package/src/evidence/downstream.js +110 -0
  279. package/src/evidence/hashes.js +109 -0
  280. package/src/evidence/run.js +68 -8
  281. package/src/extractor/page.js +29 -2
  282. package/src/extractor/site.js +298 -50
  283. package/src/observations/browser/correlation.js +83 -0
  284. package/src/observations/browser/networkCollector.js +413 -0
  285. package/src/observations/browser/runtimeEventCollector.js +337 -0
  286. package/src/observations/browser/sanitizer.js +322 -0
  287. package/src/observations/browser/stateCollector.js +386 -0
  288. package/src/observations/browserJourney.js +131 -3
  289. package/src/reporting/executiveCroReport.js +37 -14
  290. package/src/reporting/executiveExport.js +45 -16
  291. package/src/reporting/executiveSearchReport.js +57 -23
  292. package/src/reporting/report.js +56 -9
  293. package/src/shared/epistemicLanguage.js +86 -0
  294. package/src/shared/evidenceSourceResolver.js +48 -17
  295. package/src/shared/htmlEscape.js +8 -4
  296. package/src/shared/runPackageVerifier.js +99 -8
  297. package/src/shared/verifiedRunLoader.js +129 -0
  298. package/src/sow/generateSow.js +1 -1
@@ -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
- if (!page && ctx.site.mode === 'url' && ctx.site.crawl?.truncated) {
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: [`sitemap ${sm.source} lists ${u.loc}; no matching page found`],
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
+ }