sift-engine 0.1.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 (156) hide show
  1. sift_engine-0.3.0/PKG-INFO +223 -0
  2. sift_engine-0.3.0/README.md +183 -0
  3. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/plan.py +2 -3
  4. {sift_engine-0.1.0 → sift_engine-0.3.0}/pyproject.toml +11 -3
  5. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/__init__.py +1 -1
  6. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/browser.py +1 -1
  7. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/cli.py +1170 -272
  8. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/config.py +79 -0
  9. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/extract.py +216 -17
  10. sift_engine-0.3.0/sift/fetch.py +858 -0
  11. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/integrity.py +83 -0
  12. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/mcp_server.py +723 -27
  13. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/normalize.py +32 -1
  14. sift_engine-0.3.0/sift/prove.py +242 -0
  15. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/publish.py +282 -11
  16. sift_engine-0.3.0/sift/quality.py +156 -0
  17. sift_engine-0.3.0/sift/robots.py +109 -0
  18. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/__init__.py +44 -2
  19. sift_engine-0.3.0/sift/sites/cve.py +47 -0
  20. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/stripe.py +4 -0
  21. sift_engine-0.3.0/sift/sources/frontier.py +125 -0
  22. sift_engine-0.3.0/sift/sources/impersonate.py +182 -0
  23. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/status.py +15 -2
  24. sift_engine-0.3.0/sift/timestamp.py +145 -0
  25. sift_engine-0.3.0/sift/verify_proof.py +101 -0
  26. sift_engine-0.3.0/sift_engine.egg-info/PKG-INFO +223 -0
  27. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift_engine.egg-info/SOURCES.txt +22 -0
  28. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift_engine.egg-info/requires.txt +5 -0
  29. sift_engine-0.3.0/tests/test_api_connector.py +261 -0
  30. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_browser_contract.py +5 -5
  31. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_browser_real.py +1 -3
  32. sift_engine-0.3.0/tests/test_changelog_continuity.py +81 -0
  33. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_config.py +6 -2
  34. sift_engine-0.3.0/tests/test_content_admission.py +154 -0
  35. sift_engine-0.3.0/tests/test_discover.py +92 -0
  36. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_excludes_and_pdf.py +27 -1
  37. sift_engine-0.3.0/tests/test_extract_json.py +157 -0
  38. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_extract_strategy.py +5 -3
  39. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_fetch_browser.py +81 -0
  40. sift_engine-0.3.0/tests/test_frontier.py +90 -0
  41. sift_engine-0.3.0/tests/test_gates.py +321 -0
  42. sift_engine-0.3.0/tests/test_impersonate_rotation.py +143 -0
  43. sift_engine-0.3.0/tests/test_mcp_changed_since.py +244 -0
  44. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_mcp_server.py +2 -2
  45. sift_engine-0.3.0/tests/test_mcp_time_travel.py +231 -0
  46. sift_engine-0.3.0/tests/test_normalizer_scoping.py +124 -0
  47. sift_engine-0.3.0/tests/test_prove.py +346 -0
  48. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_publish_integrity.py +26 -0
  49. sift_engine-0.3.0/tests/test_robots.py +111 -0
  50. sift_engine-0.3.0/tests/test_tiered_transport.py +447 -0
  51. sift_engine-0.3.0/tests/test_timestamp.py +135 -0
  52. sift_engine-0.1.0/PKG-INFO +0 -342
  53. sift_engine-0.1.0/README.md +0 -306
  54. sift_engine-0.1.0/sift/fetch.py +0 -459
  55. sift_engine-0.1.0/sift_engine.egg-info/PKG-INFO +0 -342
  56. sift_engine-0.1.0/tests/test_gates.py +0 -135
  57. {sift_engine-0.1.0 → sift_engine-0.3.0}/LICENSE +0 -0
  58. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/__init__.py +0 -0
  59. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/__init__.py +0 -0
  60. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/agent.py +0 -0
  61. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/judge.py +0 -0
  62. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/questions.py +0 -0
  63. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/report.py +0 -0
  64. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/runner.py +0 -0
  65. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/agent_loop/tools.py +0 -0
  66. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/__init__.py +0 -0
  67. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/aggregate_report.py +0 -0
  68. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/drift.py +0 -0
  69. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/fixtures/__init__.py +0 -0
  70. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/fixtures/sites.py +0 -0
  71. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/full_suite.py +0 -0
  72. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/__init__.py +0 -0
  73. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/commit.py +0 -0
  74. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/extract.py +0 -0
  75. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/fetch.py +0 -0
  76. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/mcp.py +0 -0
  77. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/publish.py +0 -0
  78. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/per_stage/seed.py +0 -0
  79. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/report.py +0 -0
  80. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/runner.py +0 -0
  81. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/scoring/__init__.py +0 -0
  82. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/scoring/fidelity.py +0 -0
  83. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/scoring/structural.py +0 -0
  84. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/bench/scoring/use_case.py +0 -0
  85. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/cli.py +0 -0
  86. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/determinism.py +0 -0
  87. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/efficiency.py +0 -0
  88. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/facts_coverage.py +0 -0
  89. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/facts_validation.py +0 -0
  90. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/llm_judge.py +0 -0
  91. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/performance.py +0 -0
  92. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/sampler.py +0 -0
  93. {sift_engine-0.1.0 → sift_engine-0.3.0}/evals/structural.py +0 -0
  94. {sift_engine-0.1.0 → sift_engine-0.3.0}/setup.cfg +0 -0
  95. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/_io.py +0 -0
  96. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/agent_surface.py +0 -0
  97. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/classify.py +0 -0
  98. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/commit.py +0 -0
  99. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/decide.py +0 -0
  100. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/extract_code.py +0 -0
  101. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/extract_next_state.py +0 -0
  102. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/extract_strategy.py +0 -0
  103. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/facts.py +0 -0
  104. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/index_profile.py +0 -0
  105. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/manifest.py +0 -0
  106. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/paths.py +0 -0
  107. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/plan.py +0 -0
  108. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/purge.py +0 -0
  109. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/registry.py +0 -0
  110. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/ato.py +0 -0
  111. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/augov.py +0 -0
  112. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/generic.py +0 -0
  113. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/generic_browser.py +0 -0
  114. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/mdn.py +0 -0
  115. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sites/python_docs.py +0 -0
  116. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sources/__init__.py +0 -0
  117. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sources/firecrawl.py +0 -0
  118. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift/sources/sitemap.py +0 -0
  119. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift_engine.egg-info/dependency_links.txt +0 -0
  120. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift_engine.egg-info/entry_points.txt +0 -0
  121. {sift_engine-0.1.0 → sift_engine-0.3.0}/sift_engine.egg-info/top_level.txt +0 -0
  122. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_agent_loop.py +0 -0
  123. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_agent_surface.py +0 -0
  124. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_backup.py +0 -0
  125. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_classify.py +0 -0
  126. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_commit_chain.py +0 -0
  127. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_consolidate_md.py +0 -0
  128. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_decide.py +0 -0
  129. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_eval_bench_b3.py +0 -0
  130. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_eval_bench_b5.py +0 -0
  131. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_eval_bench_drift.py +0 -0
  132. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_eval_bench_scoring.py +0 -0
  133. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_evals.py +0 -0
  134. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_extract_anchors.py +0 -0
  135. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_extract_code.py +0 -0
  136. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_extract_next_state.py +0 -0
  137. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_facts.py +0 -0
  138. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_facts_coverage.py +0 -0
  139. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_firecrawl_scrape.py +0 -0
  140. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_integrity.py +0 -0
  141. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_manifest.py +0 -0
  142. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_markdown_passthrough.py +0 -0
  143. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_mcp_index.py +0 -0
  144. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_mcp_multi_index.py +0 -0
  145. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_mcp_verify.py +0 -0
  146. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_normalize.py +0 -0
  147. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_only_urls.py +0 -0
  148. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_pre_freeze_hardening.py +0 -0
  149. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_production_hardening.py +0 -0
  150. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_purge.py +0 -0
  151. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_re_extract.py +0 -0
  152. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_registry.py +0 -0
  153. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_sites.py +0 -0
  154. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_sources_firecrawl.py +0 -0
  155. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_sources_sitemap_auto.py +0 -0
  156. {sift_engine-0.1.0 → sift_engine-0.3.0}/tests/test_user_agent_warnings.py +0 -0
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: sift-engine
3
+ Version: 0.3.0
4
+ Summary: Sift — deterministic website indexing for grep-first LLM agents
5
+ Author: Deval Shah
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/dvlshah/sift
8
+ Project-URL: Repository, https://github.com/dvlshah/sift
9
+ Project-URL: Issues, https://github.com/dvlshah/sift/issues
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: trafilatura>=1.12
18
+ Requires-Dist: aiolimiter>=1.1
19
+ Requires-Dist: click>=8.1
20
+ Requires-Dist: lxml>=5.0
21
+ Requires-Dist: selectolax>=0.3
22
+ Requires-Dist: mcp>=1.0
23
+ Requires-Dist: pypdf>=4.0
24
+ Requires-Dist: pdfplumber>=0.11
25
+ Requires-Dist: jsonschema>=4
26
+ Requires-Dist: protego>=0.3
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
30
+ Requires-Dist: respx>=0.21; extra == "dev"
31
+ Provides-Extra: evals
32
+ Requires-Dist: anthropic>=0.40; extra == "evals"
33
+ Requires-Dist: pydantic>=2.0; extra == "evals"
34
+ Requires-Dist: jsonschema>=4; extra == "evals"
35
+ Provides-Extra: browser
36
+ Requires-Dist: playwright>=1.40; extra == "browser"
37
+ Provides-Extra: impersonate
38
+ Requires-Dist: curl_cffi>=0.7; extra == "impersonate"
39
+ Dynamic: license-file
40
+
41
+ <div align="center">
42
+
43
+ <img src="assets/sift.gif" alt="sift" width="480">
44
+
45
+ **Give your coding agent a complete, always-fresh copy of any website — with proof of every answer.**
46
+
47
+ [![Tests](https://github.com/dvlshah/sift/actions/workflows/tests.yml/badge.svg)](https://github.com/dvlshah/sift/actions/workflows/tests.yml)
48
+ [![PyPI](https://img.shields.io/pypi/v/sift-engine?color=blue)](https://pypi.org/project/sift-engine/)
49
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
50
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/downloads/)
51
+ [![MCP](https://img.shields.io/badge/MCP-stdio-purple)](https://modelcontextprotocol.io/)
52
+
53
+ </div>
54
+
55
+ sift crawls a site into local markdown + structured facts your agent greps, reads, and cites — files on disk, not vectors, read over MCP. Every page is **content-hashed and dated**, so answers trace back to the exact source and snapshot. Re-run anytime; only changed pages are refetched. Self-hosted.
56
+
57
+ ## ❌ Without sift
58
+
59
+ Your agent's built-in web-fetch gives you:
60
+
61
+ - **Stale** answers — the provider's crawler cached that page weeks ago
62
+ - **Partial** answers — just the one page it happened to land on
63
+ - **Unprovable** answers — no record of what the source said, or when
64
+
65
+ ## ✅ With sift
66
+
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
69
+ - **Complete & grep-native** — the whole site on disk, not a handful of retrieved snippets
70
+ - **Self-hosted** — any `http(s)` site you can reach, public or internal; your data stays yours
71
+
72
+ ---
73
+
74
+ ## Get started
75
+
76
+ Requires Python 3.11+.
77
+
78
+ ### 🤖 Let your coding agent set it up — recommended
79
+
80
+ Paste one prompt into **Claude Code, Cursor, Codex, or any MCP-aware agent**. It explains sift, asks which site to index, then installs the engine, builds an index, wires up MCP, and shows you how to query it — end to end.
81
+
82
+ <details>
83
+ <summary><b>📋 Copy the one-paste setup prompt</b></summary>
84
+
85
+ ````text
86
+ Set up **sift** in this project — a deterministic, content-hashed website indexer that
87
+ serves a verifiable markdown corpus to AI agents over MCP (https://github.com/dvlshah/sift).
88
+ Do the steps in order. Needs Python 3.11+.
89
+
90
+ 0 — EXPLAIN, THEN ASK ME (do this BEFORE installing anything)
91
+ First, explain sift to me in 4–6 plain-English lines — assume I've never heard of it:
92
+ • WHAT it is: it crawls a whole website/docs site into local markdown files (plus structured
93
+ facts) that you, the agent, can grep / read / cite — files on disk, not a vector database.
94
+ • CORE FEATURES: (1) complete — the full site, not the few pages a live web-fetch happens to
95
+ land on; (2) verifiable — every page is content-hashed + dated, so any answer can be proved
96
+ back to the exact source and snapshot; (3) always-current — re-running the crawl refetches
97
+ only what changed; (4) read over MCP — you query it with provenance, read-only by default.
98
+ • WHY IT'S NEEDED: a one-off scrape gives you 3 pages with no proof of what they said or when.
99
+ sift keeps an agent correct about an evolving body of docs AND able to prove what it cited.
100
+ (If someone only needs one page once, sift is the wrong tool — a plain fetch is fine.)
101
+ Then ASK ME: "Which website or docs site do you want to index locally?" — wait for my answer
102
+ and use it as TARGET_SITE everywhere below. Do not run any commands until I answer.
103
+
104
+ 1 — INSTALL THE ENGINE
105
+ ```bash
106
+ pip install sift-engine # adds the `sift`, `sift-mcp`, `sift-evals` commands
107
+ sift --version && which sift-mcp # confirm it's on PATH
108
+ ```
109
+
110
+ 2 — INSTALL THE SIFT SKILL INTO THIS REPO
111
+ Download the skill so you (the agent) know how to build, operate, and query an index.
112
+ Read .claude/skills/sift/SKILL.md after downloading — it is the source of truth for the rest.
113
+ ```bash
114
+ SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.3.0/.claude/skills/sift
115
+ mkdir -p .claude/skills/sift/reference
116
+ curl -fsSL $SKILL/SKILL.md -o .claude/skills/sift/SKILL.md
117
+ for f in cli config mcp-tools; do curl -fsSL $SKILL/reference/$f.md -o .claude/skills/sift/reference/$f.md; done
118
+ ```
119
+
120
+ 3 — BUILD A SMALL STARTER INDEX (using the TARGET_SITE I gave you in step 0)
121
+ Write a sift.toml (generic profile + host allow-list = the host of TARGET_SITE), then build a
122
+ capped, publishable smoke-test index:
123
+ ```bash
124
+ cat > sift.toml <<'TOML'
125
+ [site]
126
+ profile = "sift.sites.generic:GenericProfile"
127
+ [seed]
128
+ host_allow = ["HOST_OF_TARGET_SITE"] # e.g. docs.example.com — derive from TARGET_SITE
129
+ TOML
130
+ sift init --root ./sift-index
131
+ sift seed --root ./sift-index --config sift.toml --from-domain TARGET_SITE
132
+ sift run --root ./sift-index --config sift.toml --limit 25 --coverage-base planned
133
+ sift verify --root ./sift-index --skip-signature
134
+ ```
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.
140
+ Hardened / bot-blocked host (Cloudflare/Akamai/Imperva) → add `--impersonate-fallback`
141
+ (free, TLS-fingerprint impersonation; `pip install 'sift-engine[impersonate]'`).
142
+ JS-rendered SPA → `pip install 'sift-engine[browser]' && python -m playwright install chromium`,
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`).
148
+ Still blocked (JS-challenge edges) → `--firecrawl-fallback` (paid; needs FIRECRAWL_API_KEY).
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.
152
+
153
+ 4 — WIRE THE READ-ONLY MCP SERVER
154
+ Use the ABSOLUTE path to ./sift-index. Add this to the project's .mcp.json (Claude Code / Cursor / Codex):
155
+ ```json
156
+ { "mcpServers": { "sift": { "command": "sift-mcp", "args": ["--root", "ABSOLUTE/PATH/TO/sift-index"] } } }
157
+ ```
158
+ Claude Code shortcut: `claude mcp add sift -- sift-mcp --root "$(pwd)/sift-index"`
159
+ Then restart the MCP client so the `sift` tools load.
160
+
161
+ 5 — SHOW ME THE QUERY LOOP
162
+ Call the `snapshot_status` tool to confirm the index is published, then explain the loop:
163
+ snapshot_status first → grep_corpus to locate → read_md / read_facts to drill in →
164
+ cite source_url + content_hash + fetched_at. Mention that re-running `sift run` refreshes the
165
+ index, and that the /sift skill covers building, operating, and querying in depth.
166
+
167
+ (TARGET_SITE = the site I name when you ask in step 0, e.g. https://docs.example.com)
168
+ ````
169
+
170
+ </details>
171
+
172
+ ### 🧑 Or do it yourself
173
+
174
+ ```bash
175
+ pip install sift-engine
176
+
177
+ # the ATO sitemap below uses a bundled profile, so this runs with zero config
178
+ sift init --root ./index
179
+ sift seed --root ./index --from-sitemap https://www.ato.gov.au/sitemap.xml
180
+ sift run --root ./index --limit 25 --coverage-base planned # smoke-test first
181
+ sift verify --root ./index --skip-signature
182
+ sift-mcp --root ./index # serve the index to your agent
183
+ ```
184
+
185
+ Then point your agent at it (use an absolute path):
186
+
187
+ ```json
188
+ {
189
+ "mcpServers": {
190
+ "sift": { "command": "sift-mcp", "args": ["--root", "/abs/path/to/index"] }
191
+ }
192
+ }
193
+ ```
194
+
195
+ Indexing your own site? Add a `sift.toml` (generic profile + host allow-list) — see [Configuration](.claude/skills/sift/reference/config.md).
196
+
197
+ ---
198
+
199
+ ## How it works
200
+
201
+ ```
202
+ seed → plan → fetch → extract → commit → publish
203
+ ```
204
+
205
+ Five idempotent phases. `publish` runs 5 verification gates, then atomically swaps the `current/` snapshot and writes a Merkle root over every page hash. Deterministic: **same input → same `content_hash` → same Merkle root** — so any reader re-verifies a page in O(1), or the whole snapshot end-to-end with `sift verify`.
206
+
207
+ Your agent reads the published snapshot **read-only** over MCP: `snapshot_status` → `grep_corpus` → `read_md` / `read_facts` → cite source + hash + date.
208
+
209
+ ## Docs
210
+
211
+ - **[CLI reference](.claude/skills/sift/reference/cli.md)** — every command and flag
212
+ - **[Configuration & site profiles](.claude/skills/sift/reference/config.md)** — `sift.toml` and the `SiteProfile` contract
213
+ - **[MCP tools](.claude/skills/sift/reference/mcp-tools.md)** — parameters, output caps, multi-index mode
214
+ - **[Corpus format & integrity contract](./corpus.contract.md)** — on-disk layout and what each read tool returns
215
+ - **[Contributing](./CONTRIBUTING.md)** · **[Security](./SECURITY.md)**
216
+
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.
218
+
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).
220
+
221
+ ## License
222
+
223
+ [Apache-2.0](./LICENSE) — © 2026 Deval Shah.
@@ -0,0 +1,183 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/sift.gif" alt="sift" width="480">
4
+
5
+ **Give your coding agent a complete, always-fresh copy of any website — with proof of every answer.**
6
+
7
+ [![Tests](https://github.com/dvlshah/sift/actions/workflows/tests.yml/badge.svg)](https://github.com/dvlshah/sift/actions/workflows/tests.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/sift-engine?color=blue)](https://pypi.org/project/sift-engine/)
9
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
10
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/downloads/)
11
+ [![MCP](https://img.shields.io/badge/MCP-stdio-purple)](https://modelcontextprotocol.io/)
12
+
13
+ </div>
14
+
15
+ sift crawls a site into local markdown + structured facts your agent greps, reads, and cites — files on disk, not vectors, read over MCP. Every page is **content-hashed and dated**, so answers trace back to the exact source and snapshot. Re-run anytime; only changed pages are refetched. Self-hosted.
16
+
17
+ ## ❌ Without sift
18
+
19
+ Your agent's built-in web-fetch gives you:
20
+
21
+ - **Stale** answers — the provider's crawler cached that page weeks ago
22
+ - **Partial** answers — just the one page it happened to land on
23
+ - **Unprovable** answers — no record of what the source said, or when
24
+
25
+ ## ✅ With sift
26
+
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
+ - **Complete & grep-native** — the whole site on disk, not a handful of retrieved snippets
30
+ - **Self-hosted** — any `http(s)` site you can reach, public or internal; your data stays yours
31
+
32
+ ---
33
+
34
+ ## Get started
35
+
36
+ Requires Python 3.11+.
37
+
38
+ ### 🤖 Let your coding agent set it up — recommended
39
+
40
+ Paste one prompt into **Claude Code, Cursor, Codex, or any MCP-aware agent**. It explains sift, asks which site to index, then installs the engine, builds an index, wires up MCP, and shows you how to query it — end to end.
41
+
42
+ <details>
43
+ <summary><b>📋 Copy the one-paste setup prompt</b></summary>
44
+
45
+ ````text
46
+ Set up **sift** in this project — a deterministic, content-hashed website indexer that
47
+ serves a verifiable markdown corpus to AI agents over MCP (https://github.com/dvlshah/sift).
48
+ Do the steps in order. Needs Python 3.11+.
49
+
50
+ 0 — EXPLAIN, THEN ASK ME (do this BEFORE installing anything)
51
+ First, explain sift to me in 4–6 plain-English lines — assume I've never heard of it:
52
+ • WHAT it is: it crawls a whole website/docs site into local markdown files (plus structured
53
+ facts) that you, the agent, can grep / read / cite — files on disk, not a vector database.
54
+ • CORE FEATURES: (1) complete — the full site, not the few pages a live web-fetch happens to
55
+ land on; (2) verifiable — every page is content-hashed + dated, so any answer can be proved
56
+ back to the exact source and snapshot; (3) always-current — re-running the crawl refetches
57
+ only what changed; (4) read over MCP — you query it with provenance, read-only by default.
58
+ • WHY IT'S NEEDED: a one-off scrape gives you 3 pages with no proof of what they said or when.
59
+ sift keeps an agent correct about an evolving body of docs AND able to prove what it cited.
60
+ (If someone only needs one page once, sift is the wrong tool — a plain fetch is fine.)
61
+ Then ASK ME: "Which website or docs site do you want to index locally?" — wait for my answer
62
+ and use it as TARGET_SITE everywhere below. Do not run any commands until I answer.
63
+
64
+ 1 — INSTALL THE ENGINE
65
+ ```bash
66
+ pip install sift-engine # adds the `sift`, `sift-mcp`, `sift-evals` commands
67
+ sift --version && which sift-mcp # confirm it's on PATH
68
+ ```
69
+
70
+ 2 — INSTALL THE SIFT SKILL INTO THIS REPO
71
+ Download the skill so you (the agent) know how to build, operate, and query an index.
72
+ Read .claude/skills/sift/SKILL.md after downloading — it is the source of truth for the rest.
73
+ ```bash
74
+ SKILL=https://raw.githubusercontent.com/dvlshah/sift/v0.3.0/.claude/skills/sift
75
+ mkdir -p .claude/skills/sift/reference
76
+ curl -fsSL $SKILL/SKILL.md -o .claude/skills/sift/SKILL.md
77
+ for f in cli config mcp-tools; do curl -fsSL $SKILL/reference/$f.md -o .claude/skills/sift/reference/$f.md; done
78
+ ```
79
+
80
+ 3 — BUILD A SMALL STARTER INDEX (using the TARGET_SITE I gave you in step 0)
81
+ Write a sift.toml (generic profile + host allow-list = the host of TARGET_SITE), then build a
82
+ capped, publishable smoke-test index:
83
+ ```bash
84
+ cat > sift.toml <<'TOML'
85
+ [site]
86
+ profile = "sift.sites.generic:GenericProfile"
87
+ [seed]
88
+ host_allow = ["HOST_OF_TARGET_SITE"] # e.g. docs.example.com — derive from TARGET_SITE
89
+ TOML
90
+ sift init --root ./sift-index
91
+ sift seed --root ./sift-index --config sift.toml --from-domain TARGET_SITE
92
+ sift run --root ./sift-index --config sift.toml --limit 25 --coverage-base planned
93
+ sift verify --root ./sift-index --skip-signature
94
+ ```
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.
100
+ Hardened / bot-blocked host (Cloudflare/Akamai/Imperva) → add `--impersonate-fallback`
101
+ (free, TLS-fingerprint impersonation; `pip install 'sift-engine[impersonate]'`).
102
+ JS-rendered SPA → `pip install 'sift-engine[browser]' && python -m playwright install chromium`,
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`).
108
+ Still blocked (JS-challenge edges) → `--firecrawl-fallback` (paid; needs FIRECRAWL_API_KEY).
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.
112
+
113
+ 4 — WIRE THE READ-ONLY MCP SERVER
114
+ Use the ABSOLUTE path to ./sift-index. Add this to the project's .mcp.json (Claude Code / Cursor / Codex):
115
+ ```json
116
+ { "mcpServers": { "sift": { "command": "sift-mcp", "args": ["--root", "ABSOLUTE/PATH/TO/sift-index"] } } }
117
+ ```
118
+ Claude Code shortcut: `claude mcp add sift -- sift-mcp --root "$(pwd)/sift-index"`
119
+ Then restart the MCP client so the `sift` tools load.
120
+
121
+ 5 — SHOW ME THE QUERY LOOP
122
+ Call the `snapshot_status` tool to confirm the index is published, then explain the loop:
123
+ snapshot_status first → grep_corpus to locate → read_md / read_facts to drill in →
124
+ cite source_url + content_hash + fetched_at. Mention that re-running `sift run` refreshes the
125
+ index, and that the /sift skill covers building, operating, and querying in depth.
126
+
127
+ (TARGET_SITE = the site I name when you ask in step 0, e.g. https://docs.example.com)
128
+ ````
129
+
130
+ </details>
131
+
132
+ ### 🧑 Or do it yourself
133
+
134
+ ```bash
135
+ pip install sift-engine
136
+
137
+ # the ATO sitemap below uses a bundled profile, so this runs with zero config
138
+ sift init --root ./index
139
+ sift seed --root ./index --from-sitemap https://www.ato.gov.au/sitemap.xml
140
+ sift run --root ./index --limit 25 --coverage-base planned # smoke-test first
141
+ sift verify --root ./index --skip-signature
142
+ sift-mcp --root ./index # serve the index to your agent
143
+ ```
144
+
145
+ Then point your agent at it (use an absolute path):
146
+
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "sift": { "command": "sift-mcp", "args": ["--root", "/abs/path/to/index"] }
151
+ }
152
+ }
153
+ ```
154
+
155
+ Indexing your own site? Add a `sift.toml` (generic profile + host allow-list) — see [Configuration](.claude/skills/sift/reference/config.md).
156
+
157
+ ---
158
+
159
+ ## How it works
160
+
161
+ ```
162
+ seed → plan → fetch → extract → commit → publish
163
+ ```
164
+
165
+ Five idempotent phases. `publish` runs 5 verification gates, then atomically swaps the `current/` snapshot and writes a Merkle root over every page hash. Deterministic: **same input → same `content_hash` → same Merkle root** — so any reader re-verifies a page in O(1), or the whole snapshot end-to-end with `sift verify`.
166
+
167
+ Your agent reads the published snapshot **read-only** over MCP: `snapshot_status` → `grep_corpus` → `read_md` / `read_facts` → cite source + hash + date.
168
+
169
+ ## Docs
170
+
171
+ - **[CLI reference](.claude/skills/sift/reference/cli.md)** — every command and flag
172
+ - **[Configuration & site profiles](.claude/skills/sift/reference/config.md)** — `sift.toml` and the `SiteProfile` contract
173
+ - **[MCP tools](.claude/skills/sift/reference/mcp-tools.md)** — parameters, output caps, multi-index mode
174
+ - **[Corpus format & integrity contract](./corpus.contract.md)** — on-disk layout and what each read tool returns
175
+ - **[Contributing](./CONTRIBUTING.md)** · **[Security](./SECURITY.md)**
176
+
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.
178
+
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).
180
+
181
+ ## License
182
+
183
+ [Apache-2.0](./LICENSE) — © 2026 Deval Shah.
@@ -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.1.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]
@@ -39,12 +41,18 @@ evals = [
39
41
  "jsonschema>=4",
40
42
  ]
41
43
  # Optional browser-rendering stack for SPA fetch (see docs/design/browser-fetch.md).
42
- # Install with: pip install 'sift[browser]' && python -m playwright install chromium
43
- # History: v0.2.0 originally bundled crawl4ai; swapped to bare Playwright after
44
+ # Install with: pip install 'sift-engine[browser]' && python -m playwright install chromium
45
+ # History: an early prototype bundled crawl4ai; swapped to bare Playwright after
44
46
  # reproducible hangs on analytics-heavy SPAs. See archive/crawl4ai/README.md.
45
47
  browser = [
46
48
  "playwright>=1.40",
47
49
  ]
50
+ # Tier-2 escalation transport: TLS/HTTP2 fingerprint impersonation. Defeats most
51
+ # Cloudflare/Akamai/Imperva fingerprint blocks with no browser and no per-request
52
+ # cost. Install with: pip install 'sift-engine[impersonate]'
53
+ impersonate = [
54
+ "curl_cffi>=0.7",
55
+ ]
48
56
 
49
57
  [project.urls]
50
58
  Homepage = "https://github.com/dvlshah/sift"
@@ -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.
@@ -71,7 +71,7 @@ class BrowserFetchError(Exception):
71
71
  def _install_hint() -> str:
72
72
  return (
73
73
  "playwright is not installed. Install with:\n"
74
- " pip install 'sift[browser]' && python -m playwright install chromium"
74
+ " pip install 'sift-engine[browser]' && python -m playwright install chromium"
75
75
  )
76
76
 
77
77