sift-engine 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {sift_engine-0.2.0/sift_engine.egg-info → sift_engine-0.3.0}/PKG-INFO +17 -5
  2. {sift_engine-0.2.0 → sift_engine-0.3.0}/README.md +14 -4
  3. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/plan.py +2 -3
  4. {sift_engine-0.2.0 → sift_engine-0.3.0}/pyproject.toml +3 -1
  5. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/__init__.py +1 -1
  6. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/cli.py +447 -12
  7. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/config.py +31 -0
  8. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/extract.py +216 -17
  9. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/fetch.py +102 -16
  10. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/integrity.py +83 -0
  11. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/mcp_server.py +722 -26
  12. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/normalize.py +32 -1
  13. sift_engine-0.3.0/sift/prove.py +242 -0
  14. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/publish.py +282 -11
  15. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/quality.py +61 -0
  16. sift_engine-0.3.0/sift/robots.py +109 -0
  17. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/__init__.py +44 -2
  18. sift_engine-0.3.0/sift/sites/cve.py +47 -0
  19. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/stripe.py +4 -0
  20. sift_engine-0.3.0/sift/sources/frontier.py +125 -0
  21. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sources/impersonate.py +76 -53
  22. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/status.py +15 -2
  23. sift_engine-0.3.0/sift/timestamp.py +145 -0
  24. sift_engine-0.3.0/sift/verify_proof.py +101 -0
  25. {sift_engine-0.2.0 → sift_engine-0.3.0/sift_engine.egg-info}/PKG-INFO +17 -5
  26. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift_engine.egg-info/SOURCES.txt +19 -0
  27. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift_engine.egg-info/requires.txt +2 -0
  28. sift_engine-0.3.0/tests/test_api_connector.py +261 -0
  29. sift_engine-0.3.0/tests/test_changelog_continuity.py +81 -0
  30. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_config.py +6 -2
  31. sift_engine-0.3.0/tests/test_content_admission.py +154 -0
  32. sift_engine-0.3.0/tests/test_discover.py +92 -0
  33. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_excludes_and_pdf.py +27 -1
  34. sift_engine-0.3.0/tests/test_extract_json.py +157 -0
  35. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_extract_strategy.py +5 -3
  36. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_fetch_browser.py +81 -0
  37. sift_engine-0.3.0/tests/test_frontier.py +90 -0
  38. sift_engine-0.3.0/tests/test_gates.py +321 -0
  39. sift_engine-0.3.0/tests/test_impersonate_rotation.py +143 -0
  40. sift_engine-0.3.0/tests/test_mcp_changed_since.py +244 -0
  41. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_mcp_server.py +2 -2
  42. sift_engine-0.3.0/tests/test_mcp_time_travel.py +231 -0
  43. sift_engine-0.3.0/tests/test_normalizer_scoping.py +124 -0
  44. sift_engine-0.3.0/tests/test_prove.py +346 -0
  45. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_publish_integrity.py +26 -0
  46. sift_engine-0.3.0/tests/test_robots.py +111 -0
  47. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_tiered_transport.py +14 -2
  48. sift_engine-0.3.0/tests/test_timestamp.py +135 -0
  49. sift_engine-0.2.0/tests/test_gates.py +0 -135
  50. {sift_engine-0.2.0 → sift_engine-0.3.0}/LICENSE +0 -0
  51. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/__init__.py +0 -0
  52. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/__init__.py +0 -0
  53. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/agent.py +0 -0
  54. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/judge.py +0 -0
  55. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/questions.py +0 -0
  56. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/report.py +0 -0
  57. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/runner.py +0 -0
  58. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/agent_loop/tools.py +0 -0
  59. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/__init__.py +0 -0
  60. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/aggregate_report.py +0 -0
  61. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/drift.py +0 -0
  62. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/fixtures/__init__.py +0 -0
  63. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/fixtures/sites.py +0 -0
  64. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/full_suite.py +0 -0
  65. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/__init__.py +0 -0
  66. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/commit.py +0 -0
  67. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/extract.py +0 -0
  68. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/fetch.py +0 -0
  69. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/mcp.py +0 -0
  70. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/publish.py +0 -0
  71. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/per_stage/seed.py +0 -0
  72. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/report.py +0 -0
  73. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/runner.py +0 -0
  74. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/scoring/__init__.py +0 -0
  75. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/scoring/fidelity.py +0 -0
  76. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/scoring/structural.py +0 -0
  77. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/bench/scoring/use_case.py +0 -0
  78. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/cli.py +0 -0
  79. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/determinism.py +0 -0
  80. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/efficiency.py +0 -0
  81. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/facts_coverage.py +0 -0
  82. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/facts_validation.py +0 -0
  83. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/llm_judge.py +0 -0
  84. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/performance.py +0 -0
  85. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/sampler.py +0 -0
  86. {sift_engine-0.2.0 → sift_engine-0.3.0}/evals/structural.py +0 -0
  87. {sift_engine-0.2.0 → sift_engine-0.3.0}/setup.cfg +0 -0
  88. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/_io.py +0 -0
  89. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/agent_surface.py +0 -0
  90. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/browser.py +0 -0
  91. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/classify.py +0 -0
  92. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/commit.py +0 -0
  93. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/decide.py +0 -0
  94. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/extract_code.py +0 -0
  95. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/extract_next_state.py +0 -0
  96. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/extract_strategy.py +0 -0
  97. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/facts.py +0 -0
  98. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/index_profile.py +0 -0
  99. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/manifest.py +0 -0
  100. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/paths.py +0 -0
  101. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/plan.py +0 -0
  102. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/purge.py +0 -0
  103. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/registry.py +0 -0
  104. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/ato.py +0 -0
  105. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/augov.py +0 -0
  106. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/generic.py +0 -0
  107. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/generic_browser.py +0 -0
  108. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/mdn.py +0 -0
  109. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sites/python_docs.py +0 -0
  110. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sources/__init__.py +0 -0
  111. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sources/firecrawl.py +0 -0
  112. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift/sources/sitemap.py +0 -0
  113. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift_engine.egg-info/dependency_links.txt +0 -0
  114. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift_engine.egg-info/entry_points.txt +0 -0
  115. {sift_engine-0.2.0 → sift_engine-0.3.0}/sift_engine.egg-info/top_level.txt +0 -0
  116. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_agent_loop.py +0 -0
  117. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_agent_surface.py +0 -0
  118. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_backup.py +0 -0
  119. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_browser_contract.py +0 -0
  120. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_browser_real.py +0 -0
  121. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_classify.py +0 -0
  122. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_commit_chain.py +0 -0
  123. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_consolidate_md.py +0 -0
  124. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_decide.py +0 -0
  125. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_eval_bench_b3.py +0 -0
  126. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_eval_bench_b5.py +0 -0
  127. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_eval_bench_drift.py +0 -0
  128. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_eval_bench_scoring.py +0 -0
  129. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_evals.py +0 -0
  130. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_extract_anchors.py +0 -0
  131. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_extract_code.py +0 -0
  132. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_extract_next_state.py +0 -0
  133. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_facts.py +0 -0
  134. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_facts_coverage.py +0 -0
  135. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_firecrawl_scrape.py +0 -0
  136. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_integrity.py +0 -0
  137. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_manifest.py +0 -0
  138. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_markdown_passthrough.py +0 -0
  139. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_mcp_index.py +0 -0
  140. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_mcp_multi_index.py +0 -0
  141. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_mcp_verify.py +0 -0
  142. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_normalize.py +0 -0
  143. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_only_urls.py +0 -0
  144. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_pre_freeze_hardening.py +0 -0
  145. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_production_hardening.py +0 -0
  146. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_purge.py +0 -0
  147. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_re_extract.py +0 -0
  148. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_registry.py +0 -0
  149. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_sites.py +0 -0
  150. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_sources_firecrawl.py +0 -0
  151. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_sources_sitemap_auto.py +0 -0
  152. {sift_engine-0.2.0 → sift_engine-0.3.0}/tests/test_user_agent_warnings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sift-engine
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Sift — deterministic website indexing for grep-first LLM agents
5
5
  Author: Deval Shah
