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.
- {docpull-6.5.4/src/docpull.egg-info → docpull-6.6.0}/PKG-INFO +18 -12
- {docpull-6.5.4 → docpull-6.6.0}/README.md +6 -0
- {docpull-6.5.4 → docpull-6.6.0}/pyproject.toml +19 -13
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/__init__.py +4 -1
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cli.py +4 -0
- docpull-6.6.0/src/docpull/context_query.py +305 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/document_parse.py +5 -0
- docpull-6.6.0/src/docpull/frozen.py +123 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/server.py +96 -2
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/project.py +123 -17
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/surface.py +4 -0
- {docpull-6.5.4 → docpull-6.6.0/src/docpull.egg-info}/PKG-INFO +18 -12
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/SOURCES.txt +5 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/requires.txt +17 -11
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_ci_policy.py +2 -2
- docpull-6.6.0/tests/test_context_query.py +281 -0
- docpull-6.6.0/tests/test_frozen.py +65 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_mcp_server.py +20 -1
- docpull-6.6.0/tests/test_parser_dependency_support.py +54 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_project.py +181 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_real_feature_smoke_script.py +41 -0
- {docpull-6.5.4 → docpull-6.6.0}/LICENSE +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/setup.cfg +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/__main__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/accounting.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/acquisition_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/agent_publish.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/auth_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/basis.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/benchmark.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/frontier.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/manager.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/change_events.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_aliases.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_ci.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/_legacy_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/brand.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/common.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/dataset.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/feed.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/openapi.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/package.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/paper.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/policy_pack.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/product.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/relationship.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/repo.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/schema_extract.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/search.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/standards.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/styleguide.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/transcript.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/typed.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/typed_models.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/visuals.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/website.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/wiki.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/context_packs/workflow_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/contracts.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/contracts_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/article_cleanup.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/chunking.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/ensemble.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/extractor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/filings.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/markdown.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/protocols.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/special_cases.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/core/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/core/fetcher.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/composite.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/contracts.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/crawler.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/filters.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/protocols.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/discovery_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/doctor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/document_worker.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/eval_grade.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/evidence.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/evidence_pack.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/export_formats.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/exports.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/free_core.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/free_core_smoke.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/graph.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/hosted.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/client.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/protocols.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/_records.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/langchain.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/integrations/llamaindex.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/judge.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/llm_client.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/local_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/sources.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/mcp/tools.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/metadata_extractor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/config.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/document.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/events.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/profiles.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/run.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/models/schema.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/monitor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/native_integrations.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/output_contract.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pack_reader.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pack_tools.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parallel_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parity.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/parity_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/passk.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/base.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/manifest.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/convert.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/render.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_okf.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/screen.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/pipeline/steps/validate.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/policy.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/policy_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provenance.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_adapters.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_capabilities.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_keys.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/provider_probes.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/py.typed +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/redaction.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/rendering.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/artifact-manifest.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/basis.v2.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/change-event.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/citation-map.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/document.v3.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/intelligence-bundle.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/pack.v3.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/provenance.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/relationship-pack.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/rights.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/run-identity.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/website-snapshot.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/workflow-request.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/schemas/workflow-result.v1.schema.json +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/scraper.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/__init__.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/download_policy.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/injection.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/optout.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/robots.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/security/url_validator.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/server.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/share.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/skill_export.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/source_scoring.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/time_utils.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/warc.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull/workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_accounting.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_acquisition_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_archive_discovery.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_auth_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_basis.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_benchmark.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_cache_conditional_get.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_chunking.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_claim_audit_script.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_context_ci.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_context_packs.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_conversion.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_convert_step_new.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_discovery.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_discovery_contracts.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_doctor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_document_parse.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_document_record.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_evidence_pack.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_exports.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_extractor_ensemble.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_feed_pack.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_free_core.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_frontier_resume.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_graph.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_hosted.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_http_efficiency.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_import_efficiency.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_injection_screen.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_integration.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_integrations.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_judge.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_link_extractors.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_live_web_smoke.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_llm_extract.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_local_pack_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_local_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_mcp_tools.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_metadata_extractor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_monitor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_naming.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_native_integrations.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_openapi_pack.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_optional_extras_installed.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_optout.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_output_contract.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_outputs_e2e.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_pack_server.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_pack_tools.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_parallel_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_parity_workflows.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_passk.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_pipeline.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_policy.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_policy_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_provenance.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_adapters.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_cli.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_keys.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_provider_probes.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_public_feature_smoke.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_qa_judge.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_rate_limiter.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_real_site_regressions.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_relationship_workflow.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_release_a_plus_check_script.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_rendering.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_reproducible_release_build.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_save_ndjson.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_save_sqlite.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_security_hardening.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_share.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_source_scoring.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_special_cases.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_surface_contract.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_time_utils.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_token_metrics.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_trafilatura_extractor.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_typed_knowledge_packs.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_warc.py +0 -0
- {docpull-6.5.4 → docpull-6.6.0}/tests/test_website_snapshot.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
78
|
-
Requires-Dist: unstructured[all-docs]>=0.
|
|
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.
|
|
80
|
+
Requires-Dist: pypdf>=6.20.0; extra == "pdf"
|
|
81
81
|
Provides-Extra: parse
|
|
82
|
-
Requires-Dist: pypdf>=6.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|

|
|
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
|

|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
126
|
+
"pypdf>=6.20.0",
|
|
127
127
|
"markitdown[docx,pdf]>=0.1.0",
|
|
128
128
|
]
|
|
129
129
|
unstructured = [
|
|
130
|
-
"pypdf>=6.
|
|
131
|
-
"unstructured[all-docs]>=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.
|
|
134
|
+
"pypdf>=6.20.0",
|
|
135
135
|
]
|
|
136
136
|
parse = [
|
|
137
|
-
"pypdf>=6.
|
|
137
|
+
"pypdf>=6.20.0",
|
|
138
138
|
"markitdown[docx,pdf]>=0.1.0",
|
|
139
|
-
"unstructured[all-docs]>=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.
|
|
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.
|
|
166
|
+
"pypdf>=6.20.0",
|
|
167
167
|
"markitdown[docx,pdf]>=0.1.0",
|
|
168
|
-
"unstructured[all-docs]>=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 = [
|
|
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.
|
|
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
|