docpull 6.5.4__tar.gz → 6.6.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 (275) hide show
  1. {docpull-6.5.4/src/docpull.egg-info → docpull-6.6.0}/PKG-INFO +18 -12
  2. {docpull-6.5.4 → docpull-6.6.0}/README.md +6 -0
  3. {docpull-6.5.4 → docpull-6.6.0}/pyproject.toml +19 -13
  4. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/__init__.py +4 -1
  5. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cli.py +4 -0
  6. docpull-6.6.0/src/docpull/context_query.py +305 -0
  7. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/document_parse.py +5 -0
  8. docpull-6.6.0/src/docpull/frozen.py +123 -0
  9. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/server.py +96 -2
  10. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/project.py +123 -17
  11. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/surface.py +4 -0
  12. {docpull-6.5.4 → docpull-6.6.0/src/docpull.egg-info}/PKG-INFO +18 -12
  13. {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/SOURCES.txt +5 -0
  14. {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/requires.txt +17 -11
  15. {docpull-6.5.4 → docpull-6.6.0}/tests/test_ci_policy.py +2 -2
  16. docpull-6.6.0/tests/test_context_query.py +281 -0
  17. docpull-6.6.0/tests/test_frozen.py +65 -0
  18. {docpull-6.5.4 → docpull-6.6.0}/tests/test_mcp_server.py +20 -1
  19. docpull-6.6.0/tests/test_parser_dependency_support.py +54 -0
  20. {docpull-6.5.4 → docpull-6.6.0}/tests/test_project.py +181 -0
  21. {docpull-6.5.4 → docpull-6.6.0}/tests/test_real_feature_smoke_script.py +41 -0
  22. {docpull-6.5.4 → docpull-6.6.0}/LICENSE +0 -0
  23. {docpull-6.5.4 → docpull-6.6.0}/setup.cfg +0 -0
  24. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/__main__.py +0 -0
  25. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/accounting.py +0 -0
  26. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/acquisition_workflows.py +0 -0
  27. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/agent_publish.py +0 -0
  28. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/auth_cli.py +0 -0
  29. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/basis.py +0 -0
  30. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/benchmark.py +0 -0
  31. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/__init__.py +0 -0
  32. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/frontier.py +0 -0
  33. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/manager.py +0 -0
  34. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/streaming_dedup.py +0 -0
  35. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/change_events.py +0 -0
  36. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_aliases.py +0 -0
  37. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_ci.py +0 -0
  38. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/__init__.py +0 -0
  39. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/_legacy_cli.py +0 -0
  40. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/brand.py +0 -0
  41. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/cli.py +0 -0
  42. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/common.py +0 -0
  43. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/dataset.py +0 -0
  44. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/feed.py +0 -0
  45. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/openapi.py +0 -0
  46. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/package.py +0 -0
  47. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/paper.py +0 -0
  48. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/policy_pack.py +0 -0
  49. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/product.py +0 -0
  50. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/relationship.py +0 -0
  51. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/repo.py +0 -0
  52. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/schema_extract.py +0 -0
  53. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/search.py +0 -0
  54. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/standards.py +0 -0
  55. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/styleguide.py +0 -0
  56. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/transcript.py +0 -0
  57. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/typed.py +0 -0
  58. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/typed_models.py +0 -0
  59. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/visuals.py +0 -0
  60. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/website.py +0 -0
  61. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/wiki.py +0 -0
  62. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/workflow_cli.py +0 -0
  63. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/contracts.py +0 -0
  64. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/contracts_cli.py +0 -0
  65. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/__init__.py +0 -0
  66. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/article_cleanup.py +0 -0
  67. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/chunking.py +0 -0
  68. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/ensemble.py +0 -0
  69. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/extractor.py +0 -0
  70. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/filings.py +0 -0
  71. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/markdown.py +0 -0
  72. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/protocols.py +0 -0
  73. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/special_cases.py +0 -0
  74. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  75. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/core/__init__.py +0 -0
  76. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/core/fetcher.py +0 -0
  77. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/__init__.py +0 -0
  78. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/_fetch.py +0 -0
  79. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/composite.py +0 -0
  80. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/contracts.py +0 -0
  81. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/crawler.py +0 -0
  82. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/filters.py +0 -0
  83. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  84. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  85. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  86. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  87. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/protocols.py +0 -0
  88. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/sitemap.py +0 -0
  89. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery_cli.py +0 -0
  90. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/doctor.py +0 -0
  91. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/document_worker.py +0 -0
  92. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/eval_grade.py +0 -0
  93. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/evidence.py +0 -0
  94. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/evidence_pack.py +0 -0
  95. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/export_formats.py +0 -0
  96. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/exports.py +0 -0
  97. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/fixtures/__init__.py +0 -0
  98. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  99. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/free_core.py +0 -0
  100. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/free_core_smoke.py +0 -0
  101. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/graph.py +0 -0
  102. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/hosted.py +0 -0
  103. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/__init__.py +0 -0
  104. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/client.py +0 -0
  105. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/protocols.py +0 -0
  106. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/rate_limiter.py +0 -0
  107. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/__init__.py +0 -0
  108. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/_records.py +0 -0
  109. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/langchain.py +0 -0
  110. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/llamaindex.py +0 -0
  111. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/judge.py +0 -0
  112. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/llm_client.py +0 -0
  113. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/local_workflows.py +0 -0
  114. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/__init__.py +0 -0
  115. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/sources.py +0 -0
  116. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/tools.py +0 -0
  117. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/metadata_extractor.py +0 -0
  118. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/__init__.py +0 -0
  119. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/config.py +0 -0
  120. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/document.py +0 -0
  121. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/events.py +0 -0
  122. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/profiles.py +0 -0
  123. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/run.py +0 -0
  124. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/schema.py +0 -0
  125. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/monitor.py +0 -0
  126. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/native_integrations.py +0 -0
  127. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/output_contract.py +0 -0
  128. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pack_reader.py +0 -0
  129. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pack_tools.py +0 -0
  130. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parallel_workflows.py +0 -0
  131. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parity.py +0 -0
  132. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parity_cli.py +0 -0
  133. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/passk.py +0 -0
  134. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/__init__.py +0 -0
  135. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/base.py +0 -0
  136. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/manifest.py +0 -0
  137. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/__init__.py +0 -0
  138. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  139. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/convert.py +0 -0
  140. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  141. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/fetch.py +0 -0
  142. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/metadata.py +0 -0
  143. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/render.py +0 -0
  144. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save.py +0 -0
  145. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  146. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  147. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_okf.py +0 -0
  148. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  149. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/screen.py +0 -0
  150. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/validate.py +0 -0
  151. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/policy.py +0 -0
  152. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/policy_cli.py +0 -0
  153. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provenance.py +0 -0
  154. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_adapters.py +0 -0
  155. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_capabilities.py +0 -0
  156. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_cli.py +0 -0
  157. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_keys.py +0 -0
  158. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_probes.py +0 -0
  159. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/py.typed +0 -0
  160. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/redaction.py +0 -0
  161. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/rendering.py +0 -0
  162. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/artifact-manifest.v1.schema.json +0 -0
  163. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/basis.v2.schema.json +0 -0
  164. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/change-event.v1.schema.json +0 -0
  165. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/citation-map.v1.schema.json +0 -0
  166. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/document.v3.schema.json +0 -0
  167. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/intelligence-bundle.v1.schema.json +0 -0
  168. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/pack.v3.schema.json +0 -0
  169. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/provenance.v1.schema.json +0 -0
  170. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/relationship-pack.v1.schema.json +0 -0
  171. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/rights.v1.schema.json +0 -0
  172. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/run-identity.v1.schema.json +0 -0
  173. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/website-snapshot.v1.schema.json +0 -0
  174. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/workflow-request.v1.schema.json +0 -0
  175. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/workflow-result.v1.schema.json +0 -0
  176. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/scraper.py +0 -0
  177. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/__init__.py +0 -0
  178. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/download_policy.py +0 -0
  179. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/injection.py +0 -0
  180. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/optout.py +0 -0
  181. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/robots.py +0 -0
  182. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/url_validator.py +0 -0
  183. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/server.py +0 -0
  184. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/share.py +0 -0
  185. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/skill_export.py +0 -0
  186. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/source_scoring.py +0 -0
  187. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/time_utils.py +0 -0
  188. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/warc.py +0 -0
  189. {docpull-6.5.4 → docpull-6.6.0}/src/docpull/workflows.py +0 -0
  190. {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  191. {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/entry_points.txt +0 -0
  192. {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/top_level.txt +0 -0
  193. {docpull-6.5.4 → docpull-6.6.0}/tests/test_accounting.py +0 -0
  194. {docpull-6.5.4 → docpull-6.6.0}/tests/test_acquisition_workflows.py +0 -0
  195. {docpull-6.5.4 → docpull-6.6.0}/tests/test_archive_discovery.py +0 -0
  196. {docpull-6.5.4 → docpull-6.6.0}/tests/test_auth_cli.py +0 -0
  197. {docpull-6.5.4 → docpull-6.6.0}/tests/test_basis.py +0 -0
  198. {docpull-6.5.4 → docpull-6.6.0}/tests/test_benchmark.py +0 -0
  199. {docpull-6.5.4 → docpull-6.6.0}/tests/test_cache_conditional_get.py +0 -0
  200. {docpull-6.5.4 → docpull-6.6.0}/tests/test_chunking.py +0 -0
  201. {docpull-6.5.4 → docpull-6.6.0}/tests/test_claim_audit_script.py +0 -0
  202. {docpull-6.5.4 → docpull-6.6.0}/tests/test_cli.py +0 -0
  203. {docpull-6.5.4 → docpull-6.6.0}/tests/test_context_ci.py +0 -0
  204. {docpull-6.5.4 → docpull-6.6.0}/tests/test_context_packs.py +0 -0
  205. {docpull-6.5.4 → docpull-6.6.0}/tests/test_conversion.py +0 -0
  206. {docpull-6.5.4 → docpull-6.6.0}/tests/test_convert_step_new.py +0 -0
  207. {docpull-6.5.4 → docpull-6.6.0}/tests/test_discovery.py +0 -0
  208. {docpull-6.5.4 → docpull-6.6.0}/tests/test_discovery_contracts.py +0 -0
  209. {docpull-6.5.4 → docpull-6.6.0}/tests/test_doctor.py +0 -0
  210. {docpull-6.5.4 → docpull-6.6.0}/tests/test_document_parse.py +0 -0
  211. {docpull-6.5.4 → docpull-6.6.0}/tests/test_document_record.py +0 -0
  212. {docpull-6.5.4 → docpull-6.6.0}/tests/test_evidence_pack.py +0 -0
  213. {docpull-6.5.4 → docpull-6.6.0}/tests/test_exports.py +0 -0
  214. {docpull-6.5.4 → docpull-6.6.0}/tests/test_extractor_ensemble.py +0 -0
  215. {docpull-6.5.4 → docpull-6.6.0}/tests/test_feed_pack.py +0 -0
  216. {docpull-6.5.4 → docpull-6.6.0}/tests/test_free_core.py +0 -0
  217. {docpull-6.5.4 → docpull-6.6.0}/tests/test_frontier_resume.py +0 -0
  218. {docpull-6.5.4 → docpull-6.6.0}/tests/test_graph.py +0 -0
  219. {docpull-6.5.4 → docpull-6.6.0}/tests/test_hosted.py +0 -0
  220. {docpull-6.5.4 → docpull-6.6.0}/tests/test_http_efficiency.py +0 -0
  221. {docpull-6.5.4 → docpull-6.6.0}/tests/test_import_efficiency.py +0 -0
  222. {docpull-6.5.4 → docpull-6.6.0}/tests/test_injection_screen.py +0 -0
  223. {docpull-6.5.4 → docpull-6.6.0}/tests/test_integration.py +0 -0
  224. {docpull-6.5.4 → docpull-6.6.0}/tests/test_integrations.py +0 -0
  225. {docpull-6.5.4 → docpull-6.6.0}/tests/test_judge.py +0 -0
  226. {docpull-6.5.4 → docpull-6.6.0}/tests/test_link_extractors.py +0 -0
  227. {docpull-6.5.4 → docpull-6.6.0}/tests/test_live_web_smoke.py +0 -0
  228. {docpull-6.5.4 → docpull-6.6.0}/tests/test_llm_extract.py +0 -0
  229. {docpull-6.5.4 → docpull-6.6.0}/tests/test_local_pack_workflows.py +0 -0
  230. {docpull-6.5.4 → docpull-6.6.0}/tests/test_local_workflows.py +0 -0
  231. {docpull-6.5.4 → docpull-6.6.0}/tests/test_mcp_tools.py +0 -0
  232. {docpull-6.5.4 → docpull-6.6.0}/tests/test_metadata_extractor.py +0 -0
  233. {docpull-6.5.4 → docpull-6.6.0}/tests/test_monitor.py +0 -0
  234. {docpull-6.5.4 → docpull-6.6.0}/tests/test_naming.py +0 -0
  235. {docpull-6.5.4 → docpull-6.6.0}/tests/test_native_integrations.py +0 -0
  236. {docpull-6.5.4 → docpull-6.6.0}/tests/test_openapi_pack.py +0 -0
  237. {docpull-6.5.4 → docpull-6.6.0}/tests/test_optional_extras_installed.py +0 -0
  238. {docpull-6.5.4 → docpull-6.6.0}/tests/test_optout.py +0 -0
  239. {docpull-6.5.4 → docpull-6.6.0}/tests/test_output_contract.py +0 -0
  240. {docpull-6.5.4 → docpull-6.6.0}/tests/test_outputs_e2e.py +0 -0
  241. {docpull-6.5.4 → docpull-6.6.0}/tests/test_pack_server.py +0 -0
  242. {docpull-6.5.4 → docpull-6.6.0}/tests/test_pack_tools.py +0 -0
  243. {docpull-6.5.4 → docpull-6.6.0}/tests/test_parallel_workflows.py +0 -0
  244. {docpull-6.5.4 → docpull-6.6.0}/tests/test_parity_workflows.py +0 -0
  245. {docpull-6.5.4 → docpull-6.6.0}/tests/test_passk.py +0 -0
  246. {docpull-6.5.4 → docpull-6.6.0}/tests/test_pipeline.py +0 -0
  247. {docpull-6.5.4 → docpull-6.6.0}/tests/test_policy.py +0 -0
  248. {docpull-6.5.4 → docpull-6.6.0}/tests/test_policy_cli.py +0 -0
  249. {docpull-6.5.4 → docpull-6.6.0}/tests/test_provenance.py +0 -0
  250. {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_adapters.py +0 -0
  251. {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_cli.py +0 -0
  252. {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_keys.py +0 -0
  253. {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_probes.py +0 -0
  254. {docpull-6.5.4 → docpull-6.6.0}/tests/test_public_feature_smoke.py +0 -0
  255. {docpull-6.5.4 → docpull-6.6.0}/tests/test_qa_judge.py +0 -0
  256. {docpull-6.5.4 → docpull-6.6.0}/tests/test_rate_limiter.py +0 -0
  257. {docpull-6.5.4 → docpull-6.6.0}/tests/test_real_site_regressions.py +0 -0
  258. {docpull-6.5.4 → docpull-6.6.0}/tests/test_relationship_workflow.py +0 -0
  259. {docpull-6.5.4 → docpull-6.6.0}/tests/test_release_a_plus_check_script.py +0 -0
  260. {docpull-6.5.4 → docpull-6.6.0}/tests/test_rendering.py +0 -0
  261. {docpull-6.5.4 → docpull-6.6.0}/tests/test_reproducible_release_build.py +0 -0
  262. {docpull-6.5.4 → docpull-6.6.0}/tests/test_save_ndjson.py +0 -0
  263. {docpull-6.5.4 → docpull-6.6.0}/tests/test_save_sqlite.py +0 -0
  264. {docpull-6.5.4 → docpull-6.6.0}/tests/test_security_hardening.py +0 -0
  265. {docpull-6.5.4 → docpull-6.6.0}/tests/test_share.py +0 -0
  266. {docpull-6.5.4 → docpull-6.6.0}/tests/test_source_scoring.py +0 -0
  267. {docpull-6.5.4 → docpull-6.6.0}/tests/test_special_cases.py +0 -0
  268. {docpull-6.5.4 → docpull-6.6.0}/tests/test_surface_contract.py +0 -0
  269. {docpull-6.5.4 → docpull-6.6.0}/tests/test_time_utils.py +0 -0
  270. {docpull-6.5.4 → docpull-6.6.0}/tests/test_token_metrics.py +0 -0
  271. {docpull-6.5.4 → docpull-6.6.0}/tests/test_trafilatura_extractor.py +0 -0
  272. {docpull-6.5.4 → docpull-6.6.0}/tests/test_typed_knowledge_packs.py +0 -0
  273. {docpull-6.5.4 → docpull-6.6.0}/tests/test_warc.py +0 -0
  274. {docpull-6.5.4 → docpull-6.6.0}/tests/test_website_snapshot.py +0 -0
  275. {docpull-6.5.4 → docpull-6.6.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.5.4
3
+ Version: 6.6.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>
@@ -50,7 +50,7 @@ Requires-Dist: regex>=2024.11.6
50
50
  Requires-Dist: rich>=13.0.0
51
51
  Requires-Dist: pyyaml>=6.0
52
52
  Requires-Dist: pydantic>=2.0
53
- Requires-Dist: urllib3>=2.7.0
53
+ Requires-Dist: urllib3>=2.8.0
54
54
  Provides-Extra: proxy
55
55
  Requires-Dist: aiohttp-socks>=0.11.0; extra == "proxy"
56
56
  Provides-Extra: normalize
@@ -63,7 +63,7 @@ Provides-Extra: mcp
63
63
  Requires-Dist: cryptography>=50.0.0; extra == "mcp"
64
64
  Requires-Dist: mcp<2,>=1.28.1; extra == "mcp"
65
65
  Requires-Dist: pydantic-settings>=2.14.2; extra == "mcp"
66
- Requires-Dist: pyjwt>=2.13.0; extra == "mcp"
66
+ Requires-Dist: pyjwt>=2.15.1; extra == "mcp"
67
67
  Requires-Dist: python-multipart>=0.0.27; extra == "mcp"
68
68
  Requires-Dist: starlette>=1.3.1; extra == "mcp"
69
69
  Provides-Extra: serve
@@ -71,17 +71,17 @@ Requires-Dist: uvicorn>=0.30.0; extra == "serve"
71
71
  Provides-Extra: parquet
72
72
  Requires-Dist: pyarrow>=16.0.0; extra == "parquet"
73
73
  Provides-Extra: markitdown
74
- Requires-Dist: pypdf>=6.15.0; extra == "markitdown"
74
+ Requires-Dist: pypdf>=6.20.0; extra == "markitdown"
75
75
  Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "markitdown"
76
76
  Provides-Extra: unstructured
77
- Requires-Dist: pypdf>=6.15.0; extra == "unstructured"
78
- Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "unstructured"
77
+ Requires-Dist: pypdf>=6.20.0; extra == "unstructured"
78
+ Requires-Dist: unstructured[all-docs]>=0.24.1; (python_version >= "3.11" and python_version < "3.14") and extra == "unstructured"
79
79
  Provides-Extra: pdf
80
- Requires-Dist: pypdf>=6.15.0; extra == "pdf"
80
+ Requires-Dist: pypdf>=6.20.0; extra == "pdf"
81
81
  Provides-Extra: parse
82
- Requires-Dist: pypdf>=6.15.0; extra == "parse"
82
+ Requires-Dist: pypdf>=6.20.0; extra == "parse"
83
83
  Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "parse"
84
- Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "parse"
84
+ Requires-Dist: unstructured[all-docs]>=0.24.1; (python_version >= "3.11" and python_version < "3.14") and extra == "parse"
85
85
  Provides-Extra: presidio
86
86
  Requires-Dist: presidio-analyzer>=2.2.0; extra == "presidio"
87
87
  Provides-Extra: provenance
@@ -98,14 +98,14 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
98
98
  Requires-Dist: cryptography>=50.0.0; extra == "all"
99
99
  Requires-Dist: mcp<2,>=1.28.1; extra == "all"
100
100
  Requires-Dist: pydantic-settings>=2.14.2; extra == "all"
101
- Requires-Dist: pyjwt>=2.13.0; extra == "all"
101
+ Requires-Dist: pyjwt>=2.15.1; extra == "all"
102
102
  Requires-Dist: python-multipart>=0.0.27; extra == "all"
103
103
  Requires-Dist: starlette>=1.3.1; extra == "all"
104
104
  Requires-Dist: uvicorn>=0.30.0; extra == "all"
105
105
  Requires-Dist: pyarrow>=16.0.0; extra == "all"
106
- Requires-Dist: pypdf>=6.15.0; extra == "all"
106
+ Requires-Dist: pypdf>=6.20.0; extra == "all"
107
107
  Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "all"
108
- Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "all"
108
+ Requires-Dist: unstructured[all-docs]>=0.24.1; (python_version >= "3.11" and python_version < "3.14") and extra == "all"
109
109
  Requires-Dist: presidio-analyzer>=2.2.0; extra == "all"
110
110
  Requires-Dist: e2b>=2.0.0; extra == "all"
111
111
  Provides-Extra: dev
@@ -151,6 +151,7 @@ pip install docpull
151
151
  docpull init stripe-docs
152
152
  docpull add https://docs.stripe.com
153
153
  docpull sync
154
+ docpull query "retry policy" --max-tokens 4000
154
155
  docpull diff
155
156
  docpull export context-pack --target cursor
156
157
  ```
@@ -159,6 +160,11 @@ The project stores declared sources in `docpull.yaml` and resolved inputs in
159
160
  `.docpull/context.lock.json`. Later syncs produce a hash-based diff while preserving
160
161
  source URLs, content hashes, run IDs, citations, and export metadata.
161
162
 
163
+ `docpull query` retrieves deduplicated passages from locked archives with version
164
+ filters, a response budget, and exact source spans. The same query is available
165
+ through the Python SDK and MCP. See [Query locked context](docs/context-dependencies.md#query-locked-context)
166
+ for budget semantics and queries across projects.
167
+
162
168
  ![DocPull project diff showing changed pages, local semantic categories, and zero failed URLs](docs/launch-assets/docpull-project-diff-demo.png)
163
169
 
164
170
  No account or paid API is required for this path. Direct fetching, discovery,
@@ -25,6 +25,7 @@ pip install docpull
25
25
  docpull init stripe-docs
26
26
  docpull add https://docs.stripe.com
27
27
  docpull sync
28
+ docpull query "retry policy" --max-tokens 4000
28
29
  docpull diff
29
30
  docpull export context-pack --target cursor
30
31
  ```
@@ -33,6 +34,11 @@ The project stores declared sources in `docpull.yaml` and resolved inputs in
33
34
  `.docpull/context.lock.json`. Later syncs produce a hash-based diff while preserving
34
35
  source URLs, content hashes, run IDs, citations, and export metadata.
35
36
 
37
+ `docpull query` retrieves deduplicated passages from locked archives with version
38
+ filters, a response budget, and exact source spans. The same query is available
39
+ through the Python SDK and MCP. See [Query locked context](docs/context-dependencies.md#query-locked-context)
40
+ for budget semantics and queries across projects.
41
+
36
42
  ![DocPull project diff showing changed pages, local semantic categories, and zero failed URLs](docs/launch-assets/docpull-project-diff-demo.png)
37
43
 
38
44
  No account or paid API is required for this path. Direct fetching, discovery,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "6.5.4"
7
+ version = "6.6.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"}
@@ -92,7 +92,7 @@ dependencies = [
92
92
  "rich>=13.0.0",
93
93
  "pyyaml>=6.0",
94
94
  "pydantic>=2.0",
95
- "urllib3>=2.7.0", # Security floor for transitive HTTP clients; Renovate raises as needed
95
+ "urllib3>=2.8.0", # Security floor for transitive HTTP clients; Renovate raises as needed
96
96
  ]
97
97
 
98
98
  [project.optional-dependencies]
@@ -112,7 +112,7 @@ mcp = [
112
112
  "cryptography>=50.0.0",
113
113
  "mcp>=1.28.1,<2",
114
114
  "pydantic-settings>=2.14.2",
115
- "pyjwt>=2.13.0",
115
+ "pyjwt>=2.15.1",
116
116
  "python-multipart>=0.0.27",
117
117
  "starlette>=1.3.1",
118
118
  ]
@@ -123,20 +123,20 @@ parquet = [
123
123
  "pyarrow>=16.0.0",
124
124
  ]
125
125
  markitdown = [
126
- "pypdf>=6.15.0",
126
+ "pypdf>=6.20.0",
127
127
  "markitdown[docx,pdf]>=0.1.0",
128
128
  ]
129
129
  unstructured = [
130
- "pypdf>=6.15.0",
131
- "unstructured[all-docs]>=0.16.0",
130
+ "pypdf>=6.20.0",
131
+ "unstructured[all-docs]>=0.24.1; python_version >= '3.11' and python_version < '3.14'",
132
132
  ]
133
133
  pdf = [
134
- "pypdf>=6.15.0",
134
+ "pypdf>=6.20.0",
135
135
  ]
136
136
  parse = [
137
- "pypdf>=6.15.0",
137
+ "pypdf>=6.20.0",
138
138
  "markitdown[docx,pdf]>=0.1.0",
139
- "unstructured[all-docs]>=0.16.0",
139
+ "unstructured[all-docs]>=0.24.1; python_version >= '3.11' and python_version < '3.14'",
140
140
  ]
141
141
  presidio = [
142
142
  "presidio-analyzer>=2.2.0",
@@ -158,14 +158,14 @@ all = [
158
158
  "cryptography>=50.0.0",
159
159
  "mcp>=1.28.1,<2",
160
160
  "pydantic-settings>=2.14.2",
161
- "pyjwt>=2.13.0",
161
+ "pyjwt>=2.15.1",
162
162
  "python-multipart>=0.0.27",
163
163
  "starlette>=1.3.1",
164
164
  "uvicorn>=0.30.0",
165
165
  "pyarrow>=16.0.0",
166
- "pypdf>=6.15.0",
166
+ "pypdf>=6.20.0",
167
167
  "markitdown[docx,pdf]>=0.1.0",
168
- "unstructured[all-docs]>=0.16.0",
168
+ "unstructured[all-docs]>=0.24.1; python_version >= '3.11' and python_version < '3.14'",
169
169
  "presidio-analyzer>=2.2.0",
170
170
  "e2b>=2.0.0",
171
171
  ]
@@ -208,7 +208,13 @@ include = ["docpull*"]
208
208
  docpull = ["py.typed", "fixtures/*.json", "schemas/*.schema.json"]
209
209
 
210
210
  [tool.uv]
211
- constraint-dependencies = ["h2>=4.4.1"]
211
+ constraint-dependencies = [
212
+ "h2>=4.4.1",
213
+ "multidict>=6.9.1",
214
+ "accelerate>=1.15.0",
215
+ "soupsieve>=2.9",
216
+ "virtualenv>=21.7.13",
217
+ ]
212
218
 
213
219
  [tool.ruff]
214
220
  line-length = 110
@@ -15,9 +15,11 @@ from typing import TYPE_CHECKING, Any
15
15
 
16
16
  from .surface import PUBLIC_SDK_EXPORTS
17
17
 
18
- __version__ = "6.5.4"
18
+ __version__ = "6.6.0"
19
19
 
20
20
  _LAZY_EXPORTS: dict[str, tuple[str, str]] = {
21
+ "query_context": (".context_query", "query_context"),
22
+ "ContextQueryError": (".context_query", "ContextQueryError"),
21
23
  **{
22
24
  name: (".context_packs", name)
23
25
  for name in (
@@ -207,6 +209,7 @@ if TYPE_CHECKING:
207
209
  capture_screenshot_pack,
208
210
  validate_website_snapshot_pack,
209
211
  )
212
+ from .context_query import ContextQueryError, query_context
210
213
  from .contracts import (
211
214
  ArtifactManifest,
212
215
  ChangeEvent,
@@ -1623,6 +1623,10 @@ def main(argv: list[str] | None = None) -> int:
1623
1623
  from .project import run_install_cli
1624
1624
 
1625
1625
  return run_install_cli(raw_argv[1:])
1626
+ if raw_argv and raw_argv[0] == "query":
1627
+ from .context_query import run_query_cli
1628
+
1629
+ return run_query_cli(raw_argv[1:])
1626
1630
  if raw_argv and raw_argv[0] == "deps":
1627
1631
  from .project import run_deps_cli
1628
1632
 
@@ -0,0 +1,305 @@
1
+ """Offline, budgeted passage retrieval from verified project lock snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import re
9
+ import sys
10
+ import tempfile
11
+ from collections.abc import Iterator, Sequence
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .frozen import FrozenArtifactError, stage_run_artifact
16
+ from .pack_tools import _search_score, _term_count
17
+ from .project import (
18
+ ProjectError,
19
+ _content_hash_summary,
20
+ _context_lock_hash,
21
+ _read_context_lock,
22
+ _read_json,
23
+ _read_jsonl,
24
+ _safe_run_id,
25
+ _validate_context_lock,
26
+ find_project_root,
27
+ load_project_config,
28
+ project_paths,
29
+ )
30
+
31
+ _STOPWORDS = frozenset(
32
+ [
33
+ "a",
34
+ "an",
35
+ "the",
36
+ "how",
37
+ "to",
38
+ "do",
39
+ "does",
40
+ "is",
41
+ "are",
42
+ "of",
43
+ "for",
44
+ "in",
45
+ "on",
46
+ "and",
47
+ "with",
48
+ "what",
49
+ "i",
50
+ "can",
51
+ ]
52
+ )
53
+
54
+
55
+ class ContextQueryError(ValueError):
56
+ """Invalid query input or unverifiable locked evidence."""
57
+
58
+
59
+ def serialize_query_result(payload: dict[str, Any]) -> str:
60
+ """Return the canonical JSON representation covered by the token budget."""
61
+ return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
62
+
63
+
64
+ def _measure(payload: dict[str, Any]) -> int:
65
+ # One UTF-8 byte per budget token is a conservative bound for byte tokenizers.
66
+ # Unlike optional tokenizer initialization, this cannot download model data.
67
+ while True:
68
+ size = len(serialize_query_result(payload).encode("utf-8"))
69
+ if payload["budget"]["used_tokens"] == size:
70
+ return size
71
+ payload["budget"]["used_tokens"] = size
72
+
73
+
74
+ def _spans(content: str) -> Iterator[tuple[int, int]]:
75
+ """Keep paragraphs and fenced code blocks whole, with exact character offsets."""
76
+ start = offset = 0
77
+ fence = ""
78
+ for line in content.splitlines(keepends=True):
79
+ marker = re.match(r"^ {0,3}(`{3,}|~{3,})", line)
80
+ if fence:
81
+ if re.fullmatch(r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*", line):
82
+ fence = ""
83
+ yield start, offset + len(line)
84
+ start = offset + len(line)
85
+ elif marker:
86
+ if start < offset:
87
+ yield start, offset
88
+ start = offset
89
+ fence = marker.group(1)
90
+ elif not line.strip():
91
+ if start < offset:
92
+ yield start, offset
93
+ start = offset + len(line)
94
+ offset += len(line)
95
+ if start < offset:
96
+ yield start, offset
97
+
98
+
99
+ def _filters(values: list[str] | None, name: str) -> set[str]:
100
+ if values is None:
101
+ return set()
102
+ if not isinstance(values, list) or not all(isinstance(v, str) and v.strip() for v in values):
103
+ raise ContextQueryError(f"{name} must be a list of non-empty strings")
104
+ return {v.strip() for v in values}
105
+
106
+
107
+ def _locked_records(
108
+ root: Path, artifact_dirs: list[Path], pack_versions: set[str]
109
+ ) -> Iterator[tuple[dict[str, Any], int, dict[str, Any]]]:
110
+ config = load_project_config(root)
111
+ paths = project_paths(root)
112
+ lock = _read_context_lock(paths.context_lock)
113
+ if not lock:
114
+ raise ContextQueryError(f"No context lockfile in {root}; run `docpull sync` first")
115
+ _validate_context_lock(config, lock)
116
+ if lock.get("lock_hash") != _context_lock_hash(lock) or lock.get("project") != config.name:
117
+ raise ContextQueryError(f"Invalid context lockfile in {root}")
118
+ if not isinstance(lock.get("run_id"), str):
119
+ raise ContextQueryError(f"No run pinned in {root}; run `docpull sync` first")
120
+ run_id = _safe_run_id(lock["run_id"])
121
+ if pack_versions and run_id not in pack_versions:
122
+ return
123
+ artifact = lock.get("artifact")
124
+ if not isinstance(artifact, dict) or not re.fullmatch(r"[0-9a-f]{64}", str(artifact.get("sha256"))):
125
+ raise ContextQueryError(f"No frozen artifact pinned in {root}; run `docpull sync` first")
126
+ stores = [paths.state / "artifacts", *artifact_dirs]
127
+ store = next((p for p in stores if (p / f"{artifact['sha256']}.zip").is_file()), stores[0])
128
+ with tempfile.TemporaryDirectory(prefix="docpull-query-") as temporary:
129
+ staged = Path(temporary)
130
+ stage_run_artifact(artifact, store, staged)
131
+ run = _read_json(staged / "run.json")
132
+ if not isinstance(run, dict) or run.get("run_id") != run_id or run.get("project") != config.name:
133
+ raise ContextQueryError("Frozen artifact does not match the locked run")
134
+ records = _read_jsonl(staged / "documents.ndjson")
135
+ if sorted(str(row["content_hash"]) for row in records if row.get("content_hash")) != lock.get(
136
+ "content_hashes"
137
+ ) or _content_hash_summary(records) != lock.get("content_hash_summary"):
138
+ raise ContextQueryError("Frozen artifact content does not match the lockfile")
139
+ for index, record in enumerate(records, 1):
140
+ if not isinstance(record.get("url"), str) or not record["url"].strip():
141
+ raise ContextQueryError(f"Missing source URL in locked record {index}")
142
+ content = record.get("content")
143
+ if not isinstance(content, str) or hashlib.sha256(
144
+ content.encode("utf-8")
145
+ ).hexdigest() != record.get("content_hash"):
146
+ raise ContextQueryError(f"Invalid content hash in locked record {index}")
147
+ yield lock, index, record
148
+
149
+
150
+ def query_context(
151
+ query: str,
152
+ *,
153
+ project_roots: Sequence[Path | str] | None = None,
154
+ max_tokens: int = 4000,
155
+ versions: list[str] | None = None,
156
+ pack_versions: list[str] | None = None,
157
+ sources: list[str] | None = None,
158
+ limit: int = 10,
159
+ artifact_dirs: Sequence[Path | str] | None = None,
160
+ ) -> dict[str, Any]:
161
+ """Retrieve cited passages from locked archives without acquiring or changing sources.
162
+
163
+ ``versions`` matches explicit document ``metadata.version`` values; unknown
164
+ versions never match. ``pack_versions`` matches exact locked run IDs.
165
+ The complete canonical JSON result fits ``max_tokens`` using a conservative
166
+ UTF-8 byte bound, including citations and budget metadata. Oversized passages
167
+ are omitted whole, so code blocks and source offsets remain intact.
168
+ """
169
+ if not isinstance(query, str) or not query.strip():
170
+ raise ContextQueryError("query must be a non-empty string")
171
+ if type(max_tokens) is not int or max_tokens < 256:
172
+ raise ContextQueryError("max_tokens must be an integer of at least 256")
173
+ if type(limit) is not int or not 1 <= limit <= 100:
174
+ raise ContextQueryError("limit must be an integer between 1 and 100")
175
+ for values, name in ((project_roots, "project_roots"), (artifact_dirs, "artifact_dirs")):
176
+ if values is not None and (
177
+ isinstance(values, (str, bytes))
178
+ or not isinstance(values, Sequence)
179
+ or not all(isinstance(p, (str, Path)) and str(p).strip() for p in values)
180
+ ):
181
+ raise ContextQueryError(f"{name} must be a sequence of non-empty paths")
182
+ source_versions = _filters(versions, "versions")
183
+ runs = _filters(pack_versions, "pack_versions")
184
+ source_names = _filters(sources, "sources")
185
+ terms = sorted(set(re.findall(r"[^\W_]+(?:[_-][^\W_]+)*", query.casefold())) - _STOPWORDS)
186
+ if not terms:
187
+ raise ContextQueryError("query must contain a searchable term")
188
+ groups: dict[str, dict[str, Any]] = {}
189
+ try:
190
+ roots = project_roots if project_roots is not None else [find_project_root(Path.cwd())]
191
+ if not roots:
192
+ raise ContextQueryError("project_roots must contain at least one project")
193
+ stores = [Path(p).expanduser().resolve() for p in artifact_dirs or []]
194
+ for root in sorted({Path(p).expanduser().resolve() for p in roots}):
195
+ for lock, record_index, record in _locked_records(root, stores, runs):
196
+ metadata = record.get("metadata") or {}
197
+ version = metadata.get("version") if isinstance(metadata, dict) else None
198
+ source = metadata.get("docpull_project_source") if isinstance(metadata, dict) else None
199
+ if not isinstance(version, str):
200
+ version = None
201
+ if not isinstance(source, str):
202
+ source = None
203
+ if source_versions and version not in source_versions:
204
+ continue
205
+ if source_names and source not in source_names:
206
+ continue
207
+ content = record["content"]
208
+ for start, end in _spans(content):
209
+ # Trim outside whitespace only; never rewrite captured passage text.
210
+ raw = content[start:end]
211
+ start += len(raw) - len(raw.lstrip())
212
+ end -= len(raw) - len(raw.rstrip())
213
+ text = content[start:end]
214
+ if not text or not any(_term_count(text, term) for term in terms):
215
+ continue
216
+ score, _ = _search_score(
217
+ query_terms=terms,
218
+ phrase=query.strip().lower(),
219
+ title=str(record.get("title") or ""),
220
+ heading="",
221
+ url=str(record.get("url") or ""),
222
+ content=text,
223
+ )
224
+ digest = lock["artifact"]["sha256"]
225
+ identity = f"{digest}:{record_index}:{start}:{end}"
226
+ citation = {
227
+ "id": "ctx_" + hashlib.sha256(identity.encode()).hexdigest()[:24],
228
+ "pack": lock["project"],
229
+ "pack_version": lock["run_id"],
230
+ "artifact_sha256": digest,
231
+ "source": source,
232
+ "version": version,
233
+ "url": record["url"],
234
+ "document_id": record.get("document_id"),
235
+ "record_index": record_index,
236
+ "content_hash": record["content_hash"],
237
+ "char_start": start,
238
+ "char_end": end,
239
+ "line_start": content.count("\n", 0, start) + 1,
240
+ "line_end": content.count("\n", 0, end - 1) + 1,
241
+ }
242
+ group = groups.setdefault(text, {"score": score, "text": text, "citations": {}})
243
+ group["score"] = max(group["score"], score)
244
+ group["citations"][citation["id"]] = citation
245
+ except (ProjectError, FrozenArtifactError, OSError, UnicodeError) as err:
246
+ raise ContextQueryError(str(err)) from err
247
+ payload: dict[str, Any] = {
248
+ "schema_version": 1,
249
+ "status": "ok",
250
+ "reason": None,
251
+ "budget": {"max_tokens": max_tokens, "used_tokens": 0, "method": "utf8_bytes_upper_bound"},
252
+ "results": [],
253
+ "omitted_passages": 0,
254
+ }
255
+ ranked = sorted(groups.values(), key=lambda g: (-g["score"], g["text"]))
256
+ deferred: list[tuple[dict[str, Any], list[dict[str, Any]]]] = []
257
+ payload["omitted_passages"] = len(ranked)
258
+ for group in ranked:
259
+ citations = sorted(group["citations"].values(), key=lambda c: c["id"])
260
+ result = {"text": group["text"], "citations": citations[:1], "omitted_citations": len(citations) - 1}
261
+ if len(payload["results"]) >= limit:
262
+ break
263
+ payload["results"].append(result)
264
+ payload["omitted_passages"] -= 1
265
+ if _measure(payload) > max_tokens:
266
+ payload["results"].pop()
267
+ payload["omitted_passages"] += 1
268
+ else:
269
+ deferred.append((result, citations[1:]))
270
+ # Spend remaining space on duplicate provenance after selecting distinct passages.
271
+ for result, citations in deferred:
272
+ for citation in citations:
273
+ result["citations"].append(citation)
274
+ result["omitted_citations"] -= 1
275
+ if _measure(payload) > max_tokens:
276
+ result["citations"].pop()
277
+ result["omitted_citations"] += 1
278
+ if not payload["results"]:
279
+ payload["status"] = "insufficient_evidence"
280
+ payload["reason"] = "budget_exhausted" if ranked else "no_matches"
281
+ if _measure(payload) > max_tokens:
282
+ raise ContextQueryError("max_tokens is too small for response metadata")
283
+ return payload
284
+
285
+
286
+ def run_query_cli(argv: list[str] | None = None) -> int:
287
+ parser = argparse.ArgumentParser(
288
+ prog="docpull query", description="Query verified locked context offline"
289
+ )
290
+ parser.add_argument("query")
291
+ parser.add_argument("--project", action="append", type=Path, dest="project_roots")
292
+ parser.add_argument("--max-tokens", type=int, default=4000)
293
+ parser.add_argument("--version", action="append", dest="versions", help="Exact metadata.version filter")
294
+ parser.add_argument("--pack-version", action="append", dest="pack_versions", help="Exact locked run ID")
295
+ parser.add_argument("--source", action="append", dest="sources", help="Configured source name")
296
+ parser.add_argument("--limit", type=int, default=10)
297
+ parser.add_argument("--artifact-dir", action="append", type=Path, dest="artifact_dirs")
298
+ args = parser.parse_args(argv)
299
+ try:
300
+ payload = query_context(**vars(args))
301
+ except ContextQueryError as err:
302
+ print(f"Context query error: {err}", file=sys.stderr)
303
+ return 1
304
+ print(serialize_query_result(payload))
305
+ return 0
@@ -926,6 +926,11 @@ def _parse_markitdown(path: Path) -> tuple[str, dict[str, Any]]:
926
926
 
927
927
 
928
928
  def _parse_unstructured(path: Path) -> tuple[str, dict[str, Any]]:
929
+ if not (3, 11) <= sys.version_info[:2] < (3, 14):
930
+ raise DocumentParseError(
931
+ "The patched Unstructured backend requires Python 3.11–3.13. "
932
+ "Use the pypdf, markitdown, or text backend on this interpreter."
933
+ )
929
934
  try:
930
935
  module = importlib.import_module("unstructured.partition.auto")
931
936
  except ImportError as err:
@@ -0,0 +1,123 @@
1
+ """Content-addressed local archives for frozen project installation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import shutil
9
+ import stat
10
+ import tempfile
11
+ import zipfile
12
+ from pathlib import Path, PurePosixPath
13
+ from typing import Any, BinaryIO
14
+
15
+
16
+ class FrozenArtifactError(RuntimeError):
17
+ """An artifact is missing, invalid, or unsafe to restore."""
18
+
19
+
20
+ def _digest(stream: BinaryIO) -> str:
21
+ digest = hashlib.sha256()
22
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
23
+ digest.update(chunk)
24
+ return digest.hexdigest()
25
+
26
+
27
+ def run_files(root: Path) -> list[Path]:
28
+ """List every retained run file without following symbolic links."""
29
+ files = []
30
+ for path in sorted(root.rglob("*")):
31
+ if path.is_symlink():
32
+ raise FrozenArtifactError(f"Artifact contains a symbolic link: {path}")
33
+ if path.is_file():
34
+ files.append(path)
35
+ elif not path.is_dir():
36
+ raise FrozenArtifactError(f"Artifact contains a non-regular file: {path}")
37
+ return files
38
+
39
+
40
+ def run_fingerprints(root: Path) -> dict[str, str]:
41
+ """Hash evidence files without loading the corpus into memory."""
42
+ result = {}
43
+ for path in run_files(root):
44
+ if path.name == "run.json" and path.parent == root:
45
+ continue
46
+ with path.open("rb") as stream:
47
+ result[path.relative_to(root).as_posix()] = _digest(stream)
48
+ return result
49
+
50
+
51
+ def create_run_artifact(run_dir: Path, artifact_dir: Path) -> dict[str, Any]:
52
+ """Save the completed run as a ZIP addressed by its SHA-256 digest."""
53
+ artifact_dir.mkdir(parents=True, exist_ok=True)
54
+ with tempfile.TemporaryDirectory(prefix=".snapshot-", dir=artifact_dir) as temporary:
55
+ archive = Path(temporary) / "run.zip"
56
+ with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED) as bundle:
57
+ for path in run_files(run_dir):
58
+ info = zipfile.ZipInfo(path.relative_to(run_dir).as_posix())
59
+ info.compress_type = zipfile.ZIP_DEFLATED
60
+ info.external_attr = (stat.S_IFREG | 0o600) << 16
61
+ with path.open("rb") as source, bundle.open(info, "w", force_zip64=True) as target:
62
+ shutil.copyfileobj(source, target)
63
+ archive.chmod(0o600)
64
+ with archive.open("rb") as stream:
65
+ digest = _digest(stream)
66
+ destination = artifact_dir / f"{digest}.zip"
67
+ if destination.exists():
68
+ with destination.open("rb") as stream:
69
+ if _digest(stream) != digest:
70
+ raise FrozenArtifactError(f"Artifact digest mismatch: {destination}")
71
+ else:
72
+ os.replace(archive, destination)
73
+ return {"schema_version": 1, "format": "zip", "sha256": digest}
74
+
75
+
76
+ def stage_run_artifact(artifact: Any, artifact_dir: Path, destination: Path) -> None:
77
+ """Verify pinned bytes before safely unpacking into a private staging directory."""
78
+ if (
79
+ not isinstance(artifact, dict)
80
+ or artifact.get("schema_version") != 1
81
+ or artifact.get("format") != "zip"
82
+ or not isinstance(artifact.get("sha256"), str)
83
+ or not re.fullmatch(r"[0-9a-f]{64}", artifact["sha256"])
84
+ ):
85
+ raise FrozenArtifactError(
86
+ "Lockfile has no supported frozen artifact. Run `docpull sync` on the source machine first."
87
+ )
88
+ digest = artifact["sha256"]
89
+ archive = artifact_dir / f"{digest}.zip"
90
+ try:
91
+ with archive.open("rb") as stream:
92
+ if _digest(stream) != digest:
93
+ raise FrozenArtifactError(f"Artifact digest mismatch: {archive}")
94
+ stream.seek(0)
95
+ with zipfile.ZipFile(stream) as bundle:
96
+ seen: set[str] = set()
97
+ for info in bundle.infolist():
98
+ name = info.filename
99
+ path = PurePosixPath(name)
100
+ mode = info.external_attr >> 16
101
+ if (
102
+ not name
103
+ or info.orig_filename != name
104
+ or path.is_absolute()
105
+ or "\\" in name
106
+ or ":" in name
107
+ or any(part in {"", ".", ".."} for part in name.split("/"))
108
+ or name in seen
109
+ or (stat.S_IFMT(mode) not in {0, stat.S_IFREG})
110
+ ):
111
+ raise FrozenArtifactError(f"Unsafe artifact entry: {name}")
112
+ seen.add(name)
113
+ target = destination.joinpath(*path.parts)
114
+ target.parent.mkdir(parents=True, exist_ok=True)
115
+ with bundle.open(info) as source, target.open("xb") as output:
116
+ shutil.copyfileobj(source, output)
117
+ target.chmod(0o600)
118
+ except FileNotFoundError as err:
119
+ raise FrozenArtifactError(f"Missing frozen artifact: {archive}") from err
120
+ except (OSError, ValueError, zipfile.BadZipFile, RuntimeError, NotImplementedError) as err:
121
+ if isinstance(err, FrozenArtifactError):
122
+ raise
123
+ raise FrozenArtifactError(f"Cannot restore frozen artifact {archive}: {err}") from err