@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
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { loadVerifiedRun } from '../shared/verifiedRunLoader.js';
3
4
  import crypto from 'node:crypto';
4
5
  import { readJson, writeJson, nowIso } from '../shared/io.js';
5
6
  import { validateAgainst } from '../shared/schemaValidator.js';
@@ -7,6 +8,7 @@ import { buildContext } from './context.js';
7
8
  import { runDetectors } from '../detectors/framework.js';
8
9
  import { selectDetectors } from '../detectors/index.js';
9
10
  import { remediateCommand } from './remediate.js';
11
+ import { assessReobservation, resourceIdentityFor, sameResourceIdentity } from './compareSnapshots.js';
10
12
 
11
13
  const PKG_VERSION = JSON.parse(
12
14
  fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
@@ -20,14 +22,49 @@ function sha256(content) {
20
22
  return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
21
23
  }
22
24
 
23
- function subjectKey(subject) {
24
- return subject?.identifier || subject?.url || JSON.stringify(subject) || 'unknown';
25
+ function findingIdentityKey(finding) {
26
+ const identity = resourceIdentityFor(finding);
27
+ const id = identity.resourceIds.size ? [...identity.resourceIds].sort().join(',')
28
+ : identity.effective || identity.requested || (identity.fallbackUrls.size === 1 ? [...identity.fallbackUrls][0] : JSON.stringify(finding?.subject || {}));
29
+ return `${finding?.detector_id || ''}|${id}`;
25
30
  }
26
31
 
27
32
  function newVerificationId() {
28
33
  return `VR-${nowIso().replace(/\D/g, '')}-${Math.random().toString(36).slice(2, 6)}`;
29
34
  }
30
35
 
36
+ function canonicalJson(value) {
37
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
38
+ if (value && typeof value === 'object') {
39
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
40
+ }
41
+ return JSON.stringify(value);
42
+ }
43
+
44
+ export function recheckComparability(manifest, sourceFinding, afterFinding, ctx, detector) {
45
+ const sourceKind = manifest?.target?.kind || null;
46
+ const recheckKind = ctx.site?.mode || null;
47
+ const sourceDetectorVersion = sourceFinding?.provenance?.detector_version ?? 1;
48
+ const recheckDetectorVersion = afterFinding?.provenance?.detector_version ?? detector?.version ?? 1;
49
+ const sourceViewport = sourceFinding?.provenance?.viewport ?? null;
50
+ const recheckViewport = ctx.viewport ?? null;
51
+ const sourceConfig = manifest?.configuration_hash || null;
52
+ const recheckConfig = sha256(JSON.stringify(ctx.config));
53
+ const dimensions = {
54
+ tool_changed: (manifest?.tool_version || null) !== PKG_VERSION,
55
+ evaluator_changed: sourceDetectorVersion !== recheckDetectorVersion,
56
+ observation_method_changed: sourceKind !== recheckKind
57
+ || canonicalJson(sourceViewport) !== canonicalJson(recheckViewport),
58
+ configuration_changed: sourceConfig !== recheckConfig,
59
+ };
60
+ return {
61
+ comparable: !Object.values(dimensions).some(Boolean),
62
+ dimensions,
63
+ source: { tool_version: manifest?.tool_version || null, evaluator_version: sourceDetectorVersion, target_kind: sourceKind, configuration_hash: sourceConfig },
64
+ recheck: { tool_version: PKG_VERSION, evaluator_version: recheckDetectorVersion, target_kind: recheckKind, configuration_hash: recheckConfig },
65
+ };
66
+ }
67
+
31
68
  /**
32
69
  * `citable verify remediation` — closed loop: source-run finding -> patch ->
33
70
  * detector re-run -> before/after evidence bundle.
@@ -37,7 +74,7 @@ function newVerificationId() {
37
74
  * never a silent `verified`.
38
75
  */
39
76
  export async function verifyRemediation(root, options = {}) {
40
- const { run: runId, finding, target, apply = false, subject = null } = options;
77
+ const { run: runId, finding, target, apply = false, subject = null, viewport = null } = options;
41
78
  const runDir = path.join(root, '.citable', 'runs', runId || '');
42
79
  const limitations = [
43
80
  RESOLUTION_DEFINITION,
@@ -55,7 +92,7 @@ export async function verifyRemediation(root, options = {}) {
55
92
  recheck_run_id: null,
56
93
  detector_id: finding ? String(finding).toUpperCase() : null,
57
94
  subject: null,
58
- verdict: { resolved: false, before_finding_ids: [], after_finding_ids: [], new_regressions: [], resolution_definition: RESOLUTION_DEFINITION },
95
+ verdict: { resolved: false, comparison_state: 'indeterminate', before_finding_ids: [], after_finding_ids: [], new_regressions: [], resolution_definition: RESOLUTION_DEFINITION },
59
96
  patch: null,
60
97
  provenance: { command: 'verify remediation', recheck_target: target || null, detectors_run: [], detectors_skipped: [], recheck_errors: [], recheck_warnings: [] },
61
98
  limitations,
@@ -73,23 +110,23 @@ export async function verifyRemediation(root, options = {}) {
73
110
  return finalizeResult(result);
74
111
  }
75
112
 
76
- const beforeFindings = readJson(path.join(runDir, 'findings.json'));
77
- const manifest = fs.existsSync(path.join(runDir, 'manifest.json'))
78
- ? readJson(path.join(runDir, 'manifest.json'))
79
- : null;
113
+ const source = loadVerifiedRun(runDir, { requireCompletedExecution: false, allowLegacy: true, requireCoverage: false });
114
+ const beforeFindings = source.findings;
115
+ const manifest = source.manifest;
80
116
  result.tool_version = manifest?.tool_version || PKG_VERSION;
81
117
  result.repository_commit = manifest?.repository_commit ?? null;
82
118
 
119
+ const requestedSubject = subject ? { subject: { identifier: subject } } : null;
83
120
  const matches = beforeFindings.filter((f) =>
84
121
  (f.detector_id || '').toUpperCase() === result.detector_id
85
- && (!subject || subjectKey(f.subject) === subject));
122
+ && (!requestedSubject || sameResourceIdentity(f, requestedSubject)));
86
123
  if (matches.length === 0) {
87
124
  result.provenance.recheck_errors.push(`finding ${result.detector_id} not present in source run ${runId}`);
88
125
  result.status = 'blocked';
89
126
  return finalizeResult(result);
90
127
  }
91
128
  result.verdict.before_finding_ids = matches.map((f) => f.finding_id);
92
- result.subject = { type: matches[0].subject?.type || 'unknown', identifier: subject || subjectKey(matches[0].subject) };
129
+ result.subject = { type: matches[0].subject?.type || 'unknown', identifier: subject || matches[0].subject?.identifier || matches[0].subject?.url || 'unknown' };
93
130
 
94
131
  // Optional gated patch application (production-safe pipeline from remediate.js)
95
132
  if (target && apply) {
@@ -133,7 +170,7 @@ export async function verifyRemediation(root, options = {}) {
133
170
  const namespace = result.detector_id.split('-')[0];
134
171
  let ctx;
135
172
  try {
136
- ctx = await buildContext(root, { target: recheckTarget, baseUrl: options.baseUrl, refDate: options.refDate });
173
+ ctx = await buildContext(root, { target: recheckTarget, baseUrl: options.baseUrl, refDate: options.refDate, viewport });
137
174
  } catch (err) {
138
175
  result.provenance.recheck_errors.push(`re-check context failed: ${err.message}`);
139
176
  result.limitations.push('required_input: a readable target directory or URL for the detector re-run.');
@@ -155,24 +192,62 @@ export async function verifyRemediation(root, options = {}) {
155
192
 
156
193
  const afterMatches = afterFindings.filter((f) =>
157
194
  (f.detector_id || '').toUpperCase() === result.detector_id
158
- && subjectKey(f.subject) === subjectKey(matches[0].subject));
195
+ && sameResourceIdentity(f, matches[0]));
159
196
  result.verdict.after_finding_ids = afterMatches.map((f) => f.finding_id);
160
197
 
161
- const beforeKeys = new Set(beforeFindings.map((f) => `${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`));
162
- const afterKeys = new Set(afterFindings.map((f) => `${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`));
198
+ const beforeKeys = new Set(beforeFindings.map((f) => `${findingIdentityKey(f)}|${f.observation?.summary}`));
199
+ const afterKeys = new Set(afterFindings.map((f) => `${findingIdentityKey(f)}|${f.observation?.summary}`));
163
200
  result.verdict.new_regressions = afterFindings
164
201
  .filter((f) => ['critical', 'high'].includes(f.classification?.severity))
165
- .filter((f) => !beforeKeys.has(`${f.detector_id}|${subjectKey(f.subject)}|${f.observation?.summary}`))
202
+ .filter((f) => !beforeKeys.has(`${findingIdentityKey(f)}|${f.observation?.summary}`))
166
203
  .map((f) => ({ finding_id: f.finding_id, detector_id: f.detector_id, severity: f.classification.severity, summary: f.observation?.summary }));
167
204
 
205
+ const detectorDefinition = selectDetectors({ namespaces: [namespace] })
206
+ .find((candidate) => candidate.id === result.detector_id);
207
+ const comparability = recheckComparability(manifest, matches[0], afterMatches[0], ctx, detectorDefinition);
208
+
209
+ // Finalize the same collection envelope used by audit so detector absence
210
+ // is only resolution when the responsible resource was actually observed.
211
+ let recheckCoverage = null;
212
+ if (ctx.site?.finalizeCoverage) {
213
+ try { recheckCoverage = ctx.site.finalizeCoverage({ evaluated: true }); }
214
+ catch (error) { result.provenance.recheck_errors.push(`coverage finalization failed: ${error.message}`); }
215
+ }
216
+ let reobservation = assessReobservation(matches[0], recheckCoverage);
217
+ // Directory re-checks predate the coverage ledger and intentionally do not
218
+ // manufacture a package artifact. Their concrete page model is still a
219
+ // sufficient page-local observation when the subject is present.
220
+ if (!recheckCoverage && ctx.site?.pages?.some((page) => sameResourceIdentity(matches[0], page))) {
221
+ reobservation = { state: 'resolved', reason: 'page_present_in_recheck_model', resource: null };
222
+ }
223
+ result.comparison = {
224
+ state: comparability.comparable ? reobservation.state : 'not_comparable',
225
+ reason: reobservation.reason,
226
+ coverage_status: recheckCoverage?.coverage_status || 'not_available',
227
+ resource: reobservation.resource?.normalized_url || null,
228
+ comparability,
229
+ };
230
+ result.verdict.comparison_state = comparability.comparable ? reobservation.state : 'not_comparable';
231
+
168
232
  const errorsDuringRecheck = result.provenance.recheck_errors.length > 0;
169
233
  if (errorsDuringRecheck) {
170
- result.status = 'blocked';
234
+ result.status = 'indeterminate';
235
+ result.verdict.resolved = false;
236
+ result.verdict.comparison_state = 'indeterminate';
237
+ result.limitations.push('Detector or collection errors occurred during the re-check; resolution cannot be established from a failed observation.');
238
+ } else if (!comparability.comparable) {
239
+ result.status = 'not_comparable';
171
240
  result.verdict.resolved = false;
172
- result.limitations.push('Detector errors occurred during the re-check; resolution cannot be established from a failed observation.');
241
+ result.limitations.push('The source and re-check evaluator/tool/method/configuration envelopes are not comparable; resolution is not established.');
173
242
  } else if (afterMatches.length === 0) {
174
- result.status = 'verified';
175
- result.verdict.resolved = true;
243
+ if (reobservation.state === 'resolved') {
244
+ result.status = 'verified';
245
+ result.verdict.resolved = true;
246
+ } else {
247
+ result.status = reobservation.state === 'indeterminate' ? 'indeterminate' : 'not_reobserved';
248
+ result.verdict.resolved = false;
249
+ result.limitations.push(`The source subject was not sufficiently reobserved: ${reobservation.reason}.`);
250
+ }
176
251
  } else {
177
252
  result.status = 'not_resolved';
178
253
  result.verdict.resolved = false;
@@ -0,0 +1,80 @@
1
+ import { validateAgainst } from '../shared/schemaValidator.js';
2
+
3
+ export const COLLECTION_SCHEMA_VERSION = 1;
4
+ export const COLLECTION_COVERAGE_STATUSES = new Set([
5
+ 'complete',
6
+ 'truncated',
7
+ 'indeterminate',
8
+ 'provider_bounded',
9
+ ]);
10
+
11
+ function nonNegativeInteger(value, name) {
12
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`${name} must be a non-negative integer`);
13
+ return value;
14
+ }
15
+
16
+ function stringList(value, name) {
17
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) throw new TypeError(`${name} must be an array of strings`);
18
+ return [...new Set(value)];
19
+ }
20
+
21
+ /**
22
+ * Build the provider-neutral collection envelope. `items` are the items
23
+ * actually observed, never a provider-reported total or an inferred census.
24
+ */
25
+ export function collectionResult({
26
+ items = [],
27
+ paginationState = {},
28
+ providerReportedTotal = null,
29
+ continuationState = null,
30
+ limitations = [],
31
+ errors = [],
32
+ providerCompleteness = 'declared',
33
+ } = {}) {
34
+ if (!Array.isArray(items)) throw new TypeError('items must be an array');
35
+ if (!paginationState || typeof paginationState !== 'object' || Array.isArray(paginationState)) throw new TypeError('paginationState must be an object');
36
+ if (providerReportedTotal !== null) nonNegativeInteger(providerReportedTotal, 'providerReportedTotal');
37
+ stringList(limitations, 'limitations');
38
+ stringList(errors, 'errors');
39
+ if (continuationState !== null && (typeof continuationState !== 'object' || Array.isArray(continuationState))) throw new TypeError('continuationState must be an object or null');
40
+ if (!['declared', 'unknown'].includes(providerCompleteness)) throw new TypeError('providerCompleteness must be declared or unknown');
41
+
42
+ const totalErrors = providerReportedTotal !== null && providerReportedTotal < items.length
43
+ ? [`provider reported total ${providerReportedTotal}, below retrieved item count ${items.length}`]
44
+ : [];
45
+ const cleanErrors = [...new Set([...errors, ...totalErrors])];
46
+ const contradictoryTotal = providerReportedTotal !== null && providerReportedTotal < items.length;
47
+ const unresolvedTotal = providerReportedTotal !== null && providerReportedTotal > items.length && !continuationState;
48
+ const status = (cleanErrors.length || contradictoryTotal)
49
+ ? 'indeterminate'
50
+ : continuationState
51
+ ? 'truncated'
52
+ : (unresolvedTotal || providerCompleteness === 'unknown')
53
+ ? 'provider_bounded'
54
+ : 'complete';
55
+ const result = {
56
+ schema_version: COLLECTION_SCHEMA_VERSION,
57
+ items,
58
+ pagination_state: paginationState,
59
+ provider_reported_total: providerReportedTotal,
60
+ retrieved_total: items.length,
61
+ coverage_status: status,
62
+ continuation_state: continuationState,
63
+ limitations: [...new Set(limitations)],
64
+ errors: cleanErrors,
65
+ };
66
+ const check = validateAgainst('collection-result.schema.json', result);
67
+ if (!check.valid) throw new TypeError(`invalid collection result: ${check.errors.join('; ')}`);
68
+ return result;
69
+ }
70
+
71
+ export function paginationBoundary(context = {}) {
72
+ const value = context.collectionMaxPages ?? context.maxPages ?? 1000;
73
+ if (!Number.isInteger(value) || value < 1 || value > 10000) throw new RangeError('collection max pages must be an integer from 1 to 10000');
74
+ return value;
75
+ }
76
+
77
+ export function errorMessage(error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ return message.replace(/Bearer\s+\S+/gi, 'Bearer [redacted]').slice(0, 300);
80
+ }
@@ -1,4 +1,5 @@
1
1
  import { providerRequest } from './http.js';
2
+ import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
2
3
 
3
4
  const DATA = 'https://analyticsdata.googleapis.com/v1beta';
4
5
  const ADMIN = 'https://analyticsadmin.googleapis.com/v1beta';
@@ -36,24 +37,58 @@ export const ga4Connector = {
36
37
  if (!dimensions.includes('landingPagePlusQueryString')) dimensions.push('landingPagePlusQueryString');
37
38
  const rows = [];
38
39
  const limitations = [];
40
+ const errors = [];
41
+ const items = [];
42
+ const maxPages = paginationBoundary(context);
43
+ let pagesRequested = 0;
44
+ let pagesRetrieved = 0;
45
+ let continuationState = null;
46
+ let providerTotal = null;
47
+ let connectorError = null;
39
48
  let offset = 0;
40
49
  do {
41
- const result = await providerRequest(`${DATA}/properties/${encodeURIComponent(connection.property_id)}:runReport`, {
42
- ...context, method: 'POST', body: {
43
- dateRanges: [{ startDate, endDate }], dimensions: dimensions.map((name) => ({ name })),
44
- metrics: metrics.map((metric) => ({ name: metric.external_name })), limit: '100000', offset: String(offset),
45
- dimensionFilter: { filter: { fieldName: 'sessionDefaultChannelGroup', stringFilter: { matchType: 'EXACT', value: 'Organic Search' } } },
46
- },
47
- });
48
- for (const row of result.rows || []) {
50
+ pagesRequested += 1;
51
+ let result;
52
+ try {
53
+ result = await providerRequest(`${DATA}/properties/${encodeURIComponent(connection.property_id)}:runReport`, {
54
+ ...context, method: 'POST', body: {
55
+ dateRanges: [{ startDate, endDate }], dimensions: dimensions.map((name) => ({ name })),
56
+ metrics: metrics.map((metric) => ({ name: metric.external_name })), limit: '100000', offset: String(offset),
57
+ dimensionFilter: { filter: { fieldName: 'sessionDefaultChannelGroup', stringFilter: { matchType: 'EXACT', value: 'Organic Search' } } },
58
+ },
59
+ });
60
+ } catch (error) {
61
+ connectorError ||= error;
62
+ errors.push(`offset ${offset}: ${errorMessage(error)}`);
63
+ continuationState = { offset };
64
+ break;
65
+ }
66
+ pagesRetrieved += 1;
67
+ const pageRows = result.rows || [];
68
+ items.push(...pageRows);
69
+ if (Number.isInteger(result.rowCount) && result.rowCount >= 0) providerTotal = result.rowCount;
70
+ for (const row of pageRows) {
49
71
  const values = Object.fromEntries(dimensions.map((name, index) => [name === 'landingPagePlusQueryString' ? 'url' : name, row.dimensionValues[index]?.value]));
50
72
  const date = values.date?.replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3');
51
73
  rows.push(...metrics.map((metric, index) => ({ metric, value: Number(row.metricValues[index]?.value), dimensions: values, observed_at: `${date}T00:00:00.000Z` })));
52
74
  }
53
75
  if (result.metadata?.dataLossFromOtherRow) limitations.push('Provider reports data loss from the (other) row.');
54
- offset += (result.rows || []).length;
55
- if (!result.rowCount || offset >= result.rowCount || !(result.rows || []).length) break;
76
+ offset += pageRows.length;
77
+ if ((result.rowCount === undefined || result.rowCount === null)
78
+ ? pageRows.length < 100000 || !pageRows.length
79
+ : offset >= result.rowCount || !pageRows.length) break;
80
+ if (pagesRequested >= maxPages) { continuationState = { offset }; break; }
56
81
  } while (true);
57
- return { rows, cursor: endDate, limitations: [...new Set([...limitations, 'GA4 reporting identity, thresholding, attribution, and consent configuration affect results.', 'Sync is filtered to Organic Search sessions.'])] };
82
+ const providerCompleteness = limitations.some((item) => /data loss/i.test(item)) || providerTotal === null ? 'unknown' : 'declared';
83
+ const collection = collectionResult({
84
+ items,
85
+ paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: maxPages } },
86
+ providerReportedTotal: providerTotal,
87
+ continuationState,
88
+ providerCompleteness,
89
+ limitations: [...new Set([...limitations, 'GA4 reporting identity, thresholding, attribution, and consent configuration affect results.', 'Sync is filtered to Organic Search sessions.'])],
90
+ errors,
91
+ });
92
+ return { rows, cursor: endDate, limitations: collection.limitations, collection, ...(connectorError ? { connectorError } : {}) };
58
93
  },
59
94
  };
@@ -1,4 +1,5 @@
1
1
  import { providerRequest } from './http.js';
2
+ import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
2
3
 
3
4
  const BASE = 'https://www.googleapis.com/webmasters/v3';
4
5
  const METRICS = {
@@ -25,17 +26,51 @@ export const gscConnector = {
25
26
  if (!dimensions.includes('date')) dimensions.unshift('date');
26
27
  const rows = [];
27
28
  let startRow = 0;
29
+ const items = [];
30
+ const errors = [];
31
+ const maxPages = paginationBoundary(context);
32
+ let pagesRequested = 0;
33
+ let pagesRetrieved = 0;
34
+ let continuationState = null;
35
+ let providerTotal = null;
36
+ let connectorError = null;
28
37
  do {
29
- const result = await providerRequest(`${BASE}/sites/${encodeURIComponent(connection.property_id)}/searchAnalytics/query`, {
30
- ...context, method: 'POST', body: { startDate, endDate, dimensions, rowLimit: 25000, startRow, dataState: 'final' },
31
- });
32
- for (const row of result.rows || []) {
38
+ pagesRequested += 1;
39
+ let result;
40
+ try {
41
+ result = await providerRequest(`${BASE}/sites/${encodeURIComponent(connection.property_id)}/searchAnalytics/query`, {
42
+ ...context, method: 'POST', body: { startDate, endDate, dimensions, rowLimit: 25000, startRow, dataState: 'final' },
43
+ });
44
+ } catch (error) {
45
+ connectorError ||= error;
46
+ errors.push(`start row ${startRow}: ${errorMessage(error)}`);
47
+ continuationState = { start_row: startRow };
48
+ break;
49
+ }
50
+ pagesRetrieved += 1;
51
+ const pageRows = result.rows || [];
52
+ items.push(...pageRows);
53
+ if (Number.isInteger(result.rowCount) && result.rowCount >= 0) providerTotal = result.rowCount;
54
+ for (const row of pageRows) {
33
55
  const values = Object.fromEntries(dimensions.map((name, index) => [name === 'page' ? 'url' : name, row.keys[index]]));
34
56
  for (const metric of metrics) rows.push({ metric, value: row[metric.external_name], dimensions: values, observed_at: `${values.date}T00:00:00.000Z` });
35
57
  }
36
- if ((result.rows || []).length < 25000) break;
58
+ if (pageRows.length < 25000) break;
37
59
  startRow += 25000;
60
+ if (pagesRequested >= maxPages) {
61
+ continuationState = { start_row: startRow };
62
+ break;
63
+ }
38
64
  } while (true);
39
- return { rows, cursor: endDate, limitations: ['Search Console privacy filtering and aggregation apply.', 'Search Analytics does not guarantee every data row; the API can return top rows only.', 'Final data can still be revised by the provider.'] };
65
+ const collection = collectionResult({
66
+ items,
67
+ paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: maxPages } },
68
+ providerReportedTotal: providerTotal,
69
+ continuationState,
70
+ providerCompleteness: 'unknown',
71
+ limitations: ['Search Console privacy filtering and aggregation apply.', 'Search Analytics does not guarantee every data row; the API can return top rows only.', 'Final data can still be revised by the provider.'],
72
+ errors,
73
+ });
74
+ return { rows, cursor: endDate, limitations: collection.limitations, collection, ...(connectorError ? { connectorError } : {}) };
40
75
  },
41
76
  };
@@ -4,6 +4,7 @@ import { wordpressConnector } from './wordpress.js';
4
4
  import { webflowConnector } from './webflow.js';
5
5
  import { indexnowConnector } from './indexnow.js';
6
6
  import { mcpConnector } from './mcp/pilot.js';
7
+ export { collectionResult, paginationBoundary, errorMessage } from './collectionResult.js';
7
8
 
8
9
  const CONNECTORS = new Map([gscConnector, ga4Connector, wordpressConnector, webflowConnector, indexnowConnector, mcpConnector].map((connector) => [connector.provider, connector]));
9
10
 
@@ -18,4 +19,3 @@ export function listConnectors() {
18
19
  }
19
20
 
20
21
  export { McpClient, McpStdioTransport, McpHttpTransport, ALLOWED_MCP_SERVERS, isServerAllowlisted, isToolAllowlisted, assertToolAllowed, collectMcpEvidence, mcpConnector, diagnoseMcpTransports } from './mcp/index.js';
21
-
@@ -1,7 +1,9 @@
1
1
  import { providerRequest } from './http.js';
2
2
  import { sha256, nowIso } from '../shared/io.js';
3
+ import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
3
4
 
4
5
  const BASE = 'https://api.webflow.com/v2';
6
+ const API_ORIGIN = new URL(BASE).origin;
5
7
 
6
8
  const METRICS = {
7
9
  pages: { unit: 'count', value_type: 'integer' },
@@ -24,6 +26,96 @@ function computeWebflowPayloadHash(payload) {
24
26
  return sha256(JSON.stringify(canonical));
25
27
  }
26
28
 
29
+ function safeContinuation(url, { preserveOrigin = true } = {}) {
30
+ try {
31
+ const parsed = new URL(url);
32
+ return { ...(preserveOrigin ? { url: `${parsed.origin}${parsed.pathname}` } : { path: parsed.pathname }), query_redacted: parsed.search.length > 0 };
33
+ } catch {
34
+ return { url: '[invalid continuation]', query_redacted: true };
35
+ }
36
+ }
37
+
38
+ async function collectWebflowList(url, field, context) {
39
+ const maxPages = paginationBoundary(context);
40
+ const items = [];
41
+ const errors = [];
42
+ const limitations = [];
43
+ let providerTotal = null;
44
+ let pagesRequested = 0;
45
+ let pagesRetrieved = 0;
46
+ let offset = 0;
47
+ let cursor = null;
48
+ let nextUrl = null;
49
+ let continuationState = null;
50
+ let providerCompleteness = 'declared';
51
+ let connectorError = null;
52
+ while (pagesRequested < maxPages) {
53
+ pagesRequested += 1;
54
+ const query = cursor
55
+ ? `?limit=100&cursor=${encodeURIComponent(cursor)}`
56
+ : `?limit=100${offset ? `&offset=${offset}` : ''}`;
57
+ let result;
58
+ try {
59
+ if (nextUrl) {
60
+ let continuation;
61
+ try { continuation = new URL(nextUrl); } catch {
62
+ errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
63
+ continuationState = { next: safeContinuation(nextUrl), reason: 'invalid_url' };
64
+ break;
65
+ }
66
+ if (continuation.origin !== API_ORIGIN || continuation.protocol !== 'https:') {
67
+ errors.push(`${field} page ${pagesRequested}: continuation origin is outside the Webflow API`);
68
+ continuationState = { next: safeContinuation(nextUrl, { preserveOrigin: false }), reason: 'origin_mismatch' };
69
+ break;
70
+ }
71
+ }
72
+ result = await providerRequest(nextUrl || `${url}${query}`, context);
73
+ const pageItems = Array.isArray(result?.[field]) ? result[field] : [];
74
+ pagesRetrieved += 1;
75
+ items.push(...pageItems);
76
+ const pagination = result.pagination || {};
77
+ const reported = result.total ?? pagination.total ?? pagination.totalCount ?? null;
78
+ if (Number.isInteger(reported) && reported >= 0) providerTotal = reported;
79
+ const next = result.nextCursor ?? pagination.nextCursor ?? result.next?.cursor ?? null;
80
+ const providerNextUrl = typeof result.next === 'string' ? result.next : (typeof pagination.next === 'string' ? pagination.next : null);
81
+ const nextOffset = result.nextOffset ?? pagination.nextOffset ?? null;
82
+ nextUrl = null;
83
+ if (next !== null && (typeof next !== 'string' || !next.trim())) {
84
+ errors.push(`${field} page ${pagesRequested}: malformed continuation cursor`);
85
+ continuationState = { cursor: next };
86
+ break;
87
+ }
88
+ if (providerNextUrl !== null && (!/^https:\/\//i.test(providerNextUrl))) {
89
+ errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
90
+ continuationState = { next: safeContinuation(providerNextUrl, { preserveOrigin: false }) };
91
+ break;
92
+ }
93
+ if (next) cursor = String(next);
94
+ else if (providerNextUrl) { nextUrl = providerNextUrl; cursor = null; }
95
+ else if (nextOffset !== null && (!Number.isInteger(nextOffset) || nextOffset <= offset)) {
96
+ errors.push(`${field} page ${pagesRequested}: malformed continuation offset`);
97
+ continuationState = { offset: nextOffset };
98
+ break;
99
+ }
100
+ else if (providerTotal !== null && items.length >= providerTotal) break;
101
+ else if (Number.isInteger(nextOffset) && nextOffset >= 0) { offset = nextOffset; cursor = null; }
102
+ else if (pageItems.length >= 100 && (providerTotal === null || items.length < providerTotal)) offset += pageItems.length;
103
+ else { providerCompleteness = 'unknown'; break; }
104
+ if (providerTotal !== null && items.length >= providerTotal) break;
105
+ } catch (error) {
106
+ connectorError ||= error;
107
+ errors.push(`${field} page ${pagesRequested}: ${errorMessage(error)}`);
108
+ continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset };
109
+ break;
110
+ }
111
+ }
112
+ if (!continuationState && pagesRequested >= maxPages && (providerTotal === null || items.length < providerTotal)) {
113
+ continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset: offset + 100 };
114
+ limitations.push(`Webflow ${field} collection reached the ${maxPages}-page boundary.`);
115
+ }
116
+ return { items, providerTotal, providerCompleteness, connectorError, pagesRequested, pagesRetrieved, continuationState, limitations, errors };
117
+ }
118
+
27
119
  export const webflowConnector = {
28
120
  provider: 'webflow',
29
121
  defaultCredentialEnv: 'WEBFLOW_API_TOKEN',
@@ -62,10 +154,26 @@ export const webflowConnector = {
62
154
  const siteId = connection.property_id;
63
155
  const rows = [];
64
156
  const metricNames = new Set(metrics.map((m) => m.external_name));
157
+ const allItems = [];
158
+ const errors = [];
159
+ const limitations = [];
160
+ let pagesRequested = 0;
161
+ let pagesRetrieved = 0;
162
+ let continuationState = null;
163
+ let providerTotal = 0;
164
+ let providerTotalKnown = true;
165
+ let connectorError = null;
65
166
 
66
167
  if (metricNames.has('pages')) {
67
- const pageResult = await providerRequest(`${BASE}/sites/${encodeURIComponent(siteId)}/pages`, context);
68
- const pages = pageResult.pages || [];
168
+ const pageResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/pages`, 'pages', context);
169
+ const pages = pageResult.items;
170
+ allItems.push(...pages.map((item) => ({ ...item, _collection_type: 'pages' })));
171
+ errors.push(...pageResult.errors); limitations.push(...pageResult.limitations);
172
+ pagesRequested += pageResult.pagesRequested; pagesRetrieved += pageResult.pagesRetrieved;
173
+ continuationState ||= pageResult.continuationState;
174
+ connectorError ||= pageResult.connectorError;
175
+ if (pageResult.providerCompleteness === 'unknown') providerTotalKnown = false;
176
+ if (pageResult.providerTotal === null) providerTotalKnown = false; else providerTotal += pageResult.providerTotal;
69
177
  const pageMetric = metrics.find((m) => m.external_name === 'pages');
70
178
  rows.push({
71
179
  metric: pageMetric,
@@ -76,8 +184,15 @@ export const webflowConnector = {
76
184
  }
77
185
 
78
186
  if (metricNames.has('collections') || metricNames.has('items')) {
79
- const collectionsResult = await providerRequest(`${BASE}/sites/${encodeURIComponent(siteId)}/collections`, context);
80
- const collections = collectionsResult.collections || [];
187
+ const collectionsResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/collections`, 'collections', context);
188
+ const collections = collectionsResult.items;
189
+ allItems.push(...collections.map((item) => ({ ...item, _collection_type: 'collections' })));
190
+ errors.push(...collectionsResult.errors); limitations.push(...collectionsResult.limitations);
191
+ pagesRequested += collectionsResult.pagesRequested; pagesRetrieved += collectionsResult.pagesRetrieved;
192
+ continuationState ||= collectionsResult.continuationState;
193
+ connectorError ||= collectionsResult.connectorError;
194
+ if (collectionsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
195
+ if (collectionsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += collectionsResult.providerTotal;
81
196
 
82
197
  if (metricNames.has('collections')) {
83
198
  const colMetric = metrics.find((m) => m.external_name === 'collections');
@@ -92,8 +207,15 @@ export const webflowConnector = {
92
207
  if (metricNames.has('items')) {
93
208
  let totalItems = 0;
94
209
  for (const col of collections) {
95
- const itemsResult = await providerRequest(`${BASE}/collections/${encodeURIComponent(col.id)}/items?limit=100`, context);
96
- totalItems += (itemsResult.items || []).length;
210
+ const itemsResult = await collectWebflowList(`${BASE}/collections/${encodeURIComponent(col.id)}/items`, 'items', context);
211
+ totalItems += itemsResult.items.length;
212
+ allItems.push(...itemsResult.items.map((item) => ({ ...item, _collection_type: 'items', _collection_id: col.id })));
213
+ errors.push(...itemsResult.errors); limitations.push(...itemsResult.limitations);
214
+ pagesRequested += itemsResult.pagesRequested; pagesRetrieved += itemsResult.pagesRetrieved;
215
+ continuationState ||= itemsResult.continuationState;
216
+ connectorError ||= itemsResult.connectorError;
217
+ if (itemsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
218
+ if (itemsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += itemsResult.providerTotal;
97
219
  }
98
220
  const itemMetric = metrics.find((m) => m.external_name === 'items');
99
221
  rows.push({
@@ -105,13 +227,21 @@ export const webflowConnector = {
105
227
  }
106
228
  }
107
229
 
230
+ const collection = collectionResult({
231
+ items: allItems,
232
+ paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: paginationBoundary(context) } },
233
+ providerReportedTotal: providerTotalKnown ? providerTotal : null,
234
+ continuationState,
235
+ providerCompleteness: providerTotalKnown ? 'declared' : 'unknown',
236
+ limitations: [...new Set([...limitations, 'Webflow API enforces rate limits of 60 requests per minute.', 'Staged CMS changes require site publish to appear on live domains.'])],
237
+ errors,
238
+ });
108
239
  return {
109
240
  rows,
110
241
  cursor: endDate,
111
- limitations: [
112
- 'Webflow API enforces rate limits of 60 requests per minute.',
113
- 'Staged CMS changes require site publish to appear on live domains.',
114
- ],
242
+ limitations: collection.limitations,
243
+ collection,
244
+ ...(connectorError ? { connectorError } : {}),
115
245
  };
116
246
  },
117
247