gather-engine 1.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.
Files changed (62) hide show
  1. gather_engine-1.5.0/LICENSE +78 -0
  2. gather_engine-1.5.0/PKG-INFO +298 -0
  3. gather_engine-1.5.0/README.md +201 -0
  4. gather_engine-1.5.0/pyproject.toml +44 -0
  5. gather_engine-1.5.0/setup.cfg +4 -0
  6. gather_engine-1.5.0/src/gather/__init__.py +31 -0
  7. gather_engine-1.5.0/src/gather/api.py +113 -0
  8. gather_engine-1.5.0/src/gather/arxiv.py +130 -0
  9. gather_engine-1.5.0/src/gather/browser.py +66 -0
  10. gather_engine-1.5.0/src/gather/cli.py +141 -0
  11. gather_engine-1.5.0/src/gather/commands.py +229 -0
  12. gather_engine-1.5.0/src/gather/corpus_cmd.py +119 -0
  13. gather_engine-1.5.0/src/gather/credentials.py +30 -0
  14. gather_engine-1.5.0/src/gather/derive.py +132 -0
  15. gather_engine-1.5.0/src/gather/digest.py +88 -0
  16. gather_engine-1.5.0/src/gather/docs.py +57 -0
  17. gather_engine-1.5.0/src/gather/feed.py +112 -0
  18. gather_engine-1.5.0/src/gather/item.py +101 -0
  19. gather_engine-1.5.0/src/gather/method.py +47 -0
  20. gather_engine-1.5.0/src/gather/model.py +72 -0
  21. gather_engine-1.5.0/src/gather/net.py +139 -0
  22. gather_engine-1.5.0/src/gather/ocr.py +51 -0
  23. gather_engine-1.5.0/src/gather/pdf.py +46 -0
  24. gather_engine-1.5.0/src/gather/provenance.py +72 -0
  25. gather_engine-1.5.0/src/gather/recall.py +88 -0
  26. gather_engine-1.5.0/src/gather/run.py +214 -0
  27. gather_engine-1.5.0/src/gather/scope.py +26 -0
  28. gather_engine-1.5.0/src/gather/source.py +56 -0
  29. gather_engine-1.5.0/src/gather/store.py +288 -0
  30. gather_engine-1.5.0/src/gather/transcribe.py +58 -0
  31. gather_engine-1.5.0/src/gather/video.py +190 -0
  32. gather_engine-1.5.0/src/gather/web.py +113 -0
  33. gather_engine-1.5.0/src/gather_engine.egg-info/PKG-INFO +298 -0
  34. gather_engine-1.5.0/src/gather_engine.egg-info/SOURCES.txt +60 -0
  35. gather_engine-1.5.0/src/gather_engine.egg-info/dependency_links.txt +1 -0
  36. gather_engine-1.5.0/src/gather_engine.egg-info/entry_points.txt +2 -0
  37. gather_engine-1.5.0/src/gather_engine.egg-info/requires.txt +6 -0
  38. gather_engine-1.5.0/src/gather_engine.egg-info/top_level.txt +1 -0
  39. gather_engine-1.5.0/tests/test_api.py +75 -0
  40. gather_engine-1.5.0/tests/test_arxiv.py +77 -0
  41. gather_engine-1.5.0/tests/test_cli.py +64 -0
  42. gather_engine-1.5.0/tests/test_credentials.py +24 -0
  43. gather_engine-1.5.0/tests/test_demo.py +20 -0
  44. gather_engine-1.5.0/tests/test_derive.py +64 -0
  45. gather_engine-1.5.0/tests/test_digest.py +69 -0
  46. gather_engine-1.5.0/tests/test_docs.py +36 -0
  47. gather_engine-1.5.0/tests/test_feed.py +95 -0
  48. gather_engine-1.5.0/tests/test_hard_sources.py +57 -0
  49. gather_engine-1.5.0/tests/test_item.py +44 -0
  50. gather_engine-1.5.0/tests/test_method.py +47 -0
  51. gather_engine-1.5.0/tests/test_model.py +56 -0
  52. gather_engine-1.5.0/tests/test_net.py +75 -0
  53. gather_engine-1.5.0/tests/test_package.py +22 -0
  54. gather_engine-1.5.0/tests/test_pdf.py +15 -0
  55. gather_engine-1.5.0/tests/test_provenance.py +109 -0
  56. gather_engine-1.5.0/tests/test_recall.py +111 -0
  57. gather_engine-1.5.0/tests/test_run.py +140 -0
  58. gather_engine-1.5.0/tests/test_scope.py +25 -0
  59. gather_engine-1.5.0/tests/test_store.py +184 -0
  60. gather_engine-1.5.0/tests/test_video.py +120 -0
  61. gather_engine-1.5.0/tests/test_web.py +44 -0
  62. gather_engine-1.5.0/tests/test_xml_safety.py +56 -0
