@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
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Coverage requirements for determinations.
3
+ *
4
+ * A requirement describes the population a conclusion is allowed to speak
5
+ * about. It is deliberately separate from the crawler's execution status:
6
+ * observing one page can support a page-local positive finding even when a
7
+ * corpus-wide absence claim is not supportable.
8
+ */
9
+ export const REQUIREMENTS = Object.freeze({
10
+ PAGE_RESOURCE: 'page_resource',
11
+ EVALUATED_SUBSET: 'evaluated_subset',
12
+ DECLARED_SAMPLE: 'declared_sample',
13
+ EXHAUSTIVE_SCOPE: 'exhaustive_requested_scope',
14
+ RENDERED_RESOURCE: 'rendered_resource',
15
+ PROVIDER_BOUNDED: 'provider_bounded',
16
+ });
17
+
18
+ const VALID_REQUIREMENTS = new Set(Object.values(REQUIREMENTS));
19
+
20
+ export function isCoverageRequirement(value) {
21
+ return VALID_REQUIREMENTS.has(value);
22
+ }
23
+
24
+ function population(coverage) {
25
+ return coverage?.populations && typeof coverage.populations === 'object'
26
+ ? coverage.populations
27
+ : {};
28
+ }
29
+
30
+ function normalizedUrl(value) {
31
+ if (typeof value !== 'string' || !value) return null;
32
+ try {
33
+ const url = new URL(value);
34
+ url.hash = '';
35
+ return url.href;
36
+ } catch {
37
+ return value.split('#', 1)[0];
38
+ }
39
+ }
40
+
41
+ function resourcesFor(coverage) {
42
+ return Array.isArray(coverage?.resources) ? coverage.resources : [];
43
+ }
44
+
45
+ function resourceForSubject(coverage, subject) {
46
+ const candidates = [subject?.url, subject?.identifier].map(normalizedUrl).filter(Boolean);
47
+ return resourcesFor(coverage).find((resource) => {
48
+ const resourceUrl = normalizedUrl(resource?.url || resource?.normalized_url);
49
+ return resourceUrl && candidates.includes(resourceUrl);
50
+ }) || null;
51
+ }
52
+
53
+ function completeCoverage(coverage) {
54
+ const p = population(coverage);
55
+ return coverage?.coverage_status === 'complete'
56
+ && p.unvisited === 0
57
+ && p.failed === 0
58
+ && p.indeterminate === 0
59
+ && p.valid_but_unevaluated === 0
60
+ && p.evaluated > 0;
61
+ }
62
+
63
+ function result(status, extra = {}) {
64
+ return { status, ...extra };
65
+ }
66
+
67
+ function evaluatePageResource(coverage, subject, rendered = false) {
68
+ const resource = resourceForSubject(coverage, subject);
69
+ if (!resource) return result('indeterminate', { reason: 'resource_not_observed' });
70
+ const state = resource.state || (resource.evaluation ? 'evaluated' : resource.classification?.state);
71
+ if (state !== 'evaluated') {
72
+ return result('indeterminate', { reason: 'resource_not_evaluated', resource_id: resource.resource_id });
73
+ }
74
+ if (rendered && resource.rendered !== true && resource.rendering_status !== 'complete') {
75
+ return result('indeterminate', { reason: 'rendered_observation_unavailable', resource_id: resource.resource_id });
76
+ }
77
+ return result('supported', {
78
+ resource_id: resource.resource_id,
79
+ population: 'evaluated_resource',
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Evaluate whether a coverage envelope satisfies one determination contract.
85
+ * The returned status is intentionally limited to supported, qualified,
86
+ * indeterminate, and not_applicable, matching the finding/run contracts.
87
+ */
88
+ export function evaluateRequirement(requirement, coverage, subject = null) {
89
+ if (!VALID_REQUIREMENTS.has(requirement)) throw new TypeError(`unknown coverage requirement: ${requirement}`);
90
+
91
+ if (requirement === REQUIREMENTS.PAGE_RESOURCE) return evaluatePageResource(coverage, subject);
92
+ if (requirement === REQUIREMENTS.RENDERED_RESOURCE) return evaluatePageResource(coverage, subject, true);
93
+
94
+ if (requirement === REQUIREMENTS.DECLARED_SAMPLE) {
95
+ const sample = coverage?.sample;
96
+ const numerator = sample?.numerator;
97
+ const denominator = sample?.denominator;
98
+ if (!Number.isInteger(numerator) || !Number.isInteger(denominator) || denominator < 1 || numerator < 0 || numerator > denominator) {
99
+ return result('indeterminate', { reason: 'sample_denominator_unavailable' });
100
+ }
101
+ return { status: 'qualified', population: 'evaluated_subset', numerator, denominator };
102
+ }
103
+
104
+ if (requirement === REQUIREMENTS.PROVIDER_BOUNDED) {
105
+ const p = population(coverage);
106
+ if (!Number.isInteger(p.evaluated) || p.evaluated < 1) return result('indeterminate', { reason: 'no_evaluated_resources' });
107
+ return result('qualified', { population: 'provider_bounded', denominator: p.evaluated });
108
+ }
109
+
110
+ if (requirement === REQUIREMENTS.EVALUATED_SUBSET) {
111
+ // A subject-bearing determination cannot borrow aggregate subset counts:
112
+ // the named page/URL must itself be an evaluator-completed resource.
113
+ if (subject?.type === 'page' || subject?.type === 'url') {
114
+ return evaluatePageResource(coverage, subject);
115
+ }
116
+ const p = population(coverage);
117
+ if (!Number.isInteger(p.evaluated) || p.evaluated < 1) return result('indeterminate', { reason: 'no_evaluated_resources' });
118
+ return result(coverage?.coverage_status === 'complete' ? 'supported' : 'qualified', {
119
+ population: 'evaluated_subset', denominator: p.evaluated,
120
+ });
121
+ }
122
+
123
+ // Exhaustive scope is the only requirement that may support a corpus-wide
124
+ // absence claim. A completed execution with zero resources is not evidence
125
+ // of absence, so require at least one evaluated resource.
126
+ if (completeCoverage(coverage)) {
127
+ return result('supported', { population: 'requested_scope', denominator: population(coverage).evaluated });
128
+ }
129
+ return result('indeterminate', { reason: 'coverage_incomplete' });
130
+ }
131
+
132
+ /**
133
+ * Apply coverage semantics to a determination produced by a detector or
134
+ * downstream consumer. Positive page-local observations are preserved; a
135
+ * negative exhaustive conclusion is downgraded when its corpus is incomplete.
136
+ */
137
+ export function propagateDetermination(determination, coverage) {
138
+ if (!determination || typeof determination !== 'object') throw new TypeError('determination must be an object');
139
+ const current = determination.status;
140
+ if (!['supported', 'qualified', 'indeterminate', 'not_applicable'].includes(current)) {
141
+ throw new TypeError(`invalid determination status: ${current}`);
142
+ }
143
+ if (current !== 'supported') return { ...determination };
144
+ if (determination.polarity !== 'negative' || !determination.requirement) return { ...determination };
145
+ const evaluated = evaluateRequirement(determination.requirement, coverage, determination.subject);
146
+ if (evaluated.status === 'supported') return { ...determination };
147
+ return { ...determination, status: evaluated.status, reason: evaluated.reason || 'coverage_requirement_unsatisfied' };
148
+ }
149
+
150
+ export function requirementForDetector(detector, hit = null) {
151
+ if (!detector || !isCoverageRequirement(detector.coverage_requirement)) {
152
+ throw new TypeError(`detector ${detector?.id ?? '?'} must declare a valid coverage_requirement`);
153
+ }
154
+ // The detector declaration is authoritative. Subject shape must never
155
+ // silently weaken an exhaustive or rendered-resource contract.
156
+ return detector.coverage_requirement;
157
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Shared semantics for consumers that summarize or act on an evidence package.
3
+ * A consumer may describe the evaluated subset, but may not widen it to the
4
+ * requested scope when collection was incomplete or empty.
5
+ */
6
+ const DETERMINATION_STATUSES = new Set(['supported', 'qualified', 'indeterminate', 'not_applicable']);
7
+ import { validateAgainst } from '../shared/schemaValidator.js';
8
+
9
+ function populations(coverage) {
10
+ return coverage?.populations && typeof coverage.populations === 'object'
11
+ ? coverage.populations : {};
12
+ }
13
+
14
+ function contractStatus(coverage) {
15
+ if (!coverage || typeof coverage !== 'object') return { valid: false, errors: ['coverage artifact unavailable'] };
16
+ const result = validateAgainst('audit-coverage.schema.json', coverage);
17
+ return { valid: result.valid, errors: result.errors || [] };
18
+ }
19
+
20
+ export function coverageDenominator(coverage) {
21
+ const value = populations(coverage).evaluated;
22
+ return Number.isInteger(value) && value > 0 ? value : 0;
23
+ }
24
+
25
+ export function completeLocalCoverage(pages, baseUrl = 'https://example.test') {
26
+ const origin = new URL(baseUrl).origin;
27
+ const resources = pages.map((page, index) => ({
28
+ resource_id: `LOCAL-${index + 1}`,
29
+ normalized_url: page.url || `${origin}/page-${index + 1}`,
30
+ original_urls: [page.url || `${origin}/page-${index + 1}`],
31
+ state: 'evaluated',
32
+ evaluation: {},
33
+ }));
34
+ const n = resources.length;
35
+ return {
36
+ schema_version: 1,
37
+ normalization_version: 'url-identity-v1',
38
+ requested_scope: { start_url: `${origin}/`, origin, max_pages: 500, time_budget_seconds: 1800 },
39
+ discovery: { status: 'complete', methods: ['built_output'], limitations: [] },
40
+ populations: { discovered: n, eligible: n, excluded: 0, attempted: n, retrieved: n, valid_resource: n, evaluated: n, valid_but_unevaluated: 0, failed: 0, indeterminate: 0, unvisited: 0 },
41
+ resources,
42
+ stop_reason: 'frontier_exhausted',
43
+ coverage_status: 'complete',
44
+ reconciliation: { valid: true, errors: [] },
45
+ limitations: [],
46
+ };
47
+ }
48
+
49
+ export function downstreamStatus(coverage) {
50
+ if (!coverage) return 'indeterminate';
51
+ const contract = contractStatus(coverage);
52
+ const denominator = coverageDenominator(coverage);
53
+ const p = populations(coverage);
54
+ const completePopulation = p.failed === 0 && p.indeterminate === 0 && p.unvisited === 0 && p.valid_but_unevaluated === 0;
55
+ if (coverage.coverage_status === 'complete' && denominator > 0 && completePopulation && contract.valid && coverage.reconciliation?.valid === true) return 'supported';
56
+ if (coverage.coverage_status === 'truncated' && denominator > 0
57
+ && contract.valid && coverage.reconciliation?.valid === true) return 'qualified';
58
+ return 'indeterminate';
59
+ }
60
+
61
+ export function coverageLimitations(coverage) {
62
+ if (!coverage) return ['coverage artifact unavailable; scope is not established'];
63
+ const p = populations(coverage);
64
+ const limitations = Array.isArray(coverage.limitations) ? [...coverage.limitations] : [];
65
+ const contract = contractStatus(coverage);
66
+ if (!contract.valid) limitations.push('coverage artifact does not satisfy the coverage contract');
67
+ if (coverage.coverage_status !== 'complete') {
68
+ limitations.push(`coverage is ${coverage.coverage_status || 'unknown'}; conclusions are limited to evaluated resources`);
69
+ }
70
+ for (const key of ['failed', 'indeterminate', 'unvisited', 'valid_but_unevaluated']) {
71
+ if (Number.isInteger(p[key]) && p[key] > 0) limitations.push(`${p[key]} ${key} resource(s) omitted from determinations`);
72
+ }
73
+ return [...new Set(limitations)];
74
+ }
75
+
76
+ export function downstreamEnvelope(coverage, extra = {}) {
77
+ const p = populations(coverage);
78
+ const denominator = coverageDenominator(coverage);
79
+ const status = downstreamStatus(coverage);
80
+ if (!DETERMINATION_STATUSES.has(status)) throw new Error(`invalid downstream determination status: ${status}`);
81
+ return {
82
+ coverage_status: coverage?.coverage_status || 'indeterminate',
83
+ determination_status: status,
84
+ evidence_scope: {
85
+ requirement: 'evaluated_subset',
86
+ satisfaction: status,
87
+ coverage_ref: coverage ? 'coverage.json' : null,
88
+ resource_ids: Array.isArray(coverage?.resources)
89
+ ? coverage.resources.map((resource) => resource.resource_id).filter(Boolean)
90
+ : [],
91
+ population: denominator > 0 ? 'evaluated_subset' : 'none_observed',
92
+ evaluated: denominator,
93
+ eligible: Number.isInteger(p.eligible) ? p.eligible : null,
94
+ excluded: Number.isInteger(p.excluded) ? p.excluded : null,
95
+ failed: Number.isInteger(p.failed) ? p.failed : null,
96
+ indeterminate: Number.isInteger(p.indeterminate) ? p.indeterminate : null,
97
+ unvisited: Number.isInteger(p.unvisited) ? p.unvisited : null,
98
+ denominator,
99
+ },
100
+ limitations: coverageLimitations(coverage),
101
+ ...extra,
102
+ };
103
+ }
104
+
105
+ /** Return null for empty evidence; never divide by a convenient zero. */
106
+ export function scoreFromCoverage(numerator, coverage, { scale = 100 } = {}) {
107
+ const denominator = coverageDenominator(coverage);
108
+ if (!Number.isFinite(numerator) || denominator === 0) return null;
109
+ return Math.round((numerator / denominator) * scale);
110
+ }
@@ -0,0 +1,109 @@
1
+ import { sha256 } from '../shared/io.js';
2
+
3
+ export const EVIDENCE_HASH_SCHEMA_VERSION = 'evidence-hashes-v1';
4
+
5
+ /**
6
+ * Canonical JSON used for evidence representations. Object key order is not
7
+ * evidence; array order is preserved because it can be meaningful (for
8
+ * example, JSON-LD graph order and an evaluator's finding order).
9
+ */
10
+ export function canonicalEvidenceJson(value) {
11
+ if (Array.isArray(value)) return `[${value.map(canonicalEvidenceJson).join(',')}]`;
12
+ if (value && typeof value === 'object') {
13
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalEvidenceJson(value[key])}`).join(',')}}`;
14
+ }
15
+ return JSON.stringify(value);
16
+ }
17
+
18
+ function bytes(value) {
19
+ if (Buffer.isBuffer(value)) return value;
20
+ if (value instanceof Uint8Array) return Buffer.from(value);
21
+ return Buffer.from(String(value ?? ''), 'utf8');
22
+ }
23
+
24
+ function hashBytes(value) {
25
+ return sha256(bytes(value));
26
+ }
27
+
28
+ function hashCanonical(value) {
29
+ return sha256(canonicalEvidenceJson(value));
30
+ }
31
+
32
+ /** Normalize only extraction whitespace; do not remove content or metadata. */
33
+ export function normalizeExtractedText(value) {
34
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
35
+ }
36
+
37
+ /**
38
+ * Build named hashes for representations that are actually available.
39
+ * Null means that representation was not observed; it is never synthesized
40
+ * from another layer.
41
+ */
42
+ export function createEvidenceHashes({
43
+ responseBody,
44
+ extractedText,
45
+ structuredData,
46
+ renderedDom,
47
+ evidence,
48
+ artifact,
49
+ } = {}) {
50
+ return {
51
+ hash_schema_version: EVIDENCE_HASH_SCHEMA_VERSION,
52
+ response_body_hash: responseBody == null ? null : hashBytes(responseBody),
53
+ rendered_dom_hash: renderedDom == null ? null : hashBytes(renderedDom),
54
+ extracted_text_hash: extractedText == null ? null : hashBytes(normalizeExtractedText(extractedText)),
55
+ structured_data_hash: structuredData == null ? null : hashCanonical(structuredData),
56
+ evidence_hash: evidence == null ? null : hashCanonical(evidence),
57
+ artifact_hash: artifact == null ? null : hashCanonical(artifact),
58
+ };
59
+ }
60
+
61
+ /** Explicit legacy meaning for snapshots and connector payloads during migration. */
62
+ export const LEGACY_CONTENT_HASH_SEMANTICS = 'extracted_text_v1';
63
+
64
+ /**
65
+ * Canonical shape of the persisted pages/index.json record. The artifact hash
66
+ * is intentionally excluded so the hash does not contain itself; verification
67
+ * recomputes this exact record shape and compares it with the stored claim.
68
+ */
69
+ export function pageArtifactRecord(page) {
70
+ return {
71
+ url: page.url,
72
+ status: page.status,
73
+ title: page.title,
74
+ canonicals: page.canonicals,
75
+ noindex: page.noindex,
76
+ wordCount: page.wordCount,
77
+ sourceFile: page.sourceFile,
78
+ requested_url: page.requestedUrl ?? null,
79
+ effective_url: page.url,
80
+ declared_canonical_url: page.canonicals?.[0] ?? null,
81
+ resource_id: page.urlIdentity?.resource_id ?? null,
82
+ url_identity: page.urlIdentity ?? null,
83
+ hash_schema_version: page.hash_schema_version ?? null,
84
+ response_body_hash: page.response_body_hash ?? null,
85
+ rendered_dom_hash: page.rendered_dom_hash ?? null,
86
+ extracted_text_hash: page.extracted_text_hash ?? null,
87
+ structured_data_hash: page.structured_data_hash ?? null,
88
+ evidence_hash: page.evidence_hash ?? null,
89
+ contentHash: page.extracted_text_hash ?? null,
90
+ hash_semantics: LEGACY_CONTENT_HASH_SEMANTICS,
91
+ };
92
+ }
93
+
94
+ export function hashPageArtifact(page) {
95
+ return hashCanonical(pageArtifactRecord(page));
96
+ }
97
+
98
+ /** Bind a determination/finding to the representation its hash actually covers. */
99
+ export function evidenceIdentity(hashes, representation) {
100
+ const field = `${representation}_hash`;
101
+ const hash = hashes?.[field];
102
+ if (!['response_body', 'rendered_dom', 'extracted_text', 'structured_data', 'evidence', 'artifact'].includes(representation)) {
103
+ throw new TypeError(`unknown evidence representation: ${representation}`);
104
+ }
105
+ if (typeof hash !== 'string' || !/^[a-f0-9]{64}$/.test(hash)) {
106
+ throw new Error(`evidence representation is unavailable: ${representation}`);
107
+ }
108
+ return { hash_schema_version: hashes.hash_schema_version ?? EVIDENCE_HASH_SCHEMA_VERSION, representation, hash };
109
+ }
@@ -23,6 +23,26 @@ function gitInfo(root) {
23
23
  }
24
24
  }
25
25
 
26
+ function assertSafeArtifactPath(runDir, relativePath) {
27
+ if (typeof relativePath !== 'string' || !relativePath || path.isAbsolute(relativePath) || relativePath.includes('\\')) {
28
+ throw new Error(`unsafe artifact path: ${relativePath}`);
29
+ }
30
+ const parts = relativePath.split('/');
31
+ if (parts.some((part) => !part || part === '.' || part === '..')) throw new Error(`unsafe artifact path: ${relativePath}`);
32
+ let current = runDir;
33
+ for (const part of parts) {
34
+ current = path.join(current, part);
35
+ if (!fs.existsSync(current)) continue;
36
+ const stat = fs.lstatSync(current);
37
+ if (stat.isSymbolicLink()) throw new Error(`artifact path contains symbolic link: ${relativePath}`);
38
+ }
39
+ }
40
+
41
+ function fsyncPath(file) {
42
+ const handle = fs.openSync(file, 'r');
43
+ try { fs.fsyncSync(handle); } finally { fs.closeSync(handle); }
44
+ }
45
+
26
46
  /** Create a run evidence package under <root>/.citable/runs/<run-id>/. */
27
47
  export function createRun(root, { command, argv = [], target, locale = process.env.LANG || 'en', jurisdiction = null, configHash = null }) {
28
48
  const runId = newRunId(command);
@@ -30,6 +50,7 @@ export function createRun(root, { command, argv = [], target, locale = process.e
30
50
  fs.mkdirSync(dir, { recursive: true });
31
51
  const git = gitInfo(root);
32
52
  const manifest = {
53
+ schema_version: 2,
33
54
  run_id: runId,
34
55
  command,
35
56
  argv,
@@ -50,6 +71,9 @@ export function createRun(root, { command, argv = [], target, locale = process.e
50
71
  errors: [],
51
72
  warnings: [],
52
73
  status: 'incomplete',
74
+ execution_status: 'completed',
75
+ coverage_status: 'not_applicable',
76
+ determination_status: 'not_applicable',
53
77
  };
54
78
  return {
55
79
  runId,
@@ -59,6 +83,7 @@ export function createRun(root, { command, argv = [], target, locale = process.e
59
83
  manifest.input_hashes[name] = sha256(typeof content === 'string' ? content : JSON.stringify(content));
60
84
  },
61
85
  writeArtifact(relPath, data) {
86
+ assertSafeArtifactPath(dir, relPath);
62
87
  const file = path.join(dir, relPath);
63
88
  fs.mkdirSync(path.dirname(file), { recursive: true });
64
89
  if (typeof data === 'string' || Buffer.isBuffer(data)) fs.writeFileSync(file, data);
@@ -70,18 +95,53 @@ export function createRun(root, { command, argv = [], target, locale = process.e
70
95
  manifest.status = status;
71
96
  const { valid, errors } = validateAgainst('run.schema.json', manifest);
72
97
  if (!valid) throw new Error(`run manifest invalid: ${errors.join('; ')}`);
73
- writeJson(path.join(dir, 'manifest.json'), manifest);
98
+ const stage = `${dir}.sealing-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
99
+ fs.rmSync(stage, { recursive: true, force: true });
100
+ fs.cpSync(dir, stage, { recursive: true, dereference: false, errorOnExist: true });
101
+ writeJson(path.join(stage, 'manifest.json'), manifest);
102
+ const summaryFile = path.join(stage, 'summary.json');
103
+ if (fs.existsSync(summaryFile)) {
104
+ // Keep the persisted ordering contract deterministic even on filesystems
105
+ // with coarse timestamp resolution or delayed copy metadata updates.
106
+ const now = new Date(Date.now() + 1000);
107
+ fs.utimesSync(summaryFile, now, now);
108
+ }
74
109
  // checksums over every artifact in the package
75
- const checksums = {};
110
+ const checksumEntries = [];
76
111
  const walk = (d) => {
77
- for (const name of fs.readdirSync(d)) {
78
- const p = path.join(d, name);
79
- if (fs.statSync(p).isDirectory()) walk(p);
80
- else if (name !== 'checksums.json') checksums[path.relative(dir, p)] = sha256File(p);
112
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
113
+ const p = path.join(d, entry.name);
114
+ if (entry.isSymbolicLink()) throw new Error(`staging package contains symbolic link: ${path.relative(stage, p)}`);
115
+ if (entry.isDirectory()) walk(p);
116
+ else if (entry.isFile() && entry.name !== 'checksums.json') checksumEntries.push([path.relative(stage, p).split(path.sep).join('/'), sha256File(p)]);
117
+ else if (!entry.isFile()) throw new Error(`staging package contains unsupported entry: ${path.relative(stage, p)}`);
118
+ }
119
+ };
120
+ walk(stage);
121
+ const checksums = Object.fromEntries(checksumEntries.sort(([a], [b]) => a.localeCompare(b)));
122
+ writeJson(path.join(stage, 'checksums.json'), checksums);
123
+ const fsyncTree = (directory) => {
124
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
125
+ const file = path.join(directory, entry.name);
126
+ if (entry.isSymbolicLink()) throw new Error(`staging package contains symbolic link: ${path.relative(stage, file)}`);
127
+ if (entry.isDirectory()) fsyncTree(file);
128
+ else if (entry.isFile()) fsyncPath(file);
81
129
  }
130
+ fsyncPath(directory);
82
131
  };
83
- walk(dir);
84
- writeJson(path.join(dir, 'checksums.json'), checksums);
132
+ fsyncTree(stage);
133
+ const parent = path.dirname(dir);
134
+ const displaced = `${dir}.partial-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
135
+ fs.renameSync(dir, displaced);
136
+ try {
137
+ fs.renameSync(stage, dir);
138
+ fs.rmSync(displaced, { recursive: true, force: true });
139
+ fsyncPath(parent);
140
+ } catch (error) {
141
+ if (!fs.existsSync(dir) && fs.existsSync(displaced)) fs.renameSync(displaced, dir);
142
+ fs.rmSync(stage, { recursive: true, force: true });
143
+ throw error;
144
+ }
85
145
  return dir;
86
146
  },
87
147
  };
@@ -1,10 +1,11 @@
1
1
  import { parse } from 'node-html-parser';
2
+ import { createEvidenceHashes, hashPageArtifact } from '../evidence/hashes.js';
2
3
 
3
4
  /**
4
5
  * Extract a PageModel from raw HTML plus transport metadata.
5
6
  * Everything downstream (detectors, inspect, schema) consumes this model.
6
7
  */
7
- export function extractPage({ url, html, status = 200, headers = {}, sourceFile = null, redirectChain = [] }) {
8
+ export function extractPage({ url, html, responseBody = null, status = 200, headers = {}, sourceFile = null, redirectChain = [], renderedDom = null }) {
8
9
  const root = parse(html, { comment: true });
9
10
  const head = root.querySelector('head');
10
11
 
@@ -282,7 +283,7 @@ export function extractPage({ url, html, status = 200, headers = {}, sourceFile
282
283
  };
283
284
  walkDom(root, 1);
284
285
 
285
- return {
286
+ const page = {
286
287
  url,
287
288
  sourceFile,
288
289
  status,
@@ -323,4 +324,30 @@ export function extractPage({ url, html, status = 200, headers = {}, sourceFile
323
324
  domNodeCount,
324
325
  maxDomDepth,
325
326
  };
327
+
328
+ // Keep representation identity explicit. These hashes deliberately do not
329
+ // share a generic `contentHash`: raw response bytes, extracted text,
330
+ // structured data, evaluator evidence, and the persisted page artifact are
331
+ // different claims and may change independently.
332
+ const hashes = createEvidenceHashes({
333
+ responseBody: responseBody ?? html,
334
+ renderedDom,
335
+ extractedText: text,
336
+ structuredData: jsonLd.length
337
+ ? jsonLd.map(({ parsed, blocks, parseError }) => ({ parsed, blocks, parseError }))
338
+ : null,
339
+ evidence: {
340
+ extracted_text: text,
341
+ headings,
342
+ paragraphs,
343
+ structured_data: jsonLd.map(({ parsed, blocks, parseError }) => ({ parsed, blocks, parseError })),
344
+ },
345
+ // This is replaced after URL/resource metadata is attached; keeping the
346
+ // field null here prevents a pre-persistence model from being presented as
347
+ // the persisted artifact identity.
348
+ artifact: null,
349
+ });
350
+ const withHashes = { ...page, ...hashes };
351
+ withHashes.artifact_hash = hashPageArtifact(withHashes);
352
+ return withHashes;
326
353
  }