6
6
  License-Expression: Apache-2.0
@@ -21,7 +21,9 @@ Requires-Dist: lxml>=5.0
21
21
  Requires-Dist: selectolax>=0.3
22
22
  Requires-Dist: mcp>=1.0
23
23
  Requires-Dist: pypdf>=4.0
24
+ Requires-Dist: pdfplumber>=0.11
24
25
  Requires-Dist: jsonschema>=4
26
+ Requires-Dist: protego>=0.3
25
27
  Provides-Extra: dev
26
28
  Requires-Dist: pytest>=8.0; extra == "dev"
27
29
  Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
@@ -62,8 +64,8 @@ Your agent's built-in web-fetch gives you:
62
64
 
63
65
  ## ✅ With sift
64
66
 
65
- - **Always fresh** — you control the crawl; conditional GETs refetch only what changed
66
- - **Provable** — content-hash + date on every page; cite the source, hash, and snapshot
67
+ - **Always fresh** — you control the crawl; conditional GETs refetch only what changed, and `changed_since` lets your agent pull just the delta instead of re-reading the corpus
68
+ - **Provable** — content-hash + date on every page; cite the source, hash, and snapshot, read any past snapshot (`as_of`), or emit a self-contained `prove` inclusion proof a third party verifies offline (`python -m sift.verify_proof`) without trusting the server — optionally anchored by an RFC-3161 timestamp so the date is witnessed by a third party, not self-asserted
67
69
  - **Complete & grep-native** — the whole site on disk, not a handful of retrieved snippets
