docpull 6.4.0__tar.gz → 6.5.0__tar.gz

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 (270) hide show
  1. {docpull-6.4.0/src/docpull.egg-info → docpull-6.5.0}/PKG-INFO +19 -5
  2. {docpull-6.4.0 → docpull-6.5.0}/README.md +14 -2
  3. {docpull-6.4.0 → docpull-6.5.0}/pyproject.toml +6 -3
  4. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/__init__.py +1 -1
  5. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/accounting.py +5 -0
  6. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/benchmark.py +393 -5
  7. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/cli.py +56 -0
  8. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_ci.py +11 -3
  9. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/schema_extract.py +198 -4
  10. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/core/fetcher.py +23 -0
  11. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/contracts.py +215 -5
  12. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/eval_grade.py +139 -16
  13. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/free_core.py +27 -2
  14. docpull-6.5.0/src/docpull/integrations/__init__.py +31 -0
  15. docpull-6.5.0/src/docpull/integrations/_records.py +114 -0
  16. docpull-6.5.0/src/docpull/integrations/langchain.py +42 -0
  17. docpull-6.5.0/src/docpull/integrations/llamaindex.py +42 -0
  18. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/judge.py +189 -1
  19. docpull-6.5.0/src/docpull/llm_client.py +97 -0
  20. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/local_workflows.py +143 -2
  21. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/mcp/server.py +76 -1
  22. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/mcp/tools.py +157 -11
  23. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/config.py +52 -0
  24. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/events.py +1 -0
  25. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pack_tools.py +101 -1
  26. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/base.py +7 -0
  27. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/manifest.py +43 -0
  28. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/convert.py +70 -0
  29. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/fetch.py +51 -0
  30. docpull-6.5.0/src/docpull/pipeline/steps/screen.py +47 -0
  31. docpull-6.5.0/src/docpull/provenance.py +402 -0
  32. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/rendering.py +230 -45
  33. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/security/__init__.py +14 -0
  34. docpull-6.5.0/src/docpull/security/injection.py +208 -0
  35. docpull-6.5.0/src/docpull/security/optout.py +147 -0
  36. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/source_scoring.py +6 -0
  37. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/surface.py +4 -0
  38. docpull-6.5.0/src/docpull/warc.py +220 -0
  39. {docpull-6.4.0 → docpull-6.5.0/src/docpull.egg-info}/PKG-INFO +19 -5
  40. {docpull-6.4.0 → docpull-6.5.0}/src/docpull.egg-info/SOURCES.txt +19 -0
  41. {docpull-6.4.0 → docpull-6.5.0}/src/docpull.egg-info/requires.txt +5 -2
  42. {docpull-6.4.0 → docpull-6.5.0}/tests/test_accounting.py +12 -1
  43. docpull-6.5.0/tests/test_archive_discovery.py +204 -0
  44. {docpull-6.4.0 → docpull-6.5.0}/tests/test_benchmark.py +251 -0
  45. {docpull-6.4.0 → docpull-6.5.0}/tests/test_free_core.py +1 -1
  46. docpull-6.5.0/tests/test_injection_screen.py +298 -0
  47. docpull-6.5.0/tests/test_integrations.py +209 -0
  48. docpull-6.5.0/tests/test_llm_extract.py +221 -0
  49. {docpull-6.4.0 → docpull-6.5.0}/tests/test_local_workflows.py +113 -0
  50. {docpull-6.4.0 → docpull-6.5.0}/tests/test_mcp_tools.py +35 -0
  51. docpull-6.5.0/tests/test_optout.py +414 -0
  52. {docpull-6.4.0 → docpull-6.5.0}/tests/test_pack_tools.py +20 -0
  53. docpull-6.5.0/tests/test_provenance.py +329 -0
  54. docpull-6.5.0/tests/test_qa_judge.py +345 -0
  55. {docpull-6.4.0 → docpull-6.5.0}/tests/test_rendering.py +239 -0
  56. {docpull-6.4.0 → docpull-6.5.0}/tests/test_source_scoring.py +11 -0
  57. docpull-6.5.0/tests/test_token_metrics.py +74 -0
  58. docpull-6.5.0/tests/test_warc.py +315 -0
  59. {docpull-6.4.0 → docpull-6.5.0}/LICENSE +0 -0
  60. {docpull-6.4.0 → docpull-6.5.0}/setup.cfg +0 -0
  61. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/__main__.py +0 -0
  62. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/acquisition_workflows.py +0 -0
  63. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/agent_publish.py +0 -0
  64. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/auth_cli.py +0 -0
  65. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/basis.py +0 -0
  66. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/cache/__init__.py +0 -0
  67. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/cache/frontier.py +0 -0
  68. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/cache/manager.py +0 -0
  69. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/cache/streaming_dedup.py +0 -0
  70. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/change_events.py +0 -0
  71. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_aliases.py +0 -0
  72. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/__init__.py +0 -0
  73. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/_legacy_cli.py +0 -0
  74. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/brand.py +0 -0
  75. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/cli.py +0 -0
  76. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/common.py +0 -0
  77. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/dataset.py +0 -0
  78. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/feed.py +0 -0
  79. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/openapi.py +0 -0
  80. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/package.py +0 -0
  81. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/paper.py +0 -0
  82. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/policy_pack.py +0 -0
  83. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/product.py +0 -0
  84. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/relationship.py +0 -0
  85. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/repo.py +0 -0
  86. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/search.py +0 -0
  87. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/standards.py +0 -0
  88. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/styleguide.py +0 -0
  89. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/transcript.py +0 -0
  90. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/typed.py +0 -0
  91. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/typed_models.py +0 -0
  92. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/visuals.py +0 -0
  93. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/website.py +0 -0
  94. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/wiki.py +0 -0
  95. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/context_packs/workflow_cli.py +0 -0
  96. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/contracts.py +0 -0
  97. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/contracts_cli.py +0 -0
  98. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/__init__.py +0 -0
  99. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/article_cleanup.py +0 -0
  100. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/chunking.py +0 -0
  101. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/ensemble.py +0 -0
  102. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/extractor.py +0 -0
  103. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/filings.py +0 -0
  104. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/markdown.py +0 -0
  105. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/protocols.py +0 -0
  106. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/special_cases.py +0 -0
  107. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  108. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/core/__init__.py +0 -0
  109. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/__init__.py +0 -0
  110. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/_fetch.py +0 -0
  111. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/composite.py +0 -0
  112. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/crawler.py +0 -0
  113. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/filters.py +0 -0
  114. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  115. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  116. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  117. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  118. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/protocols.py +0 -0
  119. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery/sitemap.py +0 -0
  120. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/discovery_cli.py +0 -0
  121. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/doctor.py +0 -0
  122. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/document_parse.py +0 -0
  123. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/document_worker.py +0 -0
  124. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/evidence.py +0 -0
  125. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/evidence_pack.py +0 -0
  126. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/export_formats.py +0 -0
  127. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/exports.py +0 -0
  128. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/fixtures/__init__.py +0 -0
  129. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  130. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/free_core_smoke.py +0 -0
  131. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/graph.py +0 -0
  132. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/hosted.py +0 -0
  133. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/http/__init__.py +0 -0
  134. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/http/client.py +0 -0
  135. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/http/protocols.py +0 -0
  136. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/http/rate_limiter.py +0 -0
  137. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/mcp/__init__.py +0 -0
  138. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/mcp/sources.py +0 -0
  139. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/metadata_extractor.py +0 -0
  140. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/__init__.py +0 -0
  141. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/document.py +0 -0
  142. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/profiles.py +0 -0
  143. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/run.py +0 -0
  144. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/models/schema.py +0 -0
  145. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/monitor.py +0 -0
  146. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/native_integrations.py +0 -0
  147. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/output_contract.py +0 -0
  148. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pack_reader.py +0 -0
  149. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/parallel_workflows.py +0 -0
  150. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/parity.py +0 -0
  151. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/parity_cli.py +0 -0
  152. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/passk.py +0 -0
  153. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/__init__.py +0 -0
  154. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/__init__.py +0 -0
  155. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  156. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  157. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/metadata.py +0 -0
  158. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/render.py +0 -0
  159. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/save.py +0 -0
  160. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  161. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  162. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/save_okf.py +0 -0
  163. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  164. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/pipeline/steps/validate.py +0 -0
  165. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/policy.py +0 -0
  166. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/policy_cli.py +0 -0
  167. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/project.py +0 -0
  168. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/provider_adapters.py +0 -0
  169. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/provider_capabilities.py +0 -0
  170. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/provider_cli.py +0 -0
  171. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/provider_keys.py +0 -0
  172. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/provider_probes.py +0 -0
  173. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/py.typed +0 -0
  174. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/redaction.py +0 -0
  175. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/artifact-manifest.v1.schema.json +0 -0
  176. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/basis.v2.schema.json +0 -0
  177. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/change-event.v1.schema.json +0 -0
  178. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/citation-map.v1.schema.json +0 -0
  179. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/document.v3.schema.json +0 -0
  180. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/intelligence-bundle.v1.schema.json +0 -0
  181. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/pack.v3.schema.json +0 -0
  182. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/provenance.v1.schema.json +0 -0
  183. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/relationship-pack.v1.schema.json +0 -0
  184. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/rights.v1.schema.json +0 -0
  185. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/run-identity.v1.schema.json +0 -0
  186. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/website-snapshot.v1.schema.json +0 -0
  187. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/workflow-request.v1.schema.json +0 -0
  188. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/schemas/workflow-result.v1.schema.json +0 -0
  189. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/scraper.py +0 -0
  190. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/security/download_policy.py +0 -0
  191. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/security/robots.py +0 -0
  192. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/security/url_validator.py +0 -0
  193. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/server.py +0 -0
  194. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/share.py +0 -0
  195. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/skill_export.py +0 -0
  196. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/time_utils.py +0 -0
  197. {docpull-6.4.0 → docpull-6.5.0}/src/docpull/workflows.py +0 -0
  198. {docpull-6.4.0 → docpull-6.5.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  199. {docpull-6.4.0 → docpull-6.5.0}/src/docpull.egg-info/entry_points.txt +0 -0
  200. {docpull-6.4.0 → docpull-6.5.0}/src/docpull.egg-info/top_level.txt +0 -0
  201. {docpull-6.4.0 → docpull-6.5.0}/tests/test_acquisition_workflows.py +0 -0
  202. {docpull-6.4.0 → docpull-6.5.0}/tests/test_auth_cli.py +0 -0
  203. {docpull-6.4.0 → docpull-6.5.0}/tests/test_basis.py +0 -0
  204. {docpull-6.4.0 → docpull-6.5.0}/tests/test_cache_conditional_get.py +0 -0
  205. {docpull-6.4.0 → docpull-6.5.0}/tests/test_chunking.py +0 -0
  206. {docpull-6.4.0 → docpull-6.5.0}/tests/test_ci_policy.py +0 -0
  207. {docpull-6.4.0 → docpull-6.5.0}/tests/test_claim_audit_script.py +0 -0
  208. {docpull-6.4.0 → docpull-6.5.0}/tests/test_cli.py +0 -0
  209. {docpull-6.4.0 → docpull-6.5.0}/tests/test_context_ci.py +0 -0
  210. {docpull-6.4.0 → docpull-6.5.0}/tests/test_context_packs.py +0 -0
  211. {docpull-6.4.0 → docpull-6.5.0}/tests/test_conversion.py +0 -0
  212. {docpull-6.4.0 → docpull-6.5.0}/tests/test_convert_step_new.py +0 -0
  213. {docpull-6.4.0 → docpull-6.5.0}/tests/test_discovery.py +0 -0
  214. {docpull-6.4.0 → docpull-6.5.0}/tests/test_discovery_contracts.py +0 -0
  215. {docpull-6.4.0 → docpull-6.5.0}/tests/test_doctor.py +0 -0
  216. {docpull-6.4.0 → docpull-6.5.0}/tests/test_document_parse.py +0 -0
  217. {docpull-6.4.0 → docpull-6.5.0}/tests/test_document_record.py +0 -0
  218. {docpull-6.4.0 → docpull-6.5.0}/tests/test_evidence_pack.py +0 -0
  219. {docpull-6.4.0 → docpull-6.5.0}/tests/test_exports.py +0 -0
  220. {docpull-6.4.0 → docpull-6.5.0}/tests/test_extractor_ensemble.py +0 -0
  221. {docpull-6.4.0 → docpull-6.5.0}/tests/test_feed_pack.py +0 -0
  222. {docpull-6.4.0 → docpull-6.5.0}/tests/test_frontier_resume.py +0 -0
  223. {docpull-6.4.0 → docpull-6.5.0}/tests/test_graph.py +0 -0
  224. {docpull-6.4.0 → docpull-6.5.0}/tests/test_hosted.py +0 -0
  225. {docpull-6.4.0 → docpull-6.5.0}/tests/test_http_efficiency.py +0 -0
  226. {docpull-6.4.0 → docpull-6.5.0}/tests/test_import_efficiency.py +0 -0
  227. {docpull-6.4.0 → docpull-6.5.0}/tests/test_integration.py +0 -0
  228. {docpull-6.4.0 → docpull-6.5.0}/tests/test_judge.py +0 -0
  229. {docpull-6.4.0 → docpull-6.5.0}/tests/test_link_extractors.py +0 -0
  230. {docpull-6.4.0 → docpull-6.5.0}/tests/test_live_web_smoke.py +0 -0
  231. {docpull-6.4.0 → docpull-6.5.0}/tests/test_local_pack_workflows.py +0 -0
  232. {docpull-6.4.0 → docpull-6.5.0}/tests/test_mcp_server.py +0 -0
  233. {docpull-6.4.0 → docpull-6.5.0}/tests/test_metadata_extractor.py +0 -0
  234. {docpull-6.4.0 → docpull-6.5.0}/tests/test_monitor.py +0 -0
  235. {docpull-6.4.0 → docpull-6.5.0}/tests/test_naming.py +0 -0
  236. {docpull-6.4.0 → docpull-6.5.0}/tests/test_native_integrations.py +0 -0
  237. {docpull-6.4.0 → docpull-6.5.0}/tests/test_openapi_pack.py +0 -0
  238. {docpull-6.4.0 → docpull-6.5.0}/tests/test_optional_extras_installed.py +0 -0
  239. {docpull-6.4.0 → docpull-6.5.0}/tests/test_output_contract.py +0 -0
  240. {docpull-6.4.0 → docpull-6.5.0}/tests/test_outputs_e2e.py +0 -0
  241. {docpull-6.4.0 → docpull-6.5.0}/tests/test_pack_server.py +0 -0
  242. {docpull-6.4.0 → docpull-6.5.0}/tests/test_parallel_workflows.py +0 -0
  243. {docpull-6.4.0 → docpull-6.5.0}/tests/test_parity_workflows.py +0 -0
  244. {docpull-6.4.0 → docpull-6.5.0}/tests/test_passk.py +0 -0
  245. {docpull-6.4.0 → docpull-6.5.0}/tests/test_pipeline.py +0 -0
  246. {docpull-6.4.0 → docpull-6.5.0}/tests/test_policy.py +0 -0
  247. {docpull-6.4.0 → docpull-6.5.0}/tests/test_policy_cli.py +0 -0
  248. {docpull-6.4.0 → docpull-6.5.0}/tests/test_project.py +0 -0
  249. {docpull-6.4.0 → docpull-6.5.0}/tests/test_provider_adapters.py +0 -0
  250. {docpull-6.4.0 → docpull-6.5.0}/tests/test_provider_cli.py +0 -0
  251. {docpull-6.4.0 → docpull-6.5.0}/tests/test_provider_keys.py +0 -0
  252. {docpull-6.4.0 → docpull-6.5.0}/tests/test_provider_probes.py +0 -0
  253. {docpull-6.4.0 → docpull-6.5.0}/tests/test_public_feature_smoke.py +0 -0
  254. {docpull-6.4.0 → docpull-6.5.0}/tests/test_rate_limiter.py +0 -0
  255. {docpull-6.4.0 → docpull-6.5.0}/tests/test_real_feature_smoke_script.py +0 -0
  256. {docpull-6.4.0 → docpull-6.5.0}/tests/test_real_site_regressions.py +0 -0
  257. {docpull-6.4.0 → docpull-6.5.0}/tests/test_relationship_workflow.py +0 -0
  258. {docpull-6.4.0 → docpull-6.5.0}/tests/test_release_a_plus_check_script.py +0 -0
  259. {docpull-6.4.0 → docpull-6.5.0}/tests/test_reproducible_release_build.py +0 -0
  260. {docpull-6.4.0 → docpull-6.5.0}/tests/test_save_ndjson.py +0 -0
  261. {docpull-6.4.0 → docpull-6.5.0}/tests/test_save_sqlite.py +0 -0
  262. {docpull-6.4.0 → docpull-6.5.0}/tests/test_security_hardening.py +0 -0
  263. {docpull-6.4.0 → docpull-6.5.0}/tests/test_share.py +0 -0
  264. {docpull-6.4.0 → docpull-6.5.0}/tests/test_special_cases.py +0 -0
  265. {docpull-6.4.0 → docpull-6.5.0}/tests/test_surface_contract.py +0 -0
  266. {docpull-6.4.0 → docpull-6.5.0}/tests/test_time_utils.py +0 -0
  267. {docpull-6.4.0 → docpull-6.5.0}/tests/test_trafilatura_extractor.py +0 -0
  268. {docpull-6.4.0 → docpull-6.5.0}/tests/test_typed_knowledge_packs.py +0 -0
  269. {docpull-6.4.0 → docpull-6.5.0}/tests/test_website_snapshot.py +0 -0
  270. {docpull-6.4.0 → docpull-6.5.0}/tests/test_workflow_contracts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 6.4.0
3
+ Version: 6.5.0
4
4
  Summary: Declare, sync, diff, and lock context dependencies for AI agents
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -61,7 +61,7 @@ Provides-Extra: tokens
61
61
  Requires-Dist: tiktoken>=0.7.0; extra == "tokens"
62
62
  Provides-Extra: mcp
63
63
  Requires-Dist: cryptography>=48.0.1; extra == "mcp"
64
- Requires-Dist: mcp>=1.0.0; extra == "mcp"
64
+ Requires-Dist: mcp>=1.28.1; extra == "mcp"
65
65
  Requires-Dist: pydantic-settings>=2.14.2; extra == "mcp"
66
66
  Requires-Dist: pyjwt>=2.13.0; extra == "mcp"
67
67
  Requires-Dist: python-multipart>=0.0.27; extra == "mcp"
@@ -84,6 +84,8 @@ Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "parse"
84
84
  Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "parse"
85
85
  Provides-Extra: presidio
86
86
  Requires-Dist: presidio-analyzer>=2.2.0; extra == "presidio"
87
+ Provides-Extra: provenance
88
+ Requires-Dist: cryptography>=48.0.1; extra == "provenance"
87
89
  Provides-Extra: e2b
88
90
  Requires-Dist: e2b>=2.0.0; extra == "e2b"
89
91
  Provides-Extra: llm
@@ -94,7 +96,7 @@ Requires-Dist: url-normalize>=1.4.0; extra == "all"
94
96
  Requires-Dist: trafilatura>=1.12.0; extra == "all"
95
97
  Requires-Dist: tiktoken>=0.7.0; extra == "all"
96
98
  Requires-Dist: cryptography>=48.0.1; extra == "all"
97
- Requires-Dist: mcp>=1.0.0; extra == "all"
99
+ Requires-Dist: mcp>=1.28.1; extra == "all"
98
100
  Requires-Dist: pydantic-settings>=2.14.2; extra == "all"
99
101
  Requires-Dist: pyjwt>=2.13.0; extra == "all"
100
102
  Requires-Dist: python-multipart>=0.0.27; extra == "all"
@@ -180,8 +182,10 @@ through their typed lanes and do not use discovery.
180
182
 
181
183
  The original `docpull URL ...` workflow still works: fetch public or explicitly
182
184
  authorized static/server-rendered web pages and write clean Markdown, NDJSON,
183
- SQLite, or OKF outputs. Project mode adds the persistent evidence lifecycle on
184
- top: sources, runs, diffs, exports, evals, accounting, and local auditability.
185
+ SQLite, or Open Knowledge Format (OKF) outputs. OKF is a portable bundle format
186
+ that keeps content, indexes, and manifests together for reuse across tools.
187
+ Project mode adds the persistent evidence lifecycle on top: sources, runs,
188
+ diffs, exports, evals, accounting, and local auditability.
185
189
 
186
190
  DocPull is local-first: direct fetching, sitemap/link discovery, extraction,
187
191
  indexing, pack intelligence, and opt-in `agent-browser` rendering can run with
@@ -704,6 +708,13 @@ installed.
704
708
  | Hosted extraction APIs | Managed extraction at scale | External dependency and cost |
705
709
  | docpull | Local public web-source extraction and context packs | No JavaScript rendering by default |
706
710
 
711
+ For a tool-by-tool comparison, including self-hosted browser crawlers and
712
+ hosted extraction services, see [Alternatives](docs/alternatives.md). Measured
713
+ extraction-quality, tokens-per-page, and cost comparisons on a fixed corpus —
714
+ plus local extractor baselines — live in the evaluation lab under
715
+ [`bench/`](bench/README.md), which is an internal, reproducible harness rather
716
+ than a public leaderboard.
717
+
707
718
  ## Python SDK
708
719
 
709
720
  ```python
@@ -798,6 +809,9 @@ part of the package release contract.
798
809
  When running with `--proxy`, DNS pinning is delegated to the proxy. Pass
799
810
  `--require-pinned-dns` to refuse that configuration.
800
811
 
812
+ See [Compliance Posture](docs/compliance.md) for the full identity, robots.txt,
813
+ rate-limit, and AI/TDM opt-out behavior.
814
+
801
815
  ## Troubleshooting
802
816
 
803
817
  ```bash
@@ -56,8 +56,10 @@ through their typed lanes and do not use discovery.
56
56
 
57
57
  The original `docpull URL ...` workflow still works: fetch public or explicitly
58
58
  authorized static/server-rendered web pages and write clean Markdown, NDJSON,
59
- SQLite, or OKF outputs. Project mode adds the persistent evidence lifecycle on
60
- top: sources, runs, diffs, exports, evals, accounting, and local auditability.
59
+ SQLite, or Open Knowledge Format (OKF) outputs. OKF is a portable bundle format
60
+ that keeps content, indexes, and manifests together for reuse across tools.
61
+ Project mode adds the persistent evidence lifecycle on top: sources, runs,
62
+ diffs, exports, evals, accounting, and local auditability.
61
63
 
62
64
  DocPull is local-first: direct fetching, sitemap/link discovery, extraction,
63
65
  indexing, pack intelligence, and opt-in `agent-browser` rendering can run with
@@ -580,6 +582,13 @@ installed.
580
582
  | Hosted extraction APIs | Managed extraction at scale | External dependency and cost |
581
583
  | docpull | Local public web-source extraction and context packs | No JavaScript rendering by default |
582
584
 
585
+ For a tool-by-tool comparison, including self-hosted browser crawlers and
586
+ hosted extraction services, see [Alternatives](docs/alternatives.md). Measured
587
+ extraction-quality, tokens-per-page, and cost comparisons on a fixed corpus —
588
+ plus local extractor baselines — live in the evaluation lab under
589
+ [`bench/`](bench/README.md), which is an internal, reproducible harness rather
590
+ than a public leaderboard.
591
+
583
592
  ## Python SDK
584
593
 
585
594
  ```python
@@ -674,6 +683,9 @@ part of the package release contract.
674
683
  When running with `--proxy`, DNS pinning is delegated to the proxy. Pass
675
684
  `--require-pinned-dns` to refuse that configuration.
676
685
 
686
+ See [Compliance Posture](docs/compliance.md) for the full identity, robots.txt,
687
+ rate-limit, and AI/TDM opt-out behavior.
688
+
677
689
  ## Troubleshooting
678
690
 
679
691
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "6.4.0"
7
+ version = "6.5.0"
8
8
  dynamic = []
9
9
  description = "Declare, sync, diff, and lock context dependencies for AI agents"
10
10
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -110,7 +110,7 @@ tokens = [
110
110
  ]
111
111
  mcp = [
112
112
  "cryptography>=48.0.1",
113
- "mcp>=1.0.0",
113
+ "mcp>=1.28.1",
114
114
  "pydantic-settings>=2.14.2",
115
115
  "pyjwt>=2.13.0",
116
116
  "python-multipart>=0.0.27",
@@ -141,6 +141,9 @@ parse = [
141
141
  presidio = [
142
142
  "presidio-analyzer>=2.2.0",
143
143
  ]
144
+ provenance = [
145
+ "cryptography>=48.0.1",
146
+ ]
144
147
  e2b = [
145
148
  "e2b>=2.0.0",
146
149
  ]
@@ -153,7 +156,7 @@ all = [
153
156
  "trafilatura>=1.12.0",
154
157
  "tiktoken>=0.7.0",
155
158
  "cryptography>=48.0.1",
156
- "mcp>=1.0.0",
159
+ "mcp>=1.28.1",
157
160
  "pydantic-settings>=2.14.2",
158
161
  "pyjwt>=2.13.0",
159
162
  "python-multipart>=0.0.27",
@@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Any
15
15
 
16
16
  from .surface import PUBLIC_SDK_EXPORTS
17
17
 
18
- __version__ = "6.4.0"
18
+ __version__ = "6.5.0"
19
19
 
20
20
  _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
21
21
  **{
@@ -232,6 +232,11 @@ def default_route_steps(
232
232
  RouteStep("direct_http", "available", detail="fetch public HTTPS source directly"),
233
233
  RouteStep("sitemap_link_discovery", "available", detail="use sitemap and static links"),
234
234
  RouteStep("embedded_data_extraction", "available", detail="use framework/static page data"),
235
+ RouteStep(
236
+ "archive_fallback",
237
+ "available",
238
+ detail="replay Wayback CDX and Common Crawl index snapshots at zero cost",
239
+ ),
235
240
  ]
236
241
  steps.append(
237
242
  RouteStep(
@@ -87,7 +87,16 @@ BENCHMARK_SCORE_WEIGHTS = {
87
87
  "density": 0.15,
88
88
  }
89
89
  PASS_AT_K_THRESHOLDS: tuple[int, ...] = (70, 80, 90)
90
- TARGET_SET_CHOICES = ("single", "tool-docs", "provider-matrix", "zero-dollar", "phase2", "v2")
90
+ TARGET_SET_CHOICES = (
91
+ "single",
92
+ "tool-docs",
93
+ "provider-matrix",
94
+ "zero-dollar",
95
+ "phase2",
96
+ "v2",
97
+ "adversarial-v3",
98
+ "full",
99
+ )
91
100
  HTTP_MAX_RESPONSE_BYTES = 16 * 1024 * 1024
92
101
  HTTP_MAX_ERROR_BYTES = 64 * 1024
93
102
  # Conservative per-call USD figures used ONLY for the pre-flight
@@ -306,12 +315,269 @@ PHASE2_MEASUREMENT_TARGETS: tuple[_BenchmarkTarget, ...] = (
306
315
  )
307
316
 
308
317
 
318
+ ADVERSARIAL_V3_TARGETS: tuple[_BenchmarkTarget, ...] = (
319
+ _BenchmarkTarget(
320
+ id="nyt_snowfall_paywall",
321
+ label="NYT Snow Fall metered paywall",
322
+ url="https://www.nytimes.com/projects/2012/snow-fall/",
323
+ include_domains=("www.nytimes.com", "nytimes.com"),
324
+ objective="Measure honest failure/skip behavior on a metered news paywall",
325
+ queries=("New York Times Snow Fall avalanche Tunnel Creek feature",),
326
+ kind="paywalled_news",
327
+ min_expected_records=1,
328
+ notes=(
329
+ "Metered paywall family: the page presents a paywall/registration "
330
+ "prompt; the harness should record an honest skip, not fabricated content."
331
+ ),
332
+ zero_dollar_route="policy",
333
+ ),
334
+ _BenchmarkTarget(
335
+ id="bloomberg_bot_wall",
336
+ label="Bloomberg What Is Code bot wall",
337
+ url="https://www.bloomberg.com/graphics/2015-paul-ford-what-is-code/",
338
+ include_domains=("www.bloomberg.com", "bloomberg.com"),
339
+ objective="Measure behavior against a paywalled site with an aggressive bot wall",
340
+ queries=("Bloomberg Paul Ford what is code essay",),
341
+ kind="bot_wall_paywall",
342
+ min_expected_records=1,
343
+ notes="Paywall plus PX-style bot challenge; expect a challenge page, not article text.",
344
+ zero_dollar_route="cloud_browser",
345
+ ),
346
+ _BenchmarkTarget(
347
+ id="linkedin_robots_disallow",
348
+ label="LinkedIn robots-disallowed profile",
349
+ url="https://www.linkedin.com/company/anthropicresearch",
350
+ include_domains=("www.linkedin.com", "linkedin.com"),
351
+ objective="Measure robots.txt-disallowed capture behavior on LinkedIn",
352
+ queries=("Anthropic LinkedIn company page",),
353
+ kind="robots_disallowed_profile",
354
+ min_expected_records=1,
355
+ notes="robots.txt disallow + auth wall family; correct behavior is a policy skip.",
356
+ zero_dollar_route="policy",
357
+ ),
358
+ _BenchmarkTarget(
359
+ id="x_login_wall_spa",
360
+ label="X login-walled SPA profile",
361
+ url="https://x.com/AnthropicAI",
362
+ include_domains=("x.com", "twitter.com"),
363
+ objective="Measure behavior on a login-walled JavaScript-only social profile",
364
+ queries=("Anthropic X Twitter profile posts",),
365
+ kind="login_walled_spa",
366
+ min_expected_records=1,
367
+ notes="Login wall + SPA shell; static HTML carries no post content.",
368
+ zero_dollar_route="policy",
369
+ ),
370
+ _BenchmarkTarget(
371
+ id="quora_robots_login_nag",
372
+ label="Quora robots-disallowed topic",
373
+ url="https://www.quora.com/topic/Artificial-Intelligence",
374
+ include_domains=("www.quora.com", "quora.com"),
375
+ objective="Measure robots-disallowed and login-nag behavior on Quora",
376
+ queries=("Quora artificial intelligence topic questions",),
377
+ kind="robots_disallowed_forum",
378
+ min_expected_records=1,
379
+ notes="robots.txt disallows most crawling and a login overlay interrupts content.",
380
+ zero_dollar_route="policy",
381
+ ),
382
+ _BenchmarkTarget(
383
+ id="reddit_infinite_scroll",
384
+ label="Reddit infinite-scroll listing",
385
+ url="https://www.reddit.com/r/MachineLearning/",
386
+ include_domains=("www.reddit.com", "reddit.com"),
387
+ objective="Measure extraction from an infinite-scroll listing page",
388
+ queries=("Reddit MachineLearning subreddit recent posts",),
389
+ kind="infinite_scroll_listing",
390
+ min_expected_records=1,
391
+ notes="Infinite scroll family: static fetch sees only the first page of the feed.",
392
+ zero_dollar_route="local_browser",
393
+ ),
394
+ _BenchmarkTarget(
395
+ id="excalidraw_spa_only",
396
+ label="Excalidraw client-only SPA",
397
+ url="https://excalidraw.com",
398
+ include_domains=("excalidraw.com",),
399
+ objective="Measure behavior on a canvas SPA with no server-rendered content",
400
+ queries=("Excalidraw virtual whiteboard app",),
401
+ kind="spa_only_app",
402
+ min_expected_records=1,
403
+ notes="Pure client-side app shell; there is no meaningful static document body.",
404
+ zero_dollar_route="local_browser",
405
+ ),
406
+ _BenchmarkTarget(
407
+ id="openai_platform_docs_challenge",
408
+ label="OpenAI platform docs bot challenge",
409
+ url="https://platform.openai.com/docs/api-reference",
410
+ include_domains=("platform.openai.com",),
411
+ objective="Measure behavior on Cloudflare-challenge-fronted SPA docs",
412
+ queries=("OpenAI API reference platform documentation",),
413
+ kind="bot_challenge_docs",
414
+ min_expected_records=1,
415
+ freshness_terms=("changelog", "latest", "deprecat"),
416
+ notes="Cloudflare challenge + client-rendered docs; direct HTTP sees the challenge page.",
417
+ zero_dollar_route="cloud_browser",
418
+ ),
419
+ _BenchmarkTarget(
420
+ id="crunchbase_bot_wall",
421
+ label="Crunchbase hard bot wall",
422
+ url="https://www.crunchbase.com/organization/anthropic",
423
+ include_domains=("www.crunchbase.com", "crunchbase.com"),
424
+ objective="Measure honest failure against a hard bot-wall host",
425
+ queries=("Anthropic Crunchbase company profile funding",),
426
+ kind="bot_wall_profile",
427
+ min_expected_records=1,
428
+ notes="Aggressive bot wall typically answers non-browser clients with 403.",
429
+ zero_dollar_route="cloud_browser",
430
+ ),
431
+ _BenchmarkTarget(
432
+ id="whatwg_html_spec_huge",
433
+ label="WHATWG HTML spec single page",
434
+ url="https://html.spec.whatwg.org/",
435
+ include_domains=("html.spec.whatwg.org",),
436
+ objective="Build a context pack from one very large single-page spec",
437
+ queries=("WHATWG HTML living standard single page spec",),
438
+ kind="huge_single_page",
439
+ min_expected_records=1,
440
+ freshness_terms=("living standard", "last updated"),
441
+ notes="Very-large-page family: a multi-megabyte single document stresses size caps.",
442
+ ),
443
+ _BenchmarkTarget(
444
+ id="ecma262_spec_huge",
445
+ label="ECMAScript spec single page",
446
+ url="https://tc39.es/ecma262/",
447
+ include_domains=("tc39.es",),
448
+ objective="Build a context pack from the very large ECMAScript specification page",
449
+ queries=("ECMAScript language specification ecma262",),
450
+ kind="huge_single_page",
451
+ min_expected_records=1,
452
+ freshness_terms=("ecmascript", "edition"),
453
+ notes="Very-large-page family: single-page spec measured in tens of megabytes.",
454
+ ),
455
+ _BenchmarkTarget(
456
+ id="python_docs_japanese",
457
+ label="Python tutorial in Japanese",
458
+ url="https://docs.python.org/ja/3/tutorial/index.html",
459
+ include_domains=("docs.python.org",),
460
+ objective="Build a context pack from non-English (Japanese) Python docs",
461
+ queries=("Python チュートリアル 日本語 ドキュメント",),
462
+ kind="non_english_docs",
463
+ min_expected_records=3,
464
+ freshness_terms=("python", "チュートリアル"),
465
+ notes="Non-English family: English boilerplate sniffing under-reports CJK noise.",
466
+ ),
467
+ _BenchmarkTarget(
468
+ id="mdn_french_js",
469
+ label="MDN JavaScript docs in French",
470
+ url="https://developer.mozilla.org/fr/docs/Web/JavaScript",
471
+ include_domains=("developer.mozilla.org",),
472
+ objective="Build a context pack from non-English (French) MDN JavaScript docs",
473
+ queries=("MDN JavaScript documentation française guide",),
474
+ kind="non_english_docs",
475
+ min_expected_records=3,
476
+ freshness_terms=("javascript", "baseline"),
477
+ notes="Non-English family: localized navigation defeats English-only cleanup heuristics.",
478
+ ),
479
+ _BenchmarkTarget(
480
+ id="wikipedia_arabic_rtl",
481
+ label="Arabic Wikipedia AI article",
482
+ url="https://ar.wikipedia.org/wiki/%D8%B0%D9%83%D8%A7%D8%A1_%D8%A7%D8%B5%D8%B7%D9%86%D8%A7%D8%B9%D9%8A",
483
+ include_domains=("ar.wikipedia.org",),
484
+ objective="Build a context pack from a right-to-left Arabic encyclopedia article",
485
+ queries=("ويكيبيديا ذكاء اصطناعي مقالة",),
486
+ kind="non_english_rtl",
487
+ min_expected_records=1,
488
+ freshness_terms=("ذكاء",),
489
+ notes="RTL + non-Latin family: exercises encoding, direction, and template stripping.",
490
+ ),
491
+ _BenchmarkTarget(
492
+ id="django_docs_redirect_chain",
493
+ label="Django docs locale/version redirects",
494
+ url="https://docs.djangoproject.com/",
495
+ include_domains=("docs.djangoproject.com",),
496
+ objective="Build a context pack behind a language and version redirect chain",
497
+ queries=("Django documentation stable release",),
498
+ kind="redirect_chain_docs",
499
+ min_expected_records=3,
500
+ freshness_terms=("django", "release notes"),
501
+ notes="Redirect-chain family: root URL negotiates through locale and version hops.",
502
+ ),
503
+ _BenchmarkTarget(
504
+ id="httpbin_redirect_chain",
505
+ label="httpbin three-hop redirect",
506
+ url="https://httpbin.org/redirect/3",
507
+ include_domains=("httpbin.org",),
508
+ objective="Measure redirect-chain handling on a deterministic multi-hop endpoint",
509
+ queries=("httpbin redirect endpoint testing",),
510
+ kind="redirect_chain_json",
511
+ min_expected_records=1,
512
+ notes="Redirect-chain family: three deterministic hops that terminate in a JSON body.",
513
+ ),
514
+ _BenchmarkTarget(
515
+ id="yahoo_yql_gone",
516
+ label="Retired Yahoo YQL docs",
517
+ url="https://developer.yahoo.com/yql/",
518
+ include_domains=("developer.yahoo.com",),
519
+ objective="Measure archive-fallback behavior for a retired documentation product",
520
+ queries=("Yahoo YQL query language documentation archive",),
521
+ kind="gone_page_archive_fallback",
522
+ min_expected_records=1,
523
+ notes="404/410 family: YQL was retired; Wayback snapshots are the only recovery path.",
524
+ zero_dollar_route="provider",
525
+ ),
526
+ _BenchmarkTarget(
527
+ id="bootstrap2_docs_gone",
528
+ label="Legacy Bootstrap 2 docs URL",
529
+ url="https://twitter.github.io/bootstrap/",
530
+ include_domains=("twitter.github.io", "getbootstrap.com"),
531
+ objective="Measure archive fallback for a moved and deleted legacy docs site",
532
+ queries=("Bootstrap 2 documentation twitter github pages",),
533
+ kind="gone_page_archive_fallback",
534
+ min_expected_records=1,
535
+ notes="404/410 family: original Bootstrap docs host is gone but archived in Wayback.",
536
+ zero_dollar_route="provider",
537
+ ),
538
+ _BenchmarkTarget(
539
+ id="soundcloud_embedded_json",
540
+ label="SoundCloud embedded-JSON page",
541
+ url="https://soundcloud.com/nasa",
542
+ include_domains=("soundcloud.com",),
543
+ objective="Measure extraction when content lives only in embedded hydration JSON",
544
+ queries=("NASA SoundCloud audio tracks",),
545
+ kind="embedded_json_content",
546
+ min_expected_records=1,
547
+ notes="Embedded-JSON family: page content ships in a window hydration blob, not HTML.",
548
+ zero_dollar_route="local_browser",
549
+ ),
550
+ _BenchmarkTarget(
551
+ id="swagger_petstore_openapi_only",
552
+ label="Swagger Petstore OpenAPI-only UI",
553
+ url="https://petstore3.swagger.io/",
554
+ include_domains=("petstore3.swagger.io",),
555
+ objective="Measure extraction from a reference UI whose content is only an OpenAPI document",
556
+ queries=("Swagger Petstore OpenAPI 3 reference",),
557
+ kind="openapi_only_reference",
558
+ min_expected_records=1,
559
+ notes=(
560
+ "OpenAPI-only family: the HTML is an empty Swagger UI shell; the reference "
561
+ "lives in /api/v3/openapi.json fetched by JavaScript."
562
+ ),
563
+ zero_dollar_route="local_browser",
564
+ ),
565
+ )
566
+
567
+
309
568
  TARGET_SETS: dict[str, tuple[_BenchmarkTarget, ...]] = {
310
569
  "tool-docs": TOOL_DOC_TARGETS,
311
570
  "provider-matrix": (*TOOL_DOC_TARGETS, *ADVERSARIAL_TARGETS),
312
571
  "zero-dollar": (*TOOL_DOC_TARGETS, *ADVERSARIAL_TARGETS, *PHASE2_MEASUREMENT_TARGETS),
313
572
  "phase2": (*TOOL_DOC_TARGETS, *ADVERSARIAL_TARGETS, *PHASE2_MEASUREMENT_TARGETS),
314
573
  "v2": (*TOOL_DOC_TARGETS, *ADVERSARIAL_TARGETS),
574
+ "adversarial-v3": ADVERSARIAL_V3_TARGETS,
575
+ "full": (
576
+ *TOOL_DOC_TARGETS,
577
+ *ADVERSARIAL_TARGETS,
578
+ *PHASE2_MEASUREMENT_TARGETS,
579
+ *ADVERSARIAL_V3_TARGETS,
580
+ ),
315
581
  }
316
582
 
317
583
 
@@ -338,7 +604,10 @@ def create_benchmark_parser() -> argparse.ArgumentParser:
338
604
  "'provider-matrix' adds low-cap hard targets; 'zero-dollar' adds "
339
605
  "Phase 2 measurement targets for JS docs, pricing, filings, feeds, "
340
606
  "sitemaps, and search-to-evidence. 'phase2' aliases 'zero-dollar'; "
341
- "'v2' remains as a compatibility alias."
607
+ "'v2' remains as a compatibility alias. 'adversarial-v3' runs the "
608
+ "hard-failure-family matrix (paywalls, robots blocks, bot walls, "
609
+ "SPAs, huge pages, non-English docs, redirects, dead pages); "
610
+ "'full' runs every target group."
342
611
  ),
343
612
  )
344
613
  quick.add_argument(
@@ -1906,6 +2175,8 @@ def _failed_case(
1906
2175
  "artifact_size_bytes": _dir_size(output_dir),
1907
2176
  "pack_score": None,
1908
2177
  "benchmark_score": None,
2178
+ # Report data, not a credential.
2179
+ "token_metrics": None, # nosec B105
1909
2180
  "source_score_count": 0,
1910
2181
  }
1911
2182
  )
@@ -2059,11 +2330,12 @@ def _attach_benchmark_score(
2059
2330
  *,
2060
2331
  target: _BenchmarkTarget | None = None,
2061
2332
  ) -> None:
2333
+ records = _read_benchmark_records(output_dir)
2334
+ payload["token_metrics"] = _token_metrics(payload, records)
2062
2335
  score = payload.get("pack_score")
2063
2336
  if not isinstance(score, dict):
2064
2337
  payload["benchmark_score"] = None
2065
2338
  return
2066
- records = _read_benchmark_records(output_dir)
2067
2339
  payload["benchmark_score"] = _benchmark_score(
2068
2340
  payload=payload,
2069
2341
  records=records,
@@ -2072,6 +2344,48 @@ def _attach_benchmark_score(
2072
2344
  )
2073
2345
 
2074
2346
 
2347
+ def _token_metrics(payload: dict[str, Any], records: list[dict[str, Any]]) -> dict[str, Any] | None:
2348
+ """Aggregate per-record token counts into a per-case token block.
2349
+
2350
+ ``page_count`` is the number of unique record URLs (chunked records from
2351
+ one document count once); when no record carries a URL it falls back to
2352
+ the record count. ``source_bytes``/``byte_reduction`` are only reported
2353
+ when the case's crawl stats recorded downloaded bytes — provider packs do
2354
+ not expose raw source sizes, so the fields are omitted there.
2355
+ """
2356
+ if not records:
2357
+ return None
2358
+ counter = TokenCounter()
2359
+ total_tokens = 0
2360
+ markdown_bytes = 0
2361
+ urls: set[str] = set()
2362
+ for record in records:
2363
+ content = str(record.get("content") or "")
2364
+ token_count = record.get("token_count")
2365
+ if isinstance(token_count, int) and not isinstance(token_count, bool) and token_count > 0:
2366
+ total_tokens += token_count
2367
+ elif content:
2368
+ total_tokens += counter.count(content)
2369
+ markdown_bytes += len(content.encode("utf-8"))
2370
+ url = str(record.get("url") or "")
2371
+ if url:
2372
+ urls.add(url)
2373
+ page_count = len(urls) if urls else len(records)
2374
+ metrics: dict[str, Any] = {
2375
+ "total_tokens": total_tokens,
2376
+ "page_count": page_count,
2377
+ "tokens_per_page": round(total_tokens / page_count, 1) if page_count else 0.0,
2378
+ "tokenizer": f"tiktoken:{counter.encoding}" if counter.exact else "heuristic",
2379
+ "markdown_bytes": markdown_bytes,
2380
+ }
2381
+ stats = payload.get("stats")
2382
+ source_bytes = stats.get("bytes_downloaded") if isinstance(stats, dict) else None
2383
+ if isinstance(source_bytes, int) and not isinstance(source_bytes, bool) and source_bytes > 0:
2384
+ metrics["source_bytes"] = source_bytes
2385
+ metrics["byte_reduction"] = round(1 - markdown_bytes / source_bytes, 4)
2386
+ return metrics
2387
+
2388
+
2075
2389
  def _benchmark_score(
2076
2390
  *,
2077
2391
  payload: dict[str, Any],
@@ -2193,6 +2507,25 @@ def _aggregate_runs(
2193
2507
  else:
2194
2508
  case["benchmark_score"] = None
2195
2509
 
2510
+ token_metrics_runs = [
2511
+ run["token_metrics"] for run in successful if isinstance(run.get("token_metrics"), dict)
2512
+ ]
2513
+ if token_metrics_runs:
2514
+ total_tokens = [int(metrics.get("total_tokens") or 0) for metrics in token_metrics_runs]
2515
+ page_counts = [int(metrics.get("page_count") or 0) for metrics in token_metrics_runs]
2516
+ tokens_per_page = [float(metrics.get("tokens_per_page") or 0.0) for metrics in token_metrics_runs]
2517
+ case["token_metrics"] = {
2518
+ **token_metrics_runs[0],
2519
+ "total_tokens": _median_int(total_tokens),
2520
+ "page_count": _median_int(page_counts),
2521
+ "tokens_per_page": round(median(tokens_per_page), 1),
2522
+ "tokens_per_page_min": round(min(tokens_per_page), 1),
2523
+ "tokens_per_page_max": round(max(tokens_per_page), 1),
2524
+ "tokens_per_page_runs": [round(value, 1) for value in tokens_per_page],
2525
+ }
2526
+ else:
2527
+ case["token_metrics"] = None
2528
+
2196
2529
  if not successful:
2197
2530
  case["status"] = "failed"
2198
2531
  first_error = next((run.get("error") for run in runs if run.get("error")), None)
@@ -2973,10 +3306,12 @@ def _zero_dollar_escalation_suggestions(target_results: dict[str, dict[str, Any]
2973
3306
  if class_name == "complete_with_local_browser":
2974
3307
  suggestions.append(_local_browser_escalation(target_id, result))
2975
3308
  elif class_name == "requires_provider":
3309
+ suggestions.append(_archive_escalation(target_id, result))
2976
3310
  suggestions.append(_provider_escalation(target_id, result))
2977
3311
  elif class_name == "requires_cloud_browser":
2978
3312
  suggestions.append(_cloud_browser_escalation(target_id, result))
2979
3313
  elif class_name == "partial_for_0":
3314
+ suggestions.append(_archive_escalation(target_id, result))
2980
3315
  suggestions.append(_local_discovery_escalation(target_id, result))
2981
3316
  elif class_name == "blocked_by_policy":
2982
3317
  suggestions.append(_policy_escalation(target_id, result))
@@ -3034,6 +3369,43 @@ def _local_discovery_escalation(target_id: str, result: dict[str, Any]) -> dict[
3034
3369
  }
3035
3370
 
3036
3371
 
3372
+ def _archive_escalation(target_id: str, result: dict[str, Any]) -> dict[str, Any]:
3373
+ url = _target_url(result)
3374
+ archive_dir = f"./packs/{target_id}-archives"
3375
+ return {
3376
+ "target_id": target_id,
3377
+ "action": "try_archive_discovery",
3378
+ "summary": "Replay free Wayback CDX and Common Crawl index snapshots before paid providers.",
3379
+ "commands": [
3380
+ _command(
3381
+ "docpull",
3382
+ "discover",
3383
+ "scan",
3384
+ url,
3385
+ "--source",
3386
+ "wayback",
3387
+ "--source",
3388
+ "commoncrawl",
3389
+ "-o",
3390
+ archive_dir,
3391
+ ),
3392
+ _command(
3393
+ "docpull",
3394
+ "discover",
3395
+ "fetch",
3396
+ archive_dir,
3397
+ "--select",
3398
+ "top:10",
3399
+ "-o",
3400
+ f"./packs/{target_id}-archive-pack",
3401
+ ),
3402
+ ],
3403
+ "estimated_paid_request_count": 0,
3404
+ "estimated_paid_cost_usd": 0.0,
3405
+ "paid_capable": False,
3406
+ }
3407
+
3408
+
3037
3409
  def _provider_escalation(target_id: str, result: dict[str, Any]) -> dict[str, Any]:
3038
3410
  objective = _target_objective(result)
3039
3411
  query = _target_query(result)
@@ -3382,9 +3754,9 @@ def _markdown_report(report: dict[str, Any]) -> str:
3382
3754
  "",
3383
3755
  (
3384
3756
  "| Target | Case | Workflow | Wall seconds | Benchmark score | "
3385
- "Pack score | Records | Estimated cost |"
3757
+ "Pack score | Records | Tokens/page | Estimated cost |"
3386
3758
  ),
3387
- "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |",
3759
+ "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |",
3388
3760
  ]
3389
3761
  for case in report["cases"]:
3390
3762
  score = case.get("pack_score")
@@ -3415,6 +3787,7 @@ def _markdown_report(report: dict[str, Any]) -> str:
3415
3787
  f"{benchmark_score} | "
3416
3788
  f"{score_value} | "
3417
3789
  f"{record_count} | "
3790
+ f"{_case_tokens_per_page_text(case)} | "
3418
3791
  f"{cost_text} |"
3419
3792
  )
3420
3793
  heatmap = _matrix_heatmap_markdown(report)
@@ -3603,6 +3976,21 @@ def _case_wall_seconds_text(case: dict[str, Any]) -> str:
3603
3976
  return text
3604
3977
 
3605
3978
 
3979
+ def _case_tokens_per_page_text(case: dict[str, Any]) -> str:
3980
+ metrics = case.get("token_metrics")
3981
+ if not isinstance(metrics, dict):
3982
+ return ""
3983
+ value = metrics.get("tokens_per_page")
3984
+ if not isinstance(value, int | float) or isinstance(value, bool):
3985
+ return ""
3986
+ text = f"{float(value):.1f}"
3987
+ value_min = metrics.get("tokens_per_page_min")
3988
+ value_max = metrics.get("tokens_per_page_max")
3989
+ if isinstance(value_min, int | float) and isinstance(value_max, int | float) and value_min != value_max:
3990
+ text += f" [{value_min}–{value_max}]"
3991
+ return text
3992
+
3993
+
3606
3994
  def _case_provider_key(case: dict[str, Any]) -> str:
3607
3995
  workflow = str(case.get("workflow") or "")
3608
3996
  if workflow == "core-llm":