Skip to content

Commit e09e126

Browse files
committed
write-up of findings to revisit later
1 parent cd1e0fe commit e09e126

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Datasource ↔ worker resolution: context dump & handoff
2+
3+
**Parked:** 2026-07-07 · **Work branch:** `claude/practical-poincare-6d7b3d`
4+
**Status:** exploration parked, NOT for merge as-is. Revisit after `output=Datasource` lands via `redesign`.
5+
6+
## Why this is parked
7+
8+
We set out to fix one bug (X/Twitter imports missing from the results filter) and ended up doing a
9+
broad "centralize the `-search`/`-import` convention" sweep. On review, the sweep centralizes the
10+
*lookup* but not the *convention itself*, and true centralization depends on structural facts that
11+
the `output=Datasource` redesign is about to change. Rather than refactor against a moving target,
12+
we're parking the full exploration here and will:
13+
14+
1. Wait for `output=Datasource` (the `outputs.py` archetypes) to merge via `redesign`.
15+
2. Revisit with a forward-looking plan (see **Forward plan** below).
16+
3. In the meantime, land only the **minimal fix for actually-broken things** (see **Minimal fix**).
17+
18+
---
19+
20+
## The original bug
21+
22+
X/Twitter datasets imported via Zeeschuimer did **not** appear in the data-source filter dropdown on
23+
the results/overview page. (Also missing from the front-page "Zeeschuimer imports" group, and the
24+
`/data-overview/twitter` page rendered a degraded panel.)
25+
26+
### Root cause
27+
28+
`ModuleCollector.expand_datasources()` derived each datasource's worker as `{datasource_id}-search`
29+
only. The X/Twitter datasource has `DATASOURCE = "twitter"` but its worker's `type` is
30+
**`twitter-import`**, so `workers.get("twitter-search")` returned `None``has_worker=False`,
31+
`importable=False` → filtered out of the dropdown entirely.
32+
33+
---
34+
35+
## Key domain facts (the non-obvious stuff — read before touching this again)
36+
37+
1. **The `-search`/`-import` suffix is NOT semantic.** All 15 Zeeschuimer datasources
38+
(`is_from_zeeschuimer = True`) are import-only — every one raises `NotImplementedError` in
39+
`get_items`. **`twitter` is the *only* one named `-import`**; its 14 siblings (tiktok, instagram,
40+
threads, gab, facebook, pinterest, …) are equally import-only but named `-search`. What actually
41+
distinguishes import-vs-search behavior is `is_from_zeeschuimer` / per-worker `validate_query`,
42+
never the suffix.
43+
44+
2. **Renaming `twitter-import``twitter-search` is NOT an option.** Existing datasets store
45+
`type = "twitter-import"`. A rename would make every existing X/Twitter dataset render as
46+
"(Deprecated analysis)" (`item.type not in processors`, see `result-child.html`). This is why the
47+
fix must live in the loader, not the worker.
48+
49+
3. **The suffix fuses two separate facts into one string:**
50+
`twitter` + `-import` = *(which datasource)* + *(it is a collector/importer)*.
51+
52+
4. **A worker has NO `datasource` attribute — only `type`.** The explicit `datasource` value lives on
53+
**DataSets** (`dataset.parameters["datasource"]`, `dataset.py:1548`). So the *only* link from a
54+
worker back to its datasource id is the string prefix. **Some string lookup is therefore
55+
unavoidable** — there is no other bridge today. (`worker.prefix` exists on some workers but is a
56+
DB-table prefix, e.g. `4chan`, not the datasource identity.)
57+
58+
5. **`is_from_collector()` can't cleanly become `issubclass(cls, Search)`** because `search.py`
59+
imports `BasicProcessor` *from* `processor.py` — referencing `Search` in `processor.py` is a
60+
circular import. So it sniffs the suffix (`processor.py:1082`), exactly like `is_filter()` sniffs
61+
the category (which already has a `:todo: make this more robust`).
62+
63+
6. **The Zeeschuimer import itself already worked.** The import endpoint already tried
64+
`(f"{platform}-import", f"{platform}-search")`. The bug was purely in metadata/UI derivation, not
65+
in the import path.
66+
67+
---
68+
69+
## What is ACTUALLY broken vs cosmetic (drives the minimal fix)
70+
71+
**Actually broken (web-UI visible) for twitter:**
72+
- Results-page datasource filter dropdown — twitter absent. *(the reported bug)*
73+
- Front page — twitter miscategorized (shown as collectable, or absent from Zeeschuimer group).
74+
- `/data-overview/twitter` — degraded: no "zeeschuimer" label, no references, empty example keys.
75+
76+
**Broken but API-only (no web-UI caller):**
77+
- `GET /api/processor-options/twitter/``processor_type + "-search"` = `twitter-search` → 404. The
78+
web UI never calls processor-options with a datasource id (it uses `/api/datasource-form/`), so
79+
this only affected public-API callers.
80+
81+
**NOT broken for twitter (worked already, or harmless):**
82+
- Zeeschuimer import endpoint — already handled both suffixes.
83+
- `manager.validate_datasources` — already checked both suffixes.
84+
- `datasource_metrics` — twitter isn't `is_local`, so it's skipped regardless.
85+
- `manipulate_settings` label map — twitter defines no settings, so its label key is unused.
86+
- `getboards` — twitter has no boards → returns False regardless.
87+
- `check_search_queue` SQL (`LIKE '%-search'`) — only misses in-progress `-import` jobs in a count
88+
display; barely visible.
89+
- `api_standalone` `endswith("-search")` — the adjacent `issubclass(Search)` already catches
90+
twitter-import.
91+
92+
---
93+
94+
## Minimal fix (do this after the revisit — ONLY the broken things)
95+
96+
Just three edits resolve every web-UI-visible bug:
97+
98+
1. Add `ModuleCollector.get_datasource_worker(datasource_id)` — try `{id}-search`, then `{id}-import`.
99+
2. `expand_datasources()` uses it for `has_worker` / `has_options` / `importable`.
100+
*(fixes the filter dropdown + front-page categorization)*
101+
3. `data_overview` (`views_misc.py`) uses it for `worker_class` and example-keys `dataset_type`.
102+
*(fixes the info page)*
103+
104+
Everything beyond these three is centralization/polish and should be deferred (see below).
105+
Optionally also make `get_processor_options`'s datasource branch use the helper (one line) if you
106+
care about the API-only case — but the web UI does not need it.
107+
108+
---
109+
110+
## The full sweep we did this session (PARKED on this branch — do not merge as-is)
111+
112+
For the record, so you know what's in the diff:
113+
114+
- `common/lib/module_loader.py`: `get_datasource_worker()` helper; `expand_datasources` uses it and
115+
now also stores `datasources[id]["worker_type"] = worker.type`.
116+
- `webtool/views/views_misc.py`: `data_overview` + `getboards` use the helper / `worker.type`.
117+
- `webtool/views/api_tool.py`:
118+
- dropped `get_processor_options`'s datasource-id overload (it's processor-only again),
119+
- added `GET /api/datasource-options/<id>/` (JSON options for a datasource — the counterpart to
120+
`/api/processor-options/`),
121+
- `datasource_form`, the import endpoint, queue-query, and `_get_search_class` all route through
122+
the helper,
123+
- `check_search_queue` SQL now matches `-import` too.
124+
- `webtool/views/views_admin.py`: `manipulate_settings` reads `worker_type` from metadata instead of
125+
deriving `id + "-search"`.
126+
- `backend/workers/datasource_metrics.py`: uses the resolved `worker.type` for the boards config key
127+
(dropped a dead `4chan→fourchan` translation).
128+
- `backend/lib/manager.py`: `validate_datasources` uses the helper.
129+
- `webtool/views/api_standalone.py`: uses the canonical `processor.is_from_collector()` (adds
130+
`-import`, keeps the `issubclass(Search)` net).
131+
132+
---
133+
134+
## The design tension (why the sweep isn't "done")
135+
136+
We centralized the **lookup** (`get_datasource_worker`) and cached its answer (`worker_type`). We did
137+
NOT centralize the **definition of the convention** — the literal `("-search", "-import")` pair is
138+
still hardcoded in ~6 places, each doing a *different* operation:
139+
140+
| Operation | Where |
141+
|---|---|
142+
| resolve worker (id → worker) | `module_loader.get_datasource_worker` |
143+
| classify collector (worker) | `processor.is_from_collector` (`processor.py:1082`) |
144+
| classify collector (dataset) | `dataset.py:2335` |
145+
| strip type → id (display) | `template_filters.py:314` |
146+
| match in SQL | `views_admin.py` + `api_tool.py` `LIKE` clauses |
147+
| assert the convention | `tests/test_modules.py` |
148+
149+
If the convention ever changed, you'd edit ~6 spots. That's the real "not centralized."
150+
151+
---
152+
153+
## Forward plan (revisit after `output=Datasource` merges via `redesign`)
154+
155+
**What `output=Datasource` changes:** on the `output-shape` branch, `backend/lib/search.py` sets
156+
`output = Datasource()` (from `common/lib/outputs.py`). That's an **explicit declaration that a worker
157+
produces a datasource** — i.e. the "is this a collector?" fact, stated as data instead of inferred
158+
from a suffix.
159+
160+
**So after it merges, the classification sites can improve:**
161+
- `is_from_collector()` (and `dataset.py:2335`, `api_standalone.py`) could key off
162+
`isinstance(worker.output, Datasource)` instead of `type.endswith(...)`. This also sidesteps the
163+
circular-import block on the `issubclass(Search)` route.
164+
165+
**But `output` does NOT solve resolution.** `output=Datasource` says "*is* a datasource collector",
166+
not "*belongs to* datasource X". So the id → worker mapping (`get_datasource_worker`) still needs the
167+
exact-id lookup — and it must stay exact (`{id}-search`/`{id}-import`), because datasources share
168+
prefixes (`tiktok` vs `tiktok-comments` vs `tiktok-urls`), so a prefix scan would be ambiguous.
169+
170+
**The real structural fix (independent of `output`):** give collector workers an explicit back-link
171+
so the suffix is parsed exactly once. The `ModuleCollector` already pairs datasources ↔ workers at
172+
load; it could set `worker.datasource = datasource_id` (and a collector flag) there. Then:
173+
- worker → id = attribute read (no strip),
174+
- is-collector = flag or `isinstance(output, Datasource)` (no suffix),
175+
- id → worker = dict lookup,
176+
- and the suffix convention lives in ONE place (the loader's wiring).
177+
178+
**Revisit agenda:**
179+
1. Land the **minimal fix** (3 edits above) so the actual bug is gone regardless of redesign timing.
180+
2. After `output=Datasource` merges: move the *classification* sites onto `isinstance(output, Datasource)`.
181+
3. Decide whether to add the explicit `worker.datasource` back-link (kills the type→id strip and the
182+
remaining suffix duplication). Scope as its own small PR.
183+
4. Consider a single `COLLECTOR_SUFFIXES = ("-search", "-import")` constant for whatever string sites
184+
genuinely remain (SQL can reference it via query building).
185+
186+
---
187+
188+
## Verification checklist (for whenever we land the real change)
189+
190+
**Automated (run first, in Docker — Python 3.9 host can't run the suite):**
191+
- [ ] `pytest tests/test_modules.py` — module loading + naming-convention assertions. Main guard for
192+
the `module_loader` changes. (No integration/route tests exist; everything below is manual.)
193+
194+
**Manual — the bug & core UI:**
195+
- [ ] Results-page datasource filter shows **X/Twitter**; filtering by it returns twitter datasets.
196+
- [ ] Other datasources still appear + filter correctly.
197+
- [ ] Front page: X/Twitter under **"Zeeschuimer imports"**, not the collectable list.
198+
- [ ] `/data-overview/twitter` loads with the **zeeschuimer** label + references; `/data-overview/fourchan`
199+
still shows boards/metrics.
200+
201+
**Manual — create/import (highest-risk paths):**
202+
- [ ] **Zeeschuimer import of X/Twitter end-to-end** creates a dataset.
203+
- [ ] **Zeeschuimer import of a `-search` source** (TikTok/Instagram) still works (regression).
204+
- [ ] Create-dataset form for a real search source (Bluesky/Tumblr): form loads, search queues.
205+
- [ ] 4chan/8chan: boards populate in the create form.
206+
207+
**Manual — admin & metrics:**
208+
- [ ] `/admin/settings` loads; setting groups show correct datasource **names** (twitterv2, bsky, fourchan).
209+
- [ ] Backend startup log has **no** spurious "No search worker defined" errors.
210+
- [ ] 4chan metrics still compute (data-overview graph populates).
211+
212+
**Manual — API surface (only if the API changes are kept):**
213+
- [ ] `GET /api/datasource-options/twitter/` → options JSON.
214+
- [ ] `GET /api/processor-options/twitter/` → 404; `GET /api/processor-options/<real_processor>/` works.
215+
- [ ] `GET /api/check-search-queue/` returns counts (incl. `-import`).
216+
- [ ] `/api/process/<processor>/` still excludes datasources.
217+
218+
---
219+
220+
## Related
221+
222+
- Memory: `project_datasource_worker_resolution.md`
223+
- Redesign: `output-shape` branch — `common/lib/outputs.py` (`Datasource`, `Table`, `Filter`, …),
224+
`backend/lib/search.py` sets `output = Datasource()`.

0 commit comments

Comments
 (0)