@@ -0,0 +1,78 @@
1
+ Gather Fair-Source License, Version 1.0
2
+
3
+ Copyright (c) 2026 Zain Dana Harper. All rights reserved.
4
+
5
+ This license governs use of the accompanying software ("the Software", the Gather research-intake organ). By using,
6
+ copying, modifying, or distributing the Software, you accept these terms. The
7
+ Software is source-available, not open source: the source is published so you can
8
+ read it, run it, and build on it, while commercial use that competes with the
9
+ project is reserved so the project can fund its own continued development.
10
+
11
+ 1. Definitions
12
+
13
+ "Licensor" means Zain Dana Harper, the copyright holder.
14
+
15
+ "You" means the individual or entity exercising rights under this license.
16
+
17
+ "Competing Use" means making the Software, or a modified version of it,
18
+ available to a third party as a commercial product or service that
19
+ substitutes for, or offers substantially the same functionality as, the
20
+ Software or any product or service the Licensor offers using the Software.
21
+
22
+ 2. Grant
23
+
24
+ Subject to your compliance with this license, the Licensor grants you a
25
+ worldwide, royalty-free, non-exclusive, non-transferable license to read,
26
+ run, copy, modify, create derivative works of, and redistribute the Software
27
+ for any Permitted Purpose.
28
+
29
+ 3. Permitted Purpose
30
+
31
+ A Permitted Purpose is any purpose other than a Competing Use. Permitted
32
+ Purposes include, without limitation: internal use within your organization;
33
+ personal use; evaluation; non-commercial education and research; and use in
34
+ providing professional services to a party that is itself using the Software
35
+ under this license.
36
+
37
+ 4. Reserved Commercial Use
38
+
39
+ A Competing Use is reserved to the Licensor and requires a separate
40
+ commercial license. This reservation is what funds the project's continued
41
+ development. To obtain a commercial license, contact the Licensor (see
42
+ Contact below).
43
+
44
+ 5. Conditions
45
+
46
+ You must retain, in all copies and derivative works you distribute, this
47
+ license, the copyright notice, and all attribution notices. You may add your
48
+ own notices to changes you make, so long as the origin of the Software is not
49
+ misrepresented.
50
+
51
+ 6. Trademarks
52
+
53
+ This license does not grant any right to use the Licensor's names, logos, or
54
+ trademarks.
55
+
56
+ 7. Disclaimer of Warranty
57
+
58
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
59
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
60
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
61
+
62
+ 8. Limitation of Liability
63
+
64
+ IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
65
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
66
+ OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
67
+ SOFTWARE.
68
+
69
+ 9. Termination
70
+
71
+ If you breach this license, your rights under it terminate automatically. They
72
+ may be reinstated by the Licensor in writing.
73
+
74
+ 10. Contact
75
+
76
+ For commercial licensing or to report a security issue, open a private
77
+ security advisory at https://github.com/HarperZ9/gather/security or reach the
78
+ Licensor via https://github.com/HarperZ9.
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: gather-engine
3
+ Version: 1.5.0
4
+ Summary: An accountable research-intake organ: ingest from scattered sources behind clean adapters, with a provenance receipt on every item and a witnessed digest out
5
+ Author: Zain Dana Harper
6
+ License: Gather Fair-Source License, Version 1.0
7
+
8
+ Copyright (c) 2026 Zain Dana Harper. All rights reserved.
9
+
10
+ This license governs use of the accompanying software ("the Software", the Gather research-intake organ). By using,
11
+ copying, modifying, or distributing the Software, you accept these terms. The
12
+ Software is source-available, not open source: the source is published so you can
13
+ read it, run it, and build on it, while commercial use that competes with the
14
+ project is reserved so the project can fund its own continued development.
15
+
16
+ 1. Definitions
17
+
18
+ "Licensor" means Zain Dana Harper, the copyright holder.
19
+
20
+ "You" means the individual or entity exercising rights under this license.
21
+
22
+ "Competing Use" means making the Software, or a modified version of it,
23
+ available to a third party as a commercial product or service that
24
+ substitutes for, or offers substantially the same functionality as, the
25
+ Software or any product or service the Licensor offers using the Software.
26
+
27
+ 2. Grant
28
+
29
+ Subject to your compliance with this license, the Licensor grants you a
30
+ worldwide, royalty-free, non-exclusive, non-transferable license to read,
31
+ run, copy, modify, create derivative works of, and redistribute the Software
32
+ for any Permitted Purpose.
33
+
34
+ 3. Permitted Purpose
35
+
36
+ A Permitted Purpose is any purpose other than a Competing Use. Permitted
37
+ Purposes include, without limitation: internal use within your organization;
38
+ personal use; evaluation; non-commercial education and research; and use in
39
+ providing professional services to a party that is itself using the Software
40
+ under this license.
41
+
42
+ 4. Reserved Commercial Use
43
+
44
+ A Competing Use is reserved to the Licensor and requires a separate
45
+ commercial license. This reservation is what funds the project's continued
46
+ development. To obtain a commercial license, contact the Licensor (see
47
+ Contact below).
48
+
49
+ 5. Conditions
50
+
51
+ You must retain, in all copies and derivative works you distribute, this
52
+ license, the copyright notice, and all attribution notices. You may add your
53
+ own notices to changes you make, so long as the origin of the Software is not
54
+ misrepresented.
55
+
56
+ 6. Trademarks
57
+
58
+ This license does not grant any right to use the Licensor's names, logos, or
59
+ trademarks.
60
+
61
+ 7. Disclaimer of Warranty
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
64
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
66
+
67
+ 8. Limitation of Liability
68
+
69
+ IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
70
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
71
+ OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
72
+ SOFTWARE.
73
+
74
+ 9. Termination
75
+
76
+ If you breach this license, your rights under it terminate automatically. They
77
+ may be reinstated by the Licensor in writing.
78
+
79
+ 10. Contact
80
+
81
+ For commercial licensing or to report a security issue, open a private
82
+ security advisory at https://github.com/HarperZ9/gather/security or reach the
83
+ Licensor via https://github.com/HarperZ9.
84
+
85
+ Project-URL: Homepage, https://github.com/HarperZ9/gather
86
+ Project-URL: Repository, https://github.com/HarperZ9/gather
87
+ Keywords: research,intake,ingestion,provenance,transcripts,arxiv,accountability,ocr,transcription,headless-browser,synthesis,web-scraping,ssrf-safe
88
+ Requires-Python: >=3.11
89
+ Description-Content-Type: text/markdown
90
+ License-File: LICENSE
91
+ Provides-Extra: dev
92
+ Requires-Dist: pytest>=8; extra == "dev"
93
+ Requires-Dist: pytest-cov>=5; extra == "dev"
94
+ Requires-Dist: ruff>=0.6; extra == "dev"
95
+ Requires-Dist: mypy>=1.10; extra == "dev"
96
+ Dynamic: license-file
97
+
98
+ # Gather
99
+
100
+ ![python: 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
101
+ ![deps: none (core)](https://img.shields.io/badge/core%20deps-none-success.svg)
102
+ ![license: fair-source](https://img.shields.io/badge/license-fair--source-blue.svg)
103
+
104
+ Research lives behind awkward access. Captions and comments on a video, papers behind an
105
+ arXiv gate, a library buried in a repo, an API with a credential wall, a fact that exists
106
+ only across scattered fragments and has to be put together. Most tools handle one of those
107
+ and break on the rest, and when they do reach something, you cannot tell later whether a
108
+ line was pulled straight from the source or pieced together along the way.
109
+
110
+ Gather is the research-intake organ that handles all of it cohesively, and records how.
111
+ It is the one place in the constellation where network access, third-party tools, and
112
+ credentials are allowed to live, isolated behind source adapters, so the rest stays clean.
113
+ Every item it brings back carries a provenance receipt, and a run emits a witnessed digest
114
+ that index, refine, and the crucible consume.
115
+
116
+ ## Reach anywhere, and say how
117
+
118
+ The aim is to pull information from anywhere, including the extremely difficult: gated APIs,
119
+ auth and paywalls, JavaScript-walled pages, scanned PDFs, audio, obscure formats, and
120
+ information that is not sitting in one place but has to be synthesized from fragments. Many of
121
+ these ship today: alongside video, web, feed, and docs, there are adapters for arXiv papers,
122
+ PDFs, authenticated JSON APIs, JavaScript-rendered pages (a headless browser), scanned images
123
+ (OCR), and audio (transcription). Each records HOW it reached the content, so the accountability
124
+ is in place before the harder reach is trusted.
125
+
126
+ Two adapters are honest about their reach in their receipts. The `web` adapter reads the static
127
+ HTML a server returns and does not run JavaScript, so a client-rendered page yields only its
128
+ shell, and `http-get` says exactly that; the `browser` adapter runs a real headless browser and
129
+ records `browser-extract`, so you know JavaScript was executed. The browser is the most exposed
130
+ edge: its host guard covers only the first navigation, and a rendered page then follows its own
131
+ redirects and sub-requests unguarded, so do not point it at untrusted URLs where internal
132
+ services are reachable (see the threat model in [ARCHITECTURE.md](ARCHITECTURE.md)).
133
+
134
+ That accountability is one rule: the receipt records how each item was obtained. A
135
+ transcript read from captions, a page read through a browser, text recognized from a scan,
136
+ speech transcribed from audio, and a fact synthesized from fragments are all valid items,
137
+ but they are not equally direct. The `method` on every item keeps that on the record
138
+ (`yt-dlp`, `browser-extract`, `ocr`, `transcribe`, `synthesized`), so a quote is never
139
+ confused with an inference, and what was hard to get is never dressed up as if it were
140
+ lying in the open.
141
+
142
+ A derived item (one assembled or inferred from other items, rather than fetched) is the
143
+ sharp case, and the receipt is built for it. Its sha256 fingerprints the inference itself,
144
+ not its sources, because it is a new statement and can only witness itself; a `derived_from`
145
+ field records the content hash of each input, a re-checkable pointer back to the exact
146
+ source content. The digest seal folds in `method` and `derived_from` alongside the hash, so
147
+ relabelling an inference as a direct fetch, or quietly rewriting what it was built from,
148
+ breaks the seal exactly as altering the content does.
149
+
150
+ The honesty is mechanical where it can be. The `method="synthesized"` label is reachable
151
+ only through the `Synthesizer` seam: the bare `gather.derive` builder defaults to `compiled`
152
+ and refuses to stamp `synthesized` at all, so a bare call can never forge a synthesis. With no
153
+ edge wired in, the default `NullSynthesizer` performs a deterministic, extractive *compilation*:
154
+ it assembles inputs verbatim, labels them `compiled`, invents nothing. What the seam attests is
155
+ that the configured edge produced the text; that the edge is actually a model is the operator's
156
+ responsibility, the same trust as choosing the browser binary or the API token (point the seam at
157
+ `cat` and you get a verbatim echo labelled `synthesized`). And `derived_from` records the inputs
158
+ supplied to the edge, an upper bound: a model may ignore some or generate beyond them, so it
159
+ attests availability, not use.
160
+
161
+ ## The discipline
162
+
163
+ - **One isolated impure edge.** Each source is a small adapter behind a single `Source`
164
+ shape: `fetch(target) -> list[Item]`. The adapter can use the network, a tool, a
165
+ credential, a browser, whatever the source demands; the rest of Gather imports none of
166
+ that. Awkward access is an adapter problem, not a system problem.
167
+ - **A receipt on every item.** Each `Item` carries a `Provenance` (source, ref, method,
168
+ time, and a sha256 of the content). Re-hash the content and you can confirm it is what
169
+ was obtained, unaltered.
170
+ - **A witnessed digest out.** A run folds its items' receipts into one re-checkable seal.
171
+ Downstream organs consume the digest; the seal lets a reader confirm it was not altered.
172
+ - **Scope to the work.** A deterministic scope filter keeps what serves the theses and
173
+ drops the rest, and records how many it dropped.
174
+ - **A peer, not a feature.** Gather is deliberately impure, so it is not part of index
175
+ (which is zero-dependency, offline, and deterministic, and would forfeit exactly that
176
+ if it grew a scraper). It composes through the digest seam, the way Forum does.
177
+
178
+ ## Install
179
+
180
+ ```bash
181
+ pip install gather-engine
182
+ ```
183
+
184
+ The distribution is `gather-engine`; it installs the `gather` command and the `gather` package
185
+ (`import gather`). The core is pure standard library; a few adapters call an external tool
186
+ (`yt-dlp`, `pdftotext`, a headless `chromium`, `tesseract`, `whisper`), which you install only if
187
+ you use that adapter.
188
+
189
+ ## Watch it work
190
+
191
+ `examples/demo.py` parses an already-harvested video (a yt-dlp `info.json` plus its `.vtt`
192
+ captions) into items, each with a provenance receipt, scope-filters them, folds them into a
193
+ witnessed digest, then tampers with one receipt to show the seal catch it. All offline, no
194
+ install, nothing downloaded:
195
+
196
+ ```bash
197
+ python examples/demo.py # one video parsed, scoped, digested, then a receipt catches tampering
198
+ python examples/pipeline.py # the whole organ: run -> store -> verify -> recall, offline
199
+ ```
200
+
201
+ ```
202
+ parsed 3 items from one video, each with a receipt:
203
+ metadata abc123 sha256=40d9839ffb0e... verify=True
204
+ transcript abc123 sha256=f798cd2c334c... verify=True
205
+ comment c1 sha256=301cf39d5091... verify=True
206
+
207
+ scope to ['tile','monotile']: kept 3, dropped 0
208
+ witnessed digest: 3 receipts, seal 7da7dc456b11..., verified True
209
+
210
+ after tampering one receipt, digest verifies: False <- caught
211
+ ```
212
+
213
+ (The hash and seal prefixes above are illustrative; the load-bearing facts are the `verify`
214
+ results, which the test suite pins.)
215
+
216
+ The `gather` CLI fetches live from each adapter; every fetch command takes the same `--scope`,
217
+ `--json`, and `--store` (the `run` and `corpus` commands are driven by a config file and
218
+ sub-actions instead). Of the commands shown here, web/feed/docs are pure standard library and only
219
+ `video` needs an external tool (`yt-dlp`); the harder adapters (pdf, browser, ocr, transcribe) each
220
+ shell out to their own external tool, never a Python dependency, as the module list notes:
221
+
222
+ ```bash
223
+ gather docs ./research-notes --scope "rubik,group theory" # local files, offline
224
+ gather web "https://example.com/article" --store ./corpus # static page, kept in a corpus
225
+ gather feed "https://example.com/feed.xml" --json # RSS or Atom
226
+ gather arxiv "aperiodic monotile" --store ./corpus # papers (abstracts + metadata)
227
+ gather video "https://youtu.be/<id>" --comments --scope "rubik,group theory"
228
+ gather corpus verify ./corpus # re-hash every stored body
229
+ ```
230
+
231
+ Any command takes `--store DIR` to persist what it gathered into a content-addressed corpus,
232
+ and `gather corpus list|verify|digest|search DIR` inspects it. `verify` re-hashes every stored
233
+ body against its receipt and exits non-zero if anything is missing or corrupt. `search` matches
234
+ its terms as case-insensitive substrings of title and body (so `art` also matches `cartesian`).
235
+ Write a corpus from one process at a time: the dedup is single-writer, and `prune` (which reads
236
+ the catalog then deletes unreferenced objects) must likewise run with no concurrent writer.
237
+
238
+ ## What's here
239
+
240
+ - `gather.item`: an `Item` and its `Provenance` receipt (with `derived_from` for inferences); `make_item` computes the receipt from the content.
241
+ - `gather.source`: the `Source` adapter shape (the isolated impure edge) and a `Catalog` of what was gathered.
242
+ - `gather.scope`: the scope-to-telos filter, deterministic and order-preserving.
243
+ - `gather.digest`: the witnessed, provenance-stamped digest with a re-checkable seal (folds in `method` and `derived_from`).
244
+ - `gather.derive`: the derive seam, building a derived item with `derived_from`; a `Synthesizer` seam whose `NullSynthesizer` default compiles verbatim (never fabricates a synthesis).
245
+ - `gather.net`: the single network transport (`http_get`, urllib.request, + pure `decode_body`). HTTP transport lives here and in adapter fetches, nowhere else; pure URL string-building (urllib.parse) may live in an adapter.
246
+ - `gather.video`: video intake via `yt-dlp`. Pure parsing, impure shell.
247
+ - `gather.web`: static web pages via http(s); pure HTML-to-text, no JavaScript.
248
+ - `gather.feed`: RSS and Atom feeds; pure parser handles both.
249
+ - `gather.docs`: local text files or a directory of them; the impure edge is the filesystem.
250
+ - `gather.arxiv`: papers from the arXiv API by id or query; pure parser, the Item carries the abstract and metadata.
251
+ - `gather.pdf`: text from a local PDF via `pdftotext` (an external tool, not a dependency); a best-effort reading, labelled as such.
252
+ - `gather.store`: a durable, content-addressed `Corpus`. Bodies are deduped by hash while every distinct receipt is kept (no provenance dropped); the catalog streams; `verify` re-hashes every stored body (MATCH/MISSING/CORRUPT); the run history is kept too.
253
+ - `gather.run`: the witnessed gather session. `gather_run` orchestrates fetch, scope, optional synthesis, digest, and store into one re-checkable `RunRecord` (its own seal plus the items' digest seal); the scope and synthesizer are composition seams that default to Null so the run stands alone.
254
+ - `gather.recall`: a `Query` over a stored corpus (substring scope terms, plus source/kind/method filters: OR within a filter, AND across) returning reconstructed items that are re-verified (missing or corrupt bodies are skipped and reported), so downstream organs draw scoped, trustworthy subsets.
255
+ - `gather.credentials`: the one place secrets enter, read from the environment by name, never logged, never put in a receipt or a URL.
256
+ - `gather.api`: an authenticated JSON-API adapter, the worked example of the credentials pattern (token from env, sent as a header, never witnessed).
257
+ - `gather.browser`: JavaScript-rendered pages via a headless browser; the `browser-extract` method records that JS was run.
258
+ - `gather.ocr`: text from a scanned image via `tesseract`; a machine reading, labelled `ocr`.
259
+ - `gather.transcribe`: a transcript from audio via a Whisper-style CLI; a machine transcription, labelled `transcribe`.
260
+ - `gather.model`: the real model edge for the synthesizer seam; shells to a model CLI (prompt on stdin), stamping a genuine `synthesized` inference, `derived_from` set.
261
+ - `gather.provenance`: the `ProvenanceProvider` seam, composing an external origin verdict (forged? re-encode? authentic?) per item; the `Null` default stands alone, a subprocess edge calls an external provenance organ. Verdicts are sealed into the run record.
262
+ - `gather.method`: the method ladder. Classifies a method as direct or derived, and `make_item` enforces it: a fetched item cannot carry a derivation chain and a synthesized one cannot lack it.
263
+ - `gather.cli`: a `gather` command (`parse`/`docs`/`pdf` offline, `web`/`feed`/`video`/`arxiv`/`api`/`browser`/`ocr`/`transcribe` live), every command takes `--store DIR`; plus `run` and `corpus list/verify/digest/runs/search/stats/prune`.
264
+ - `gather.commands`: the command implementations behind the CLI surface (split from `cli` so no module exceeds the size budget).
265
+
266
+ The core is pure standard library. A source adapter may pull in whatever its source
267
+ demands, isolated behind the `Source` shape.
268
+
269
+ [ARCHITECTURE.md](ARCHITECTURE.md) is the design map (the seams, the receipt, the corpus, the
270
+ run, the threat model); [CHANGELOG.md](CHANGELOG.md) is the version history.
271
+
272
+ ## Roadmap
273
+
274
+ Shipped:
275
+
276
+ - The provenance receipt, the scope filter, the witnessed digest with a re-checkable seal, the catalog.
277
+ - Adapters behind one `Source` shape: video (`yt-dlp`), web (static http), feed (RSS/Atom), docs (local files), arXiv (papers), PDF (`pdftotext`), authenticated JSON APIs (env-isolated credentials).
278
+ - The derive seam: the `Synthesizer` shape with an honest compiling default and a real model edge (`gather.model`); a model produces `synthesized`, the default produces `compiled`, nothing fabricates.
279
+ - A durable, content-addressed corpus (`--store DIR`): bodies deduped by hash, the catalog streamed, and `corpus verify` re-hashing every stored body against its receipt.
280
+ - A witnessed gather run (`gather run config.json`): orchestrates many sources, scope, and optional synthesis into one re-checkable record, kept in the corpus run history.
281
+ - Recall over the corpus (`gather corpus search`): query by scope terms and source/kind/method, returning re-verifiable items and a scoped digest.
282
+ - Isolated credentials (env-only, never witnessed) with an authenticated-API adapter, and the method ladder enforced at construction (a fetch cannot claim inputs, a synthesis cannot lack them).
283
+ - The hard sources behind the same seam, as isolated external-tool edges: JavaScript pages (headless browser), scanned images (OCR), and audio (transcription).
284
+ - A real model edge for the synthesizer seam, and a provenance-composition seam that folds an external origin verdict per item into the witnessed run.
285
+
286
+ Gather reached its organic completion at 1.5.0: every planned source and seam is shipped, and the
287
+ accountability claims hold end to end across a final whole-system review. The item below is a scale
288
+ optimization, not missing function.
289
+
290
+ Possible future work (not required for the completion milestone):
291
+
292
+ - Corpus indexing so recall need not read every body at large scale.
293
+
294
+ ## License
295
+
296
+ Gather is fair-source: the code is open to read, run, and build on, with commercial use
297
+ reserved so the project can fund its own development. Copyright stays with the author. See
298
+ [LICENSE](LICENSE) for the exact terms.
@@ -0,0 +1,201 @@
1
+ # Gather
2
+
3
+ ![python: 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
4
+ ![deps: none (core)](https://img.shields.io/badge/core%20deps-none-success.svg)
5
+ ![license: fair-source](https://img.shields.io/badge/license-fair--source-blue.svg)
6
+
7
+ Research lives behind awkward access. Captions and comments on a video, papers behind an
8
+ arXiv gate, a library buried in a repo, an API with a credential wall, a fact that exists
9
+ only across scattered fragments and has to be put together. Most tools handle one of those
10
+ and break on the rest, and when they do reach something, you cannot tell later whether a
11
+ line was pulled straight from the source or pieced together along the way.
12
+
13
+ Gather is the research-intake organ that handles all of it cohesively, and records how.
14
+ It is the one place in the constellation where network access, third-party tools, and
15
+ credentials are allowed to live, isolated behind source adapters, so the rest stays clean.
16
+ Every item it brings back carries a provenance receipt, and a run emits a witnessed digest
17
+ that index, refine, and the crucible consume.
18
+
19
+ ## Reach anywhere, and say how
20
+
21
+ The aim is to pull information from anywhere, including the extremely difficult: gated APIs,
22
+ auth and paywalls, JavaScript-walled pages, scanned PDFs, audio, obscure formats, and
23
+ information that is not sitting in one place but has to be synthesized from fragments. Many of
24
+ these ship today: alongside video, web, feed, and docs, there are adapters for arXiv papers,
25
+ PDFs, authenticated JSON APIs, JavaScript-rendered pages (a headless browser), scanned images
26
+ (OCR), and audio (transcription). Each records HOW it reached the content, so the accountability
27
+ is in place before the harder reach is trusted.
28
+
29
+ Two adapters are honest about their reach in their receipts. The `web` adapter reads the static
30
+ HTML a server returns and does not run JavaScript, so a client-rendered page yields only its
31
+ shell, and `http-get` says exactly that; the `browser` adapter runs a real headless browser and
32
+ records `browser-extract`, so you know JavaScript was executed. The browser is the most exposed
33
+ edge: its host guard covers only the first navigation, and a rendered page then follows its own
34
+ redirects and sub-requests unguarded, so do not point it at untrusted URLs where internal
35
+ services are reachable (see the threat model in [ARCHITECTURE.md](ARCHITECTURE.md)).
36
+
37
+ That accountability is one rule: the receipt records how each item was obtained. A
38
+ transcript read from captions, a page read through a browser, text recognized from a scan,
39
+ speech transcribed from audio, and a fact synthesized from fragments are all valid items,
40
+ but they are not equally direct. The `method` on every item keeps that on the record
41
+ (`yt-dlp`, `browser-extract`, `ocr`, `transcribe`, `synthesized`), so a quote is never
42
+ confused with an inference, and what was hard to get is never dressed up as if it were
43
+ lying in the open.
44
+
45
+ A derived item (one assembled or inferred from other items, rather than fetched) is the
46
+ sharp case, and the receipt is built for it. Its sha256 fingerprints the inference itself,
47
+ not its sources, because it is a new statement and can only witness itself; a `derived_from`
48
+ field records the content hash of each input, a re-checkable pointer back to the exact
49
+ source content. The digest seal folds in `method` and `derived_from` alongside the hash, so
50
+ relabelling an inference as a direct fetch, or quietly rewriting what it was built from,
51
+ breaks the seal exactly as altering the content does.
52
+
53
+ The honesty is mechanical where it can be. The `method="synthesized"` label is reachable
54
+ only through the `Synthesizer` seam: the bare `gather.derive` builder defaults to `compiled`
55
+ and refuses to stamp `synthesized` at all, so a bare call can never forge a synthesis. With no
56
+ edge wired in, the default `NullSynthesizer` performs a deterministic, extractive *compilation*:
57
+ it assembles inputs verbatim, labels them `compiled`, invents nothing. What the seam attests is
58
+ that the configured edge produced the text; that the edge is actually a model is the operator's
59
+ responsibility, the same trust as choosing the browser binary or the API token (point the seam at
60
+ `cat` and you get a verbatim echo labelled `synthesized`). And `derived_from` records the inputs
61
+ supplied to the edge, an upper bound: a model may ignore some or generate beyond them, so it
62
+ attests availability, not use.
63
+
64
+ ## The discipline
65
+
66
+ - **One isolated impure edge.** Each source is a small adapter behind a single `Source`
67
+ shape: `fetch(target) -> list[Item]`. The adapter can use the network, a tool, a
68
+ credential, a browser, whatever the source demands; the rest of Gather imports none of
69
+ that. Awkward access is an adapter problem, not a system problem.
70
+ - **A receipt on every item.** Each `Item` carries a `Provenance` (source, ref, method,
71
+ time, and a sha256 of the content). Re-hash the content and you can confirm it is what
72
+ was obtained, unaltered.
73
+ - **A witnessed digest out.** A run folds its items' receipts into one re-checkable seal.
74
+ Downstream organs consume the digest; the seal lets a reader confirm it was not altered.
75
+ - **Scope to the work.** A deterministic scope filter keeps what serves the theses and
76
+ drops the rest, and records how many it dropped.
77
+ - **A peer, not a feature.** Gather is deliberately impure, so it is not part of index
78
+ (which is zero-dependency, offline, and deterministic, and would forfeit exactly that
79
+ if it grew a scraper). It composes through the digest seam, the way Forum does.
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install gather-engine
85
+ ```
86
+
87
+ The distribution is `gather-engine`; it installs the `gather` command and the `gather` package
88
+ (`import gather`). The core is pure standard library; a few adapters call an external tool
89
+ (`yt-dlp`, `pdftotext`, a headless `chromium`, `tesseract`, `whisper`), which you install only if
90
+ you use that adapter.
91
+
92
+ ## Watch it work
93
+
94
+ `examples/demo.py` parses an already-harvested video (a yt-dlp `info.json` plus its `.vtt`
95
+ captions) into items, each with a provenance receipt, scope-filters them, folds them into a
96
+ witnessed digest, then tampers with one receipt to show the seal catch it. All offline, no
97
+ install, nothing downloaded:
98
+
99
+ ```bash
100
+ python examples/demo.py # one video parsed, scoped, digested, then a receipt catches tampering
101
+ python examples/pipeline.py # the whole organ: run -> store -> verify -> recall, offline
102
+ ```
103
+
104
+ ```
105
+ parsed 3 items from one video, each with a receipt:
106
+ metadata abc123 sha256=40d9839ffb0e... verify=True
107
+ transcript abc123 sha256=f798cd2c334c... verify=True
108
+ comment c1 sha256=301cf39d5091... verify=True
109
+
110
+ scope to ['tile','monotile']: kept 3, dropped 0
111
+ witnessed digest: 3 receipts, seal 7da7dc456b11..., verified True
112
+
113
+ after tampering one receipt, digest verifies: False <- caught
114
+ ```
115
+
116
+ (The hash and seal prefixes above are illustrative; the load-bearing facts are the `verify`
117
+ results, which the test suite pins.)
118
+
119
+ The `gather` CLI fetches live from each adapter; every fetch command takes the same `--scope`,
120
+ `--json`, and `--store` (the `run` and `corpus` commands are driven by a config file and
121
+ sub-actions instead). Of the commands shown here, web/feed/docs are pure standard library and only
122
+ `video` needs an external tool (`yt-dlp`); the harder adapters (pdf, browser, ocr, transcribe) each
123
+ shell out to their own external tool, never a Python dependency, as the module list notes:
124
+
125
+ ```bash
126
+ gather docs ./research-notes --scope "rubik,group theory" # local files, offline
127
+ gather web "https://example.com/article" --store ./corpus # static page, kept in a corpus
128
+ gather feed "https://example.com/feed.xml" --json # RSS or Atom
129
+ gather arxiv "aperiodic monotile" --store ./corpus # papers (abstracts + metadata)
130
+ gather video "https://youtu.be/<id>" --comments --scope "rubik,group theory"
131
+ gather corpus verify ./corpus # re-hash every stored body
132
+ ```
133
+
134
+ Any command takes `--store DIR` to persist what it gathered into a content-addressed corpus,
135
+ and `gather corpus list|verify|digest|search DIR` inspects it. `verify` re-hashes every stored
136
+ body against its receipt and exits non-zero if anything is missing or corrupt. `search` matches
137
+ its terms as case-insensitive substrings of title and body (so `art` also matches `cartesian`).
138
+ Write a corpus from one process at a time: the dedup is single-writer, and `prune` (which reads
139
+ the catalog then deletes unreferenced objects) must likewise run with no concurrent writer.
140
+
141
+ ## What's here
142
+
143
+ - `gather.item`: an `Item` and its `Provenance` receipt (with `derived_from` for inferences); `make_item` computes the receipt from the content.
144
+ - `gather.source`: the `Source` adapter shape (the isolated impure edge) and a `Catalog` of what was gathered.
145
+ - `gather.scope`: the scope-to-telos filter, deterministic and order-preserving.
146
+ - `gather.digest`: the witnessed, provenance-stamped digest with a re-checkable seal (folds in `method` and `derived_from`).
147
+ - `gather.derive`: the derive seam, building a derived item with `derived_from`; a `Synthesizer` seam whose `NullSynthesizer` default compiles verbatim (never fabricates a synthesis).
148
+ - `gather.net`: the single network transport (`http_get`, urllib.request, + pure `decode_body`). HTTP transport lives here and in adapter fetches, nowhere else; pure URL string-building (urllib.parse) may live in an adapter.
149
+ - `gather.video`: video intake via `yt-dlp`. Pure parsing, impure shell.
150
+ - `gather.web`: static web pages via http(s); pure HTML-to-text, no JavaScript.
151
+ - `gather.feed`: RSS and Atom feeds; pure parser handles both.
152
+ - `gather.docs`: local text files or a directory of them; the impure edge is the filesystem.
153
+ - `gather.arxiv`: papers from the arXiv API by id or query; pure parser, the Item carries the abstract and metadata.
154
+ - `gather.pdf`: text from a local PDF via `pdftotext` (an external tool, not a dependency); a best-effort reading, labelled as such.
155
+ - `gather.store`: a durable, content-addressed `Corpus`. Bodies are deduped by hash while every distinct receipt is kept (no provenance dropped); the catalog streams; `verify` re-hashes every stored body (MATCH/MISSING/CORRUPT); the run history is kept too.
156
+ - `gather.run`: the witnessed gather session. `gather_run` orchestrates fetch, scope, optional synthesis, digest, and store into one re-checkable `RunRecord` (its own seal plus the items' digest seal); the scope and synthesizer are composition seams that default to Null so the run stands alone.
157
+ - `gather.recall`: a `Query` over a stored corpus (substring scope terms, plus source/kind/method filters: OR within a filter, AND across) returning reconstructed items that are re-verified (missing or corrupt bodies are skipped and reported), so downstream organs draw scoped, trustworthy subsets.
158
+ - `gather.credentials`: the one place secrets enter, read from the environment by name, never logged, never put in a receipt or a URL.
159
+ - `gather.api`: an authenticated JSON-API adapter, the worked example of the credentials pattern (token from env, sent as a header, never witnessed).
160
+ - `gather.browser`: JavaScript-rendered pages via a headless browser; the `browser-extract` method records that JS was run.
161
+ - `gather.ocr`: text from a scanned image via `tesseract`; a machine reading, labelled `ocr`.
162
+ - `gather.transcribe`: a transcript from audio via a Whisper-style CLI; a machine transcription, labelled `transcribe`.
163
+ - `gather.model`: the real model edge for the synthesizer seam; shells to a model CLI (prompt on stdin), stamping a genuine `synthesized` inference, `derived_from` set.
164
+ - `gather.provenance`: the `ProvenanceProvider` seam, composing an external origin verdict (forged? re-encode? authentic?) per item; the `Null` default stands alone, a subprocess edge calls an external provenance organ. Verdicts are sealed into the run record.
165
+ - `gather.method`: the method ladder. Classifies a method as direct or derived, and `make_item` enforces it: a fetched item cannot carry a derivation chain and a synthesized one cannot lack it.
166
+ - `gather.cli`: a `gather` command (`parse`/`docs`/`pdf` offline, `web`/`feed`/`video`/`arxiv`/`api`/`browser`/`ocr`/`transcribe` live), every command takes `--store DIR`; plus `run` and `corpus list/verify/digest/runs/search/stats/prune`.
167
+ - `gather.commands`: the command implementations behind the CLI surface (split from `cli` so no module exceeds the size budget).
168
+
169
+ The core is pure standard library. A source adapter may pull in whatever its source
170
+ demands, isolated behind the `Source` shape.
171
+
172
+ [ARCHITECTURE.md](ARCHITECTURE.md) is the design map (the seams, the receipt, the corpus, the
173
+ run, the threat model); [CHANGELOG.md](CHANGELOG.md) is the version history.
174
+
175
+ ## Roadmap
176
+
177
+ Shipped:
178
+
179
+ - The provenance receipt, the scope filter, the witnessed digest with a re-checkable seal, the catalog.
180
+ - Adapters behind one `Source` shape: video (`yt-dlp`), web (static http), feed (RSS/Atom), docs (local files), arXiv (papers), PDF (`pdftotext`), authenticated JSON APIs (env-isolated credentials).
181
+ - The derive seam: the `Synthesizer` shape with an honest compiling default and a real model edge (`gather.model`); a model produces `synthesized`, the default produces `compiled`, nothing fabricates.
182
+ - A durable, content-addressed corpus (`--store DIR`): bodies deduped by hash, the catalog streamed, and `corpus verify` re-hashing every stored body against its receipt.
183
+ - A witnessed gather run (`gather run config.json`): orchestrates many sources, scope, and optional synthesis into one re-checkable record, kept in the corpus run history.
184
+ - Recall over the corpus (`gather corpus search`): query by scope terms and source/kind/method, returning re-verifiable items and a scoped digest.
185
+ - Isolated credentials (env-only, never witnessed) with an authenticated-API adapter, and the method ladder enforced at construction (a fetch cannot claim inputs, a synthesis cannot lack them).
186
+ - The hard sources behind the same seam, as isolated external-tool edges: JavaScript pages (headless browser), scanned images (OCR), and audio (transcription).
187
+ - A real model edge for the synthesizer seam, and a provenance-composition seam that folds an external origin verdict per item into the witnessed run.
188
+
189
+ Gather reached its organic completion at 1.5.0: every planned source and seam is shipped, and the
190
+ accountability claims hold end to end across a final whole-system review. The item below is a scale
191
+ optimization, not missing function.
192
+
193
+ Possible future work (not required for the completion milestone):
194
+
195
+ - Corpus indexing so recall need not read every body at large scale.
196
+
197
+ ## License
198
+
199
+ Gather is fair-source: the code is open to read, run, and build on, with commercial use
200
+ reserved so the project can fund its own development. Copyright stays with the author. See
201
+ [LICENSE](LICENSE) for the exact terms.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gather-engine"
7
+ version = "1.5.0"
8
+ description = "An accountable research-intake organ: ingest from scattered sources behind clean adapters, with a provenance receipt on every item and a witnessed digest out"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Zain Dana Harper" }]
13
+ keywords = [
14
+ "research", "intake", "ingestion", "provenance", "transcripts", "arxiv", "accountability",
15
+ "ocr", "transcription", "headless-browser", "synthesis", "web-scraping", "ssrf-safe",
16
+ ]
17
+ dependencies = []
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.10"]
21
+
22
+ [project.scripts]
23
+ gather = "gather.cli:main"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/HarperZ9/gather"
27
+ Repository = "https://github.com/HarperZ9/gather"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+
32
+ [tool.pytest.ini_options]
33
+ pythonpath = ["src"]
34
+ testpaths = ["tests"]
35
+
36
+ [tool.ruff]
37
+ target-version = "py311"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E4", "E7", "E9", "F", "I"]
41
+
42
+ [tool.mypy]
43
+ python_version = "3.11"
44
+ files = ["src/gather"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+