|
| 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