68
70
  - **Self-hosted** — any `http(s)` site you can reach, public or internal; your data stays yours
69
71
 
@@ -109,7 +111,7 @@ sift --version && which sift-mcp # confirm it's on PATH
109
111
  Download the skill so you (the agent) know how to build, operate, and query an index.
110
112
  Read .claude/skills/sift/SKILL.md after downloading — it is the source of truth for the rest.
111
113
  ```bash
112
- SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.2.0/.claude/skills/sift
114
+ SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.3.0/.claude/skills/sift
113
115
  mkdir -p .claude/skills/sift/reference
114
116
  curl -fsSL $SKILL/SKILL.md -o .claude/skills/sift/SKILL.md
115
117
  for f in cli config mcp-tools; do curl -fsSL $SKILL/reference/$f.md -o .claude/skills/sift/reference/$f.md; done
@@ -131,12 +133,22 @@ sift run --root ./sift-index --config sift.toml --limit 25 --coverage-base pl
131
133
  sift verify --root ./sift-index --skip-signature
132
134
  ```
133
135
  Drop `--limit 25 --coverage-base planned` for a full crawl once extraction looks good.
136
+ Preview exactly which URLs a crawl would touch — and how many — *without fetching a
137
+ byte*: `sift discover --from-domain TARGET_SITE` (writes nothing; emits JSON).
138
+ No `sitemap.xml` (or an incomplete one)? After a run, `sift seed --from-frontier`
139
+ extracts in-scope links from the fetched pages and crawls one hop deeper each pass.
134
140
  Hardened / bot-blocked host (Cloudflare/Akamai/Imperva) → add `--impersonate-fallback`
135
141
  (free, TLS-fingerprint impersonation; `pip install 'sift-engine[impersonate]'`).
136
142
  JS-rendered SPA → `pip install 'sift-engine[browser]' && python -m playwright install chromium`,
137
143
  then add `[browser]\nenabled = true` (it joins the ladder as a free render tier).
144
+ JS-rendered SPA that has an official API (eCFR, CVE.org, FederalRegister …) → a
145
+ profile's `api_url(url)` fetches the robots-allowed API instead of the shell —
146
+ cleaner and cheaper than a browser, and the citation stays the human page (see
147
+ the bundled `sift.sites.cve:CVEProfile`).
138
148
  Still blocked (JS-challenge edges) → `--firecrawl-fallback` (paid; needs FIRECRAWL_API_KEY).
139
149
  These compose into one escalation ladder: native → impersonate → browser → Firecrawl.
150
+ sift respects `robots.txt` `Disallow` at seed by default; set `[crawl]` →
151
+ `respect_robots = false` only for sources you have permission to index.
140
152
 
141
153
  4 — WIRE THE READ-ONLY MCP SERVER
142
154
  Use the ABSOLUTE path to ./sift-index. Add this to the project's .mcp.json (Claude Code / Cursor / Codex):
