@nebulacomponents/citable 1.19.0 → 1.21.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 (360) hide show
  1. package/CHANGELOG.md +35 -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 +24 -12
  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/claim-evidence.schema.json +21 -0
  9. package/dist/universal/.agents/skills/citable/schemas/claim-verification.schema.json +18 -0
  10. package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
  11. package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
  12. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
  13. package/dist/universal/.agents/skills/citable/schemas/derivation.schema.json +18 -0
  14. package/dist/universal/.agents/skills/citable/schemas/external-comparison.schema.json +16 -0
  15. package/dist/universal/.agents/skills/citable/schemas/external-observation.schema.json +32 -0
  16. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
  17. package/dist/universal/.agents/skills/citable/schemas/prompt-set.schema.json +47 -0
  18. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
  19. package/dist/universal/.agents/skills/citable/schemas/representation-data.schema.json +21 -0
  20. package/dist/universal/.agents/skills/citable/schemas/research-dataset.schema.json +27 -0
  21. package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
  22. package/dist/universal/.agents/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  23. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
  24. package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
  25. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  26. package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
  27. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  28. package/dist/universal/.claude/skills/citable/manifest.json +24 -12
  29. package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
  30. package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
  31. package/dist/universal/.claude/skills/citable/schemas/claim-evidence.schema.json +21 -0
  32. package/dist/universal/.claude/skills/citable/schemas/claim-verification.schema.json +18 -0
  33. package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
  34. package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
  35. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
  36. package/dist/universal/.claude/skills/citable/schemas/derivation.schema.json +18 -0
  37. package/dist/universal/.claude/skills/citable/schemas/external-comparison.schema.json +16 -0
  38. package/dist/universal/.claude/skills/citable/schemas/external-observation.schema.json +32 -0
  39. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
  40. package/dist/universal/.claude/skills/citable/schemas/prompt-set.schema.json +47 -0
  41. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
  42. package/dist/universal/.claude/skills/citable/schemas/representation-data.schema.json +21 -0
  43. package/dist/universal/.claude/skills/citable/schemas/research-dataset.schema.json +27 -0
  44. package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
  45. package/dist/universal/.claude/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  46. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
  47. package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
  48. package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
  49. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  50. package/dist/universal/.cursor/skills/citable/manifest.json +24 -12
  51. package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
  52. package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
  53. package/dist/universal/.cursor/skills/citable/schemas/claim-evidence.schema.json +21 -0
  54. package/dist/universal/.cursor/skills/citable/schemas/claim-verification.schema.json +18 -0
  55. package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
  56. package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
  57. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
  58. package/dist/universal/.cursor/skills/citable/schemas/derivation.schema.json +18 -0
  59. package/dist/universal/.cursor/skills/citable/schemas/external-comparison.schema.json +16 -0
  60. package/dist/universal/.cursor/skills/citable/schemas/external-observation.schema.json +32 -0
  61. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
  62. package/dist/universal/.cursor/skills/citable/schemas/prompt-set.schema.json +47 -0
  63. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
  64. package/dist/universal/.cursor/skills/citable/schemas/representation-data.schema.json +21 -0
  65. package/dist/universal/.cursor/skills/citable/schemas/research-dataset.schema.json +27 -0
  66. package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
  67. package/dist/universal/.cursor/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  68. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
  69. package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
  70. package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
  71. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  72. package/dist/universal/.gemini/skills/citable/manifest.json +24 -12
  73. package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
  74. package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
  75. package/dist/universal/.gemini/skills/citable/schemas/claim-evidence.schema.json +21 -0
  76. package/dist/universal/.gemini/skills/citable/schemas/claim-verification.schema.json +18 -0
  77. package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
  78. package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
  79. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
  80. package/dist/universal/.gemini/skills/citable/schemas/derivation.schema.json +18 -0
  81. package/dist/universal/.gemini/skills/citable/schemas/external-comparison.schema.json +16 -0
  82. package/dist/universal/.gemini/skills/citable/schemas/external-observation.schema.json +32 -0
  83. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
  84. package/dist/universal/.gemini/skills/citable/schemas/prompt-set.schema.json +47 -0
  85. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
  86. package/dist/universal/.gemini/skills/citable/schemas/representation-data.schema.json +21 -0
  87. package/dist/universal/.gemini/skills/citable/schemas/research-dataset.schema.json +27 -0
  88. package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
  89. package/dist/universal/.gemini/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  90. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
  91. package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
  92. package/dist/universal/.github/skills/citable/SKILL.md +11 -2
  93. package/dist/universal/.github/skills/citable/VERSION +1 -1
  94. package/dist/universal/.github/skills/citable/manifest.json +24 -12
  95. package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
  96. package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
  97. package/dist/universal/.github/skills/citable/schemas/claim-evidence.schema.json +21 -0
  98. package/dist/universal/.github/skills/citable/schemas/claim-verification.schema.json +18 -0
  99. package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
  100. package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
  101. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
  102. package/dist/universal/.github/skills/citable/schemas/derivation.schema.json +18 -0
  103. package/dist/universal/.github/skills/citable/schemas/external-comparison.schema.json +16 -0
  104. package/dist/universal/.github/skills/citable/schemas/external-observation.schema.json +32 -0
  105. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
  106. package/dist/universal/.github/skills/citable/schemas/prompt-set.schema.json +47 -0
  107. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
  108. package/dist/universal/.github/skills/citable/schemas/representation-data.schema.json +21 -0
  109. package/dist/universal/.github/skills/citable/schemas/research-dataset.schema.json +27 -0
  110. package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
  111. package/dist/universal/.github/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  112. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
  113. package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
  114. package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
  115. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  116. package/dist/universal/.kiro/skills/citable/manifest.json +24 -12
  117. package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
  118. package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
  119. package/dist/universal/.kiro/skills/citable/schemas/claim-evidence.schema.json +21 -0
  120. package/dist/universal/.kiro/skills/citable/schemas/claim-verification.schema.json +18 -0
  121. package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
  122. package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
  123. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
  124. package/dist/universal/.kiro/skills/citable/schemas/derivation.schema.json +18 -0
  125. package/dist/universal/.kiro/skills/citable/schemas/external-comparison.schema.json +16 -0
  126. package/dist/universal/.kiro/skills/citable/schemas/external-observation.schema.json +32 -0
  127. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
  128. package/dist/universal/.kiro/skills/citable/schemas/prompt-set.schema.json +47 -0
  129. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
  130. package/dist/universal/.kiro/skills/citable/schemas/representation-data.schema.json +21 -0
  131. package/dist/universal/.kiro/skills/citable/schemas/research-dataset.schema.json +27 -0
  132. package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
  133. package/dist/universal/.kiro/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  134. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
  135. package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
  136. package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
  137. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  138. package/dist/universal/.opencode/skills/citable/manifest.json +24 -12
  139. package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
  140. package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
  141. package/dist/universal/.opencode/skills/citable/schemas/claim-evidence.schema.json +21 -0
  142. package/dist/universal/.opencode/skills/citable/schemas/claim-verification.schema.json +18 -0
  143. package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
  144. package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
  145. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
  146. package/dist/universal/.opencode/skills/citable/schemas/derivation.schema.json +18 -0
  147. package/dist/universal/.opencode/skills/citable/schemas/external-comparison.schema.json +16 -0
  148. package/dist/universal/.opencode/skills/citable/schemas/external-observation.schema.json +32 -0
  149. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
  150. package/dist/universal/.opencode/skills/citable/schemas/prompt-set.schema.json +47 -0
  151. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
  152. package/dist/universal/.opencode/skills/citable/schemas/representation-data.schema.json +21 -0
  153. package/dist/universal/.opencode/skills/citable/schemas/research-dataset.schema.json +27 -0
  154. package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
  155. package/dist/universal/.opencode/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  156. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
  157. package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
  158. package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
  159. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  160. package/dist/universal/.pi/agent/skills/citable/manifest.json +24 -12
  161. package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
  162. package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
  163. package/dist/universal/.pi/agent/skills/citable/schemas/claim-evidence.schema.json +21 -0
  164. package/dist/universal/.pi/agent/skills/citable/schemas/claim-verification.schema.json +18 -0
  165. package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
  166. package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
  167. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
  168. package/dist/universal/.pi/agent/skills/citable/schemas/derivation.schema.json +18 -0
  169. package/dist/universal/.pi/agent/skills/citable/schemas/external-comparison.schema.json +16 -0
  170. package/dist/universal/.pi/agent/skills/citable/schemas/external-observation.schema.json +32 -0
  171. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
  172. package/dist/universal/.pi/agent/skills/citable/schemas/prompt-set.schema.json +47 -0
  173. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
  174. package/dist/universal/.pi/agent/skills/citable/schemas/representation-data.schema.json +21 -0
  175. package/dist/universal/.pi/agent/skills/citable/schemas/research-dataset.schema.json +27 -0
  176. package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
  177. package/dist/universal/.pi/agent/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  178. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
  179. package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
  180. package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
  181. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  182. package/dist/universal/.qoder/skills/citable/manifest.json +24 -12
  183. package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
  184. package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
  185. package/dist/universal/.qoder/skills/citable/schemas/claim-evidence.schema.json +21 -0
  186. package/dist/universal/.qoder/skills/citable/schemas/claim-verification.schema.json +18 -0
  187. package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
  188. package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
  189. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
  190. package/dist/universal/.qoder/skills/citable/schemas/derivation.schema.json +18 -0
  191. package/dist/universal/.qoder/skills/citable/schemas/external-comparison.schema.json +16 -0
  192. package/dist/universal/.qoder/skills/citable/schemas/external-observation.schema.json +32 -0
  193. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
  194. package/dist/universal/.qoder/skills/citable/schemas/prompt-set.schema.json +47 -0
  195. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
  196. package/dist/universal/.qoder/skills/citable/schemas/representation-data.schema.json +21 -0
  197. package/dist/universal/.qoder/skills/citable/schemas/research-dataset.schema.json +27 -0
  198. package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
  199. package/dist/universal/.qoder/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  200. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
  201. package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
  202. package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
  203. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  204. package/dist/universal/.rovodev/skills/citable/manifest.json +24 -12
  205. package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
  206. package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
  207. package/dist/universal/.rovodev/skills/citable/schemas/claim-evidence.schema.json +21 -0
  208. package/dist/universal/.rovodev/skills/citable/schemas/claim-verification.schema.json +18 -0
  209. package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
  210. package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
  211. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
  212. package/dist/universal/.rovodev/skills/citable/schemas/derivation.schema.json +18 -0
  213. package/dist/universal/.rovodev/skills/citable/schemas/external-comparison.schema.json +16 -0
  214. package/dist/universal/.rovodev/skills/citable/schemas/external-observation.schema.json +32 -0
  215. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
  216. package/dist/universal/.rovodev/skills/citable/schemas/prompt-set.schema.json +47 -0
  217. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
  218. package/dist/universal/.rovodev/skills/citable/schemas/representation-data.schema.json +21 -0
  219. package/dist/universal/.rovodev/skills/citable/schemas/research-dataset.schema.json +27 -0
  220. package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
  221. package/dist/universal/.rovodev/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  222. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
  223. package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
  224. package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
  225. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  226. package/dist/universal/.trae/skills/citable/manifest.json +24 -12
  227. package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
  228. package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
  229. package/dist/universal/.trae/skills/citable/schemas/claim-evidence.schema.json +21 -0
  230. package/dist/universal/.trae/skills/citable/schemas/claim-verification.schema.json +18 -0
  231. package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
  232. package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
  233. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
  234. package/dist/universal/.trae/skills/citable/schemas/derivation.schema.json +18 -0
  235. package/dist/universal/.trae/skills/citable/schemas/external-comparison.schema.json +16 -0
  236. package/dist/universal/.trae/skills/citable/schemas/external-observation.schema.json +32 -0
  237. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
  238. package/dist/universal/.trae/skills/citable/schemas/prompt-set.schema.json +47 -0
  239. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
  240. package/dist/universal/.trae/skills/citable/schemas/representation-data.schema.json +21 -0
  241. package/dist/universal/.trae/skills/citable/schemas/research-dataset.schema.json +27 -0
  242. package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
  243. package/dist/universal/.trae/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  244. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
  245. package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
  246. package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
  247. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  248. package/dist/universal/.trae-cn/skills/citable/manifest.json +24 -12
  249. package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
  250. package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
  251. package/dist/universal/.trae-cn/skills/citable/schemas/claim-evidence.schema.json +21 -0
  252. package/dist/universal/.trae-cn/skills/citable/schemas/claim-verification.schema.json +18 -0
  253. package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
  254. package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
  255. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
  256. package/dist/universal/.trae-cn/skills/citable/schemas/derivation.schema.json +18 -0
  257. package/dist/universal/.trae-cn/skills/citable/schemas/external-comparison.schema.json +16 -0
  258. package/dist/universal/.trae-cn/skills/citable/schemas/external-observation.schema.json +32 -0
  259. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
  260. package/dist/universal/.trae-cn/skills/citable/schemas/prompt-set.schema.json +47 -0
  261. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
  262. package/dist/universal/.trae-cn/skills/citable/schemas/representation-data.schema.json +21 -0
  263. package/dist/universal/.trae-cn/skills/citable/schemas/research-dataset.schema.json +27 -0
  264. package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
  265. package/dist/universal/.trae-cn/skills/citable/schemas/search-discovery-data.schema.json +20 -0
  266. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
  267. package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
  268. package/dist/universal/manifest.json +292 -148
  269. package/package.json +1 -1
  270. package/schemas/action-plan.schema.json +38 -1
  271. package/schemas/audit-coverage.schema.json +118 -0
  272. package/schemas/claim-evidence.schema.json +21 -0
  273. package/schemas/claim-verification.schema.json +18 -0
  274. package/schemas/collection-result.schema.json +35 -0
  275. package/schemas/config.schema.json +2 -0
  276. package/schemas/cro-report.schema.json +8 -1
  277. package/schemas/derivation.schema.json +18 -0
  278. package/schemas/external-comparison.schema.json +16 -0
  279. package/schemas/external-observation.schema.json +32 -0
  280. package/schemas/finding.schema.json +105 -2
  281. package/schemas/prompt-set.schema.json +47 -0
  282. package/schemas/remediation-verification.schema.json +12 -1
  283. package/schemas/representation-data.schema.json +21 -0
  284. package/schemas/research-dataset.schema.json +27 -0
  285. package/schemas/run.schema.json +15 -2
  286. package/schemas/search-discovery-data.schema.json +20 -0
  287. package/schemas/search-report.schema.json +8 -1
  288. package/schemas/summary.schema.json +41 -0
  289. package/skill/SKILL.md +11 -2
  290. package/src/acceptance/reproducibility.js +5 -25
  291. package/src/analysis/croRoadmap.js +3 -3
  292. package/src/analysis/strategicRoadmap.js +3 -3
  293. package/src/artifacts/interchange.js +2 -6
  294. package/src/cli/index.js +15 -3
  295. package/src/commands/actionPlan.js +46 -5
  296. package/src/commands/applyRemediation.js +5 -5
  297. package/src/commands/audit.js +75 -8
  298. package/src/commands/ciWorkflow.js +7 -2
  299. package/src/commands/compareSnapshots.js +155 -47
  300. package/src/commands/connect.js +24 -4
  301. package/src/commands/context.js +41 -2
  302. package/src/commands/croSuite.js +51 -13
  303. package/src/commands/delivery.js +7 -7
  304. package/src/commands/governance.js +9 -10
  305. package/src/commands/implementationKit.js +2 -1
  306. package/src/commands/init.js +8 -1
  307. package/src/commands/previewCro.js +1 -1
  308. package/src/commands/prioritize.js +3 -2
  309. package/src/commands/reviews.js +31 -5
  310. package/src/commands/roadmapCmd.js +7 -13
  311. package/src/commands/sweep.js +15 -2
  312. package/src/commands/verifyRemediation.js +94 -19
  313. package/src/connectors/collectionResult.js +80 -0
  314. package/src/connectors/ga4.js +46 -11
  315. package/src/connectors/gsc.js +45 -6
  316. package/src/connectors/index.js +1 -1
  317. package/src/connectors/webflow.js +140 -10
  318. package/src/connectors/wordpress.js +92 -8
  319. package/src/crawler/fetch.js +220 -24
  320. package/src/crawler/resourceValidity.js +119 -0
  321. package/src/crawler/sitemap.js +435 -16
  322. package/src/crawler/sitemapCollector.js +364 -0
  323. package/src/crawler/urlIdentity.js +97 -0
  324. package/src/detectors/agent.js +11 -0
  325. package/src/detectors/ans.js +16 -1
  326. package/src/detectors/arch.js +6 -0
  327. package/src/detectors/claim.js +9 -0
  328. package/src/detectors/crawl.js +7 -0
  329. package/src/detectors/cro.js +21 -0
  330. package/src/detectors/cwv.js +4 -0
  331. package/src/detectors/entity.js +7 -0
  332. package/src/detectors/evd.js +9 -0
  333. package/src/detectors/framework.js +28 -2
  334. package/src/detectors/geoReco.js +16 -0
  335. package/src/detectors/hreflang.js +3 -0
  336. package/src/detectors/lifeMeas.js +9 -0
  337. package/src/detectors/link.js +6 -1
  338. package/src/detectors/page.js +11 -0
  339. package/src/detectors/schemaData.js +16 -1
  340. package/src/detectors/tech.js +34 -3
  341. package/src/discovery/searchConsole.js +186 -0
  342. package/src/evidence/coverage.js +448 -0
  343. package/src/evidence/determination.js +157 -0
  344. package/src/evidence/downstream.js +110 -0
  345. package/src/evidence/external.js +251 -0
  346. package/src/evidence/hashes.js +109 -0
  347. package/src/evidence/run.js +68 -8
  348. package/src/extractor/page.js +29 -2
  349. package/src/extractor/site.js +298 -50
  350. package/src/reporting/executiveCroReport.js +36 -13
  351. package/src/reporting/executiveExport.js +45 -16
  352. package/src/reporting/executiveSearchReport.js +56 -22
  353. package/src/reporting/report.js +56 -9
  354. package/src/representation/prompts.js +117 -0
  355. package/src/representation/representation.js +186 -0
  356. package/src/shared/epistemicLanguage.js +86 -0
  357. package/src/shared/evidenceSourceResolver.js +48 -17
  358. package/src/shared/htmlEscape.js +8 -4
  359. package/src/shared/runPackageVerifier.js +99 -8
  360. package/src/shared/verifiedRunLoader.js +129 -0
