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