@@ -204,7 +216,7 @@ Your agent reads the published snapshot **read-only** over MCP: `snapshot_status
204
216
 
205
217
  > **Open core, Apache-2.0.** This repo is the full open-source engine (pipeline + MCP server) and runs standalone. A hosted platform built on it is in development.
206
218
 
207
- **Status — v0.2.0**; tests green on Python 3.11–3.13. Adds the tiered fetch transport (native curl_cffi browser Firecrawl) for hardened and JS-rendered sites. Known limits: no run-dir GC yet, stdout-only logging, stdio-only MCP transport. Issues & roadmap → [GitHub Issues](https://github.com/dvlshah/sift/issues).
219
+ **Status — v0.3.0**; tests green on Python 3.11–3.13. Adds proof-carrying answers (`prove` / `verify-proof` + an RFC-3161 timestamp anchor), time-travel reads (`changed_since`, `diff_md`, `as_of` over retained snapshots), API acquisition/content transports (`api_url` source + `json` extract), robots + SSRF + bot-challenge hardening, digital-PDF tables, a recursive link frontier, and `sift discover` (read-only pre-crawl preview). Known limits: no run-dir GC yet, stdout-only logging, stdio-only MCP transport. Issues & roadmap → [GitHub Issues](https://github.com/dvlshah/sift/issues).
208
220
 
209
221
  ## License
210
222
 
@@ -24,8 +24,8 @@ Your agent's built-in web-fetch gives you:
24
24
 
25
25
  ## ✅ With sift
26
26
 
27
- - **Always fresh** — you control the crawl; conditional GETs refetch only what changed
28
- - **Provable** — content-hash + date on every page; cite the source, hash, and snapshot
27
+ - **Always fresh** — you control the crawl; conditional GETs refetch only what changed, and `changed_since` lets your agent pull just the delta instead of re-reading the corpus
28
+ - **Provable** — content-hash + date on every page; cite the source, hash, and snapshot, read any past snapshot (`as_of`), or emit a self-contained `prove` inclusion proof a third party verifies offline (`python -m sift.verify_proof`) without trusting the server — optionally anchored by an RFC-3161 timestamp so the date is witnessed by a third party, not self-asserted
29
29
  - **Complete & grep-native** — the whole site on disk, not a handful of retrieved snippets
30
30
  - **Self-hosted** — any `http(s)` site you can reach, public or internal; your data stays yours
31
31
 
@@ -71,7 +71,7 @@ sift --version && which sift-mcp # confirm it's on PATH
71
71
  Download the skill so you (the agent) know how to build, operate, and query an index.
72
72
  Read .claude/skills/sift/SKILL.md after downloading — it is the source of truth for the rest.
73
73
  ```bash
74
- SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.2.0/.claude/skills/sift
74
+ SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.3.0/.claude/skills/sift
75
75
  mkdir -p .claude/skills/sift/reference
76
76
  curl -fsSL $SKILL/SKILL.md -o .claude/skills/sift/SKILL.md
77
77
  for f in cli config mcp-tools; do curl -fsSL $SKILL/reference/$f.md -o .claude/skills/sift/reference/$f.md; done
@@ -93,12 +93,22 @@ sift run --root ./sift-index --config sift.toml --limit 25 --coverage-base pl
93
93
  sift verify --root ./sift-index --skip-signature
94
94
  ```
95
95
  Drop `--limit 25 --coverage-base planned` for a full crawl once extraction looks good.
96
+ Preview exactly which URLs a crawl would touch — and how many — *without fetching a
97
+ byte*: `sift discover --from-domain TARGET_SITE` (writes nothing; emits JSON).
98
+ No `sitemap.xml` (or an incomplete one)? After a run, `sift seed --from-frontier`
99
+ extracts in-scope links from the fetched pages and crawls one hop deeper each pass.
96
100
  Hardened / bot-blocked host (Cloudflare/Akamai/Imperva) → add `--impersonate-fallback`
97
101
  (free, TLS-fingerprint impersonation; `pip install 'sift-engine[impersonate]'`).
98
102
  JS-rendered SPA → `pip install 'sift-engine[browser]' && python -m playwright install chromium`,
99
103
  then add `[browser]\nenabled = true` (it joins the ladder as a free render tier).
104
+ JS-rendered SPA that has an official API (eCFR, CVE.org, FederalRegister …) → a
105
+ profile's `api_url(url)` fetches the robots-allowed API instead of the shell —
106
+ cleaner and cheaper than a browser, and the citation stays the human page (see
107
+ the bundled `sift.sites.cve:CVEProfile`).
100
108
  Still blocked (JS-challenge edges) → `--firecrawl-fallback` (paid; needs FIRECRAWL_API_KEY).
101
109
  These compose into one escalation ladder: native → impersonate → browser → Firecrawl.
110
+ sift respects `robots.txt` `Disallow` at seed by default; set `[crawl]` →
111
+ `respect_robots = false` only for sources you have permission to index.
102
112
 
103
113
  4 — WIRE THE READ-ONLY MCP SERVER
104
114
  Use the ABSOLUTE path to ./sift-index. Add this to the project's .mcp.json (Claude Code / Cursor / Codex):
@@ -166,7 +176,7 @@ Your agent reads the published snapshot **read-only** over MCP: `snapshot_status
166
176
 