@@ -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
 
@@ -1,5 +1,6 @@
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 METRICS = {
5
6
  published_posts: { unit: 'count', value_type: 'integer' },
@@ -25,6 +26,53 @@ function computeCmsPayloadHash(payload) {
25
26
  return sha256(JSON.stringify(canonical));
26
27
  }
27
28
 
29
+ async function collectWordPressEndpoint(base, endpoint, context) {
30
+ const maxPages = paginationBoundary(context);
31
+ const items = [];
32
+ const limitations = [];
33
+ const errors = [];
34
+ let providerTotal = null;
35
+ let totalPages = null;
36
+ let page = 1;
37
+ let pagesRequested = 0;
38
+ let pagesRetrieved = 0;
39
+ let continuationState = null;
40
+ let providerCompleteness = 'declared';
41
+ let connectorError = null;
42
+
43
+ while (page <= maxPages) {
44
+ pagesRequested += 1;
45
+ const suffix = page === 1 ? '?per_page=100&status=publish' : `?per_page=100&status=publish&page=${page}`;
46
+ try {
47
+ const response = await providerRequest(`${base}/wp-json/wp/v2/${endpoint}${suffix}`, { ...context, rawResponse: true });
48
+ const pageItems = await response.json();
49
+ if (!Array.isArray(pageItems)) throw new Error(`WordPress ${endpoint} page ${page} did not return an array`);
50
+ pagesRetrieved += 1;
51
+ items.push(...pageItems.map((item) => ({ ...item, _collection_endpoint: endpoint })));
52
+ const headerTotal = Number(response.headers.get('x-wp-total'));
53
+ const headerPages = Number(response.headers.get('x-wp-totalpages'));
54
+ if (Number.isInteger(headerTotal) && headerTotal >= 0) providerTotal = headerTotal;
55
+ if (Number.isInteger(headerPages) && headerPages >= 1) totalPages = headerPages;
56
+ if (!totalPages) providerCompleteness = 'unknown';
57
+ if (pageItems.length === 0 || (totalPages && page >= totalPages)) break;
58
+ if (!totalPages && pageItems.length >= 100) {
59
+ continuationState = { endpoint, page: page + 1 };
60
+ limitations.push('WordPress did not report total pages for a full page of results.');
61
+ break;
62
+ }
63
+ page += 1;
64
+ } catch (error) {
65
+ connectorError ||= error;
66
+ errors.push(`${endpoint} page ${page}: ${errorMessage(error)}`);
67
+ continuationState = { endpoint, page };
68
+ break;
69
+ }
70
+ }
71
+ if (!continuationState && totalPages && page < totalPages) continuationState = { endpoint, page };
72
+ if (continuationState && page > maxPages) limitations.push(`WordPress ${endpoint} collection reached the ${maxPages}-page boundary.`);
73
+ return { items, providerTotal, providerCompleteness, connectorError, pagesRequested, pagesRetrieved, continuationState, limitations, errors };
74
+ }
75
+
28
76
  export const wordpressConnector = {
29
77
  provider: 'wordpress',
30
78
  defaultCredentialEnv: 'WORDPRESS_AUTH_TOKEN',
@@ -78,35 +126,71 @@ export const wordpressConnector = {
78
126
  const rows = [];
79
127
  const metricNames = new Set(metrics.map((m) => m.external_name));
80
128
 
129
+ const collections = [];
130
+ const collectionErrors = [];
131
+ const collectionLimitations = [];
132
+ let pagesRequested = 0;
133
+ let pagesRetrieved = 0;
134
+ let continuationState = null;
135
+ let providerTotal = 0;
136
+ let providerTotalKnown = true;
137
+ let connectorError = null;
138
+
81
139
  if (metricNames.has('published_posts')) {
82
- const posts = await providerRequest(`${base}/wp-json/wp/v2/posts?per_page=100&status=publish`, context);
140
+ const postsResult = await collectWordPressEndpoint(base, 'posts', context);
141
+ collections.push(...postsResult.items);
142
+ collectionErrors.push(...postsResult.errors);
143
+ collectionLimitations.push(...postsResult.limitations);
144
+ pagesRequested += postsResult.pagesRequested;
145
+ pagesRetrieved += postsResult.pagesRetrieved;
146
+ continuationState ||= postsResult.continuationState;
147
+ connectorError ||= postsResult.connectorError;
148
+ if (postsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
149
+ if (postsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += postsResult.providerTotal;
83
150
  const postMetric = metrics.find((m) => m.external_name === 'published_posts');
84
151
  rows.push({
85
152
  metric: postMetric,
86
- value: Array.isArray(posts) ? posts.length : 0,
153
+ value: postsResult.items.length,
87
154
  dimensions: { date: endDate },
88
155
  observed_at: `${endDate}T00:00:00.000Z`,
89
156
  });
90
157
  }
91
158
 
92
159
  if (metricNames.has('published_pages')) {
93
- const pages = await providerRequest(`${base}/wp-json/wp/v2/pages?per_page=100&status=publish`, context);
160
+ const pagesResult = await collectWordPressEndpoint(base, 'pages', context);
161
+ collections.push(...pagesResult.items);
162
+ collectionErrors.push(...pagesResult.errors);
163
+ collectionLimitations.push(...pagesResult.limitations);
164
+ pagesRequested += pagesResult.pagesRequested;
165
+ pagesRetrieved += pagesResult.pagesRetrieved;
166
+ continuationState ||= pagesResult.continuationState;
167
+ connectorError ||= pagesResult.connectorError;
168
+ if (pagesResult.providerCompleteness === 'unknown') providerTotalKnown = false;
169
+ if (pagesResult.providerTotal === null) providerTotalKnown = false; else providerTotal += pagesResult.providerTotal;
94
170
  const pageMetric = metrics.find((m) => m.external_name === 'published_pages');
95
171
  rows.push({
96
172
  metric: pageMetric,
97
- value: Array.isArray(pages) ? pages.length : 0,
173
+ value: pagesResult.items.length,
98
174
  dimensions: { date: endDate },
99
175
  observed_at: `${endDate}T00:00:00.000Z`,
100
176
  });
101
177
  }
102
178
 
179
+ const collection = collectionResult({
180
+ items: collections,
181
+ paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: paginationBoundary(context) } },
182
+ providerReportedTotal: providerTotalKnown ? providerTotal : null,
183
+ continuationState,
184
+ providerCompleteness: providerTotalKnown ? 'declared' : 'unknown',
185
+ limitations: [...collectionLimitations, 'WordPress REST API collection is bounded to accessible posts and pages.', 'Custom post types outside standard posts and pages require dedicated endpoint parameters.'],
186
+ errors: collectionErrors,
187
+ });
103
188
  return {
104
189
  rows,
105
190
  cursor: endDate,
106
- limitations: [
107
- 'WordPress REST API collection is bounded to accessible posts and pages.',
108
- 'Custom post types outside standard posts and pages require dedicated endpoint parameters.',
109
- ],
191
+ limitations: collection.limitations,
192
+ collection,
193
+ ...(connectorError ? { connectorError } : {}),
110
194
  };
111
195
  },
112
196
 
@@ -1,7 +1,12 @@
1
1
  import dns from 'node:dns';
2
+ import http from 'node:http';
3
+ import https from 'node:https';
2
4
  import net from 'node:net';
5
+ import { Readable } from 'node:stream';
6
+ import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
3
7
 
4
8
  const blockedAddresses = new net.BlockList();
9
+ const GUARDED_LOOKUP_TRANSPORT = Symbol('citable.guardedLookupTransport');
5
10
  for (const [network, prefix] of [
6
11
  ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
7
12
  ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24],
@@ -22,18 +27,89 @@ function isBlockedAddress(address) {
22
27
  }
23
28
 
24
29
  export async function validatePublicUrl(url, { lookup = dns.promises.lookup } = {}) {
30
+ const parsed = parseNetworkUrl(url);
31
+ await resolvePublicAddresses(parsed.hostname, lookup);
32
+ return parsed;
33
+ }
34
+
35
+ function parseNetworkUrl(url) {
25
36
  const parsed = new URL(url);
26
37
  if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`unsupported URL protocol: ${parsed.protocol}`);
27
38
  if (parsed.username || parsed.password) throw new Error('URL credentials are not permitted');
28
- const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
39
+ return parsed;
40
+ }
41
+
42
+ async function resolvePublicAddresses(rawHostname, lookup, options = {}) {
43
+ const hostname = rawHostname.replace(/^\[|\]$/g, '');
29
44
  const literalFamily = net.isIP(hostname);
30
45
  const addresses = literalFamily
31
46
  ? [{ address: hostname, family: literalFamily }]
32
- : await lookup(hostname, { all: true, verbatim: true });
47
+ : await lookup(hostname, { ...options, all: true, verbatim: true });
33
48
  if (!addresses.length || addresses.some(({ address }) => isBlockedAddress(address))) {
34
49
  throw new Error(`refusing private, loopback, or non-public destination: ${hostname}`);
35
50
  }
36
- return parsed;
51
+ return addresses;
52
+ }
53
+
54
+ function createGuardedLookup(lookup) {
55
+ return (hostname, options, callback) => {
56
+ const normalizedOptions = typeof options === 'number' ? { family: options } : (options ?? {});
57
+ resolvePublicAddresses(hostname, lookup, { family: normalizedOptions.family || 0 })
58
+ .then((addresses) => {
59
+ if (normalizedOptions.all) callback(null, addresses);
60
+ else callback(null, addresses[0].address, addresses[0].family);
61
+ })
62
+ .catch((error) => callback(error));
63
+ };
64
+ }
65
+
66
+ function fetchWithGuardedLookup(url, { headers, signal, lookup }) {
67
+ const parsed = new URL(url);
68
+ const transport = parsed.protocol === 'https:' ? https : http;
69
+ return new Promise((resolve, reject) => {
70
+ const request = transport.request(parsed, {
71
+ method: 'GET',
72
+ headers,
73
+ lookup,
74
+ servername: parsed.protocol === 'https:' ? parsed.hostname : undefined,
75
+ signal,
76
+ }, (response) => {
77
+ const responseHeaders = new Headers();
78
+ for (const [name, value] of Object.entries(response.headers)) {
79
+ if (Array.isArray(value)) for (const item of value) responseHeaders.append(name, item);
80
+ else if (value !== undefined) responseHeaders.set(name, value);
81
+ }
82
+ const encoding = String(response.headers['content-encoding'] ?? '').toLowerCase();
83
+ let body = response;
84
+ if (encoding === 'gzip' || encoding === 'x-gzip') body = response.pipe(createGunzip());
85
+ else if (encoding === 'deflate') body = response.pipe(createInflate());
86
+ else if (encoding === 'br') body = response.pipe(createBrotliDecompress());
87
+ const noBody = response.statusCode === 204 || response.statusCode === 205 || response.statusCode === 304;
88
+ resolve(new Response(noBody ? null : Readable.toWeb(body), {
89
+ status: response.statusCode,
90
+ statusText: response.statusMessage,
91
+ headers: responseHeaders,
92
+ }));
93
+ });
94
+ request.on('error', reject);
95
+ request.end();
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Declare that a custom transport uses the `lookup` callback passed by fetchUrl
101
+ * for its actual socket connection. Undeclared transports are rejected before
102
+ * execution because URL preflight alone does not prevent DNS rebinding.
103
+ */
104
+ export function declareGuardedLookupTransport(fetchImpl) {
105
+ if (typeof fetchImpl !== 'function') throw new TypeError('custom transport must be a function');
106
+ Object.defineProperty(fetchImpl, GUARDED_LOOKUP_TRANSPORT, {
107
+ value: true,
108
+ enumerable: false,
109
+ configurable: false,
110
+ writable: false,
111
+ });
112
+ return fetchImpl;
37
113
  }
38
114
 
39
115
  /** Enforce the public-network policy for each Playwright request. */
@@ -62,12 +138,12 @@ export async function handlePublicBrowserRoute(route, { lookup = dns.promises.lo
62
138
  }
63
139
  }
64
140
 
65
- async function readBodyLimited(res, maxBodyBytes) {
141
+ async function readBodyLimited(res, maxBodyBytes, responseType = 'text') {
66
142
  const declared = Number(res.headers.get('content-length'));
67
143
  if (Number.isFinite(declared) && declared > maxBodyBytes) {
68
- throw new Error(`response body exceeds ${maxBodyBytes} bytes`);
144
+ throw bodyLimitError(maxBodyBytes);
69
145
  }
70
- if (!res.body) return '';
146
+ if (!res.body) return { body: responseType === 'buffer' ? Buffer.alloc(0) : '', bodyBytes: Buffer.alloc(0) };
71
147
  const reader = res.body.getReader();
72
148
  const chunks = [];
73
149
  let total = 0;
@@ -76,7 +152,7 @@ async function readBodyLimited(res, maxBodyBytes) {
76
152
  const { done, value } = await reader.read();
77
153
  if (done) break;
78
154
  total += value.byteLength;
79
- if (total > maxBodyBytes) throw new Error(`response body exceeds ${maxBodyBytes} bytes`);
155
+ if (total > maxBodyBytes) throw bodyLimitError(maxBodyBytes);
80
156
  chunks.push(value);
81
157
  }
82
158
  } finally {
@@ -85,42 +161,127 @@ async function readBodyLimited(res, maxBodyBytes) {
85
161
  const bytes = new Uint8Array(total);
86
162
  let offset = 0;
87
163
  for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
88
- return new TextDecoder().decode(bytes);
164
+ const bodyBytes = Buffer.from(bytes);
165
+ return { body: responseType === 'buffer' ? bodyBytes : new TextDecoder().decode(bytes), bodyBytes };
166
+ }
167
+
168
+ function bodyLimitError(maxBodyBytes) {
169
+ const error = new Error(`response body exceeds ${maxBodyBytes} bytes`);
170
+ error.code = 'FETCH_BODY_LIMIT';
171
+ return error;
172
+ }
173
+
174
+ function timeoutError(timeoutMs) {
175
+ const error = new Error(`request timeout after ${timeoutMs}ms`);
176
+ error.code = 'FETCH_TIMEOUT';
177
+ return error;
178
+ }
179
+
180
+ function errorCode(error) {
181
+ const code = typeof error?.code === 'string' && error.code ? error.code : null;
182
+ if (code && /^[A-Z][A-Z0-9_]{0,63}$/.test(code)) return code;
183
+ if (error?.name === 'AbortError') return 'FETCH_ABORTED';
184
+ return 'FETCH_ERROR';
185
+ }
186
+
187
+ function attemptUrl(url) {
188
+ const parsed = new URL(url);
189
+ parsed.hash = '';
190
+ if (parsed.search) parsed.search = '?[REDACTED]';
191
+ return parsed.href;
192
+ }
193
+
194
+ function isRetryableResponseStatus(status) {
195
+ return status === 500 || status === 502 || status === 503 || status === 504;
196
+ }
197
+
198
+ function codedFetchError(message, code) {
199
+ const error = new Error(message);
200
+ error.code = code;
201
+ return error;
202
+ }
203
+
204
+ function attachAttempts(error, attempts) {
205
+ const target = error instanceof Error ? error : new Error('request failed');
206
+ Object.defineProperty(target, 'attempts', {
207
+ value: attempts.map((attempt) => ({ ...attempt })),
208
+ enumerable: true,
209
+ configurable: true,
210
+ });
211
+ return target;
89
212
  }
90
213
 
91
214
  /** Fetch a public URL with bounded retries, redirects, time, and response size. */
92
215
  export async function fetchUrl(url, {
93
216
  userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000,
94
217
  maxRetries = 3, retryDelayMs = 1000, maxBodyBytes = 5 * 1024 * 1024,
95
- fetchImpl = globalThis.fetch, lookup = dns.promises.lookup,
218
+ responseType = 'text',
219
+ fetchImpl, lookup = dns.promises.lookup,
220
+ allowUnsafeCustomTransportForTest = false,
96
221
  } = {}) {
97
- const requested = await validatePublicUrl(url, { lookup });
222
+ if (!['text', 'buffer'].includes(responseType)) throw new TypeError('responseType must be text or buffer');
223
+ const requested = parseNetworkUrl(url);
98
224
  const allowedOrigin = requested.origin;
99
225
  const chain = [];
226
+ const attempts = [];
227
+ let attemptNumber = 0;
100
228
  let current = requested.href;
229
+ const customFetch = fetchImpl && fetchImpl !== globalThis.fetch;
230
+ if (customFetch && allowUnsafeCustomTransportForTest !== true && fetchImpl[GUARDED_LOOKUP_TRANSPORT] !== true) {
231
+ throw new TypeError('custom transport must declare and implement the guarded lookup contract');
232
+ }
233
+ const effectiveFetch = customFetch ? fetchImpl : fetchWithGuardedLookup;
101
234
 
102
235
  for (let redirects = 0; redirects <= maxRedirects; redirects += 1) {
103
- await validatePublicUrl(current, { lookup });
104
236
  let res;
105
237
  let lastError;
238
+ let responseStartedAtMs = null;
106
239
  for (let attempt = 0; attempt < Math.max(1, maxRetries); attempt += 1) {
240
+ attemptNumber += 1;
241
+ const startedAtMs = performance.now();
242
+ try {
243
+ await validatePublicUrl(current, { lookup });
244
+ } catch (error) {
245
+ const endedAtMs = performance.now();
246
+ attempts.push({
247
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
248
+ elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'error', status: null,
249
+ errorCode: errorCode(error), retryDecision: 'stop',
250
+ });
251
+ throw attachAttempts(error, attempts);
252
+ }
107
253
  const controller = new AbortController();
108
- const timer = setTimeout(() => controller.abort(new Error(`request timeout after ${timeoutMs}ms`)), timeoutMs);
254
+ const timer = setTimeout(() => controller.abort(timeoutError(timeoutMs)), timeoutMs);
109
255
  try {
110
- res = await fetchImpl(current, {
256
+ res = await effectiveFetch(current, {
111
257
  redirect: 'manual',
112
258
  headers: { 'user-agent': userAgent },
113
259
  signal: controller.signal,
260
+ lookup: createGuardedLookup(lookup),
114
261
  });
115
- if (res.status < 500 || attempt === Math.max(1, maxRetries) - 1) {
262
+ const retry = isRetryableResponseStatus(res.status) && attempt < Math.max(1, maxRetries) - 1;
263
+ if (!retry) {
116
264
  lastError = null;
265
+ responseStartedAtMs = startedAtMs;
117
266
  break;
118
267
  }
268
+ const endedAtMs = performance.now();
269
+ attempts.push({
270
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
271
+ elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'response',
272
+ status: res.status, errorCode: null, retryDecision: 'retry',
273
+ });
119
274
  await res.body?.cancel();
120
275
  lastError = new Error(`server returned ${res.status}`);
121
276
  } catch (err) {
122
- lastError = err;
123
- if (controller.signal.aborted) throw controller.signal.reason ?? err;
277
+ lastError = controller.signal.aborted ? (controller.signal.reason ?? err) : err;
278
+ const retry = attempt < Math.max(1, maxRetries) - 1;
279
+ const endedAtMs = performance.now();
280
+ attempts.push({
281
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
282
+ elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'error', status: null,
283
+ errorCode: errorCode(lastError), retryDecision: retry ? 'retry' : 'stop',
284
+ });
124
285
  } finally {
125
286
  clearTimeout(timer);
126
287
  }
@@ -129,21 +290,56 @@ export async function fetchUrl(url, {
129
290
  await new Promise((resolve) => setTimeout(resolve, delay));
130
291
  }
131
292
  }
132
- if (lastError) throw lastError;
293
+ if (lastError) throw attachAttempts(lastError, attempts);
133
294
 
134
295
  const headers = Object.fromEntries(res.headers.entries());
135
296
  if (res.status >= 300 && res.status < 400 && headers.location) {
297
+ const endedAtMs = performance.now();
298
+ attempts.push({
299
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
300
+ elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'response',
301
+ status: res.status, errorCode: null,
302
+ retryDecision: redirects === maxRedirects ? 'stop' : 'redirect',
303
+ });
136
304
  if (redirects === maxRedirects) break;
137
- const next = new URL(headers.location, current);
138
- if (next.origin !== allowedOrigin) throw new Error(`redirect leaves audited origin: ${next.href}`);
139
- await validatePublicUrl(next.href, { lookup });
140
- chain.push({ url: current, status: res.status, location: headers.location });
305
+ let next;
306
+ try {
307
+ next = new URL(headers.location, current);
308
+ } catch {
309
+ throw attachAttempts(codedFetchError('invalid redirect location', 'FETCH_REDIRECT_INVALID'), attempts);
310
+ }
311
+ if (next.origin !== allowedOrigin) {
312
+ throw attachAttempts(codedFetchError('redirect leaves audited origin', 'FETCH_REDIRECT_ORIGIN'), attempts);
313
+ }
314
+ chain.push({ url: attemptUrl(current), status: res.status, location: attemptUrl(next.href) });
141
315
  current = next.href;
142
316
  await res.body?.cancel();
143
317
  continue;
144
318
  }
145
- const body = await readBodyLimited(res, maxBodyBytes);
146
- return { url: current, requestedUrl: url, status: res.status, headers, body, redirectChain: chain };
319
+ try {
320
+ const { body, bodyBytes } = await readBodyLimited(res, maxBodyBytes, responseType);
321
+ const endedAtMs = performance.now();
322
+ attempts.push({
323
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
324
+ elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'response',
325
+ status: res.status, errorCode: null, retryDecision: res.status >= 500 ? 'stop' : 'complete',
326
+ });
327
+ return {
328
+ url: current, requestedUrl: url, status: res.status, headers, body, bodyBytes,
329
+ bodyComplete: true, redirectChain: chain, attempts,
330
+ };
331
+ } catch (error) {
332
+ const endedAtMs = performance.now();
333
+ attempts.push({
334
+ attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
335
+ elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'error', status: res.status,
336
+ errorCode: errorCode(error), retryDecision: 'stop',
337
+ });
338
+ throw attachAttempts(error, attempts);
339
+ }
147
340
  }
148
- throw new Error(`redirect chain exceeded ${maxRedirects} hops for ${url}`);
341
+ throw attachAttempts(codedFetchError(
342
+ `redirect chain exceeded ${maxRedirects} hops`,
343
+ 'FETCH_REDIRECT_LIMIT',
344
+ ), attempts);
149
345
  }