167
177
  > **Open core, Apache-2.0.** This repo is the full open-source engine (pipeline + MCP server) and runs standalone. A hosted platform built on it is in development.
168
178
 
169
- **Status — v0.2.0**; tests green on Python 3.11–3.13. Adds the tiered fetch transport (native curl_cffi browser Firecrawl) for hardened and JS-rendered sites. Known limits: no run-dir GC yet, stdout-only logging, stdio-only MCP transport. Issues & roadmap → [GitHub Issues](https://github.com/dvlshah/sift/issues).
179
+ **Status — v0.3.0**; tests green on Python 3.11–3.13. Adds proof-carrying answers (`prove` / `verify-proof` + an RFC-3161 timestamp anchor), time-travel reads (`changed_since`, `diff_md`, `as_of` over retained snapshots), API acquisition/content transports (`api_url` source + `json` extract), robots + SSRF + bot-challenge hardening, digital-PDF tables, a recursive link frontier, and `sift discover` (read-only pre-crawl preview). Known limits: no run-dir GC yet, stdout-only logging, stdio-only MCP transport. Issues & roadmap → [GitHub Issues](https://github.com/dvlshah/sift/issues).
170
180
 
171
181
  ## License
172
182
 
@@ -57,9 +57,8 @@ def eval_plan_decision_correctness() -> PlanDecisionCorrectnessResult:
57
57
  from sift.config import IndexConfig, TierConfig
58
58
  from sift.classify import CLASSIFIER_VERSION
59
59
  from sift.extract import EXTRACTOR_VERSION
60
- from sift.normalize import NORMALIZER_VERSION
60
+ from sift.normalize import normalizer_version
61
61
  from sift import decide as decide_mod
62
- from sift import sites as sites_mod
63
62
  from sift import paths
64
63
 
65
64
  # Set up an in-memory-ish manifest in a temp dir
@@ -151,7 +150,7 @@ def eval_plan_decision_correctness() -> PlanDecisionCorrectnessResult:
151
150
  plan_phase(
152
151
  conn, plan_path, now=now,
153
152
  extractor_version=EXTRACTOR_VERSION,
154
- normalizer_version=NORMALIZER_VERSION,
153
+ normalizer_version=normalizer_version(),
155
154
  profile=GenericProfile(),
156
155
  cfg=cfg,
157
156
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sift-engine"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Sift — deterministic website indexing for grep-first LLM agents"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -24,7 +24,9 @@ dependencies = [
24
24
  "selectolax>=0.3",
25
25
  "mcp>=1.0",
26
26
  "pypdf>=4.0",
27
+ "pdfplumber>=0.11",
27
28
  "jsonschema>=4",
29
+ "protego>=0.3",
28
30
  ]
29
31
 
30
32
  [project.optional-dependencies]
@@ -16,7 +16,7 @@ bumping a version triggers re-derivation from cached raw, not refetch.
16
16
  See README.md for the full design.
17
17
  """
18
18
 
19
- __version__ = "0.1.0"
19
+ __version__ = "0.3.0"
20
20
  # Provenance stamp on manifest rows — a *behavioral* version (like
21
21
  # EXTRACTOR_VERSION), bumped only when crawl/fetch behavior changes, not on
22
22
  # every package release. this is the initial public release; crawl behavior is unchanged, so this stays.