conductor(track): complete twitter_threads_extraction_20260705 + TRACK_COMPLETION report [state=completed]
All 5 phases done, 34 tests passing, VC1-VC7 pass. Task 5.3 (live 8-URL smoke) + 5.6 (user manual verification) deferred to user (network/cookies + merge decision).
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# Track Completion Report: Twitter/X Thread Extraction Tooling
|
||||
|
||||
**Track:** `twitter_threads_extraction_20260705`
|
||||
**Branch:** `tier2/twitter_threads_extraction_20260705` (based on `origin/master` @ `0908f8fa`)
|
||||
**Mode:** Tier 2 autonomous sandbox
|
||||
**Status:** Implementation complete; one network-gated acceptance step (live smoke) deferred to the user.
|
||||
|
||||
## What was built
|
||||
|
||||
A standalone `scripts/twitter_threads/` package (the Twitter/X analog of `scripts/video_analysis/`) that acquires X.com posts/threads + media and emits a self-contained Markdown document per thread. Zero imports from `src/`, `conductor/`, or `scripts.video_analysis` — copy-pasteable to another repo with only `gallery-dl` as an external dep.
|
||||
|
||||
### Files created
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `scripts/twitter_threads/__init__.py` | Namespace docstring |
|
||||
| `scripts/twitter_threads/error_types.py` | `ErrorInfo` + `make_error`; `Result[T]` (frozen+slots, `ok`/`err` classmethods, `is_ok`); `PostMetrics`/`PostData`/`ThreadData` dataclasses; `thread_from_dict` (JSON boundary) |
|
||||
| `scripts/twitter_threads/render_markdown.py` | `render_markdown(thread, media_names, output) -> Result[Path]` + CLI |
|
||||
| `scripts/twitter_threads/download_media.py` | `media_names_for_post` + `download_media(thread, output_dir) -> Result[list[Path]]` (stdlib urllib, idempotent) + CLI |
|
||||
| `scripts/twitter_threads/fetch_thread.py` | `fetch_thread_from_html` (stdlib `html.parser`), `fetch_thread_from_url` (gallery-dl subprocess), `_normalize_url` (FR2 query-strip), `thread_to_dict`, CLI |
|
||||
| `scripts/twitter_threads/README.md` | Standalone-usage doc (FR7) |
|
||||
|
||||
### Tests created (34 tests total, all passing)
|
||||
|
||||
| File | Tests | Covers |
|
||||
|---|---|---|
|
||||
| `tests/test_twitter_threads_types.py` | 9 | dataclass fields / frozen / slots |
|
||||
| `tests/test_twitter_threads_render.py` | 7 | Result contract + Markdown schema |
|
||||
| `tests/test_twitter_threads_media.py` | 6 | per-kind naming, idempotency, HttpError (authorized HTTP-boundary mocks) |
|
||||
| `tests/test_twitter_threads_fetch.py` | 7 | HTML-parse contract + URL normalization + ParseError |
|
||||
| `tests/test_twitter_threads_pipeline.py` | 4 | JSON round-trip + fetch→render pipeline + download/render CLIs |
|
||||
|
||||
## Acceptance criteria (from spec §Verification)
|
||||
|
||||
| Criterion | Result |
|
||||
|---|---|
|
||||
| VC1 — package has 5 code files | PASS (`__init__`, `error_types`, `fetch_thread`, `download_media`, `render_markdown`) |
|
||||
| VC2 — `README.md` exists with usage/layout/copy instructions | PASS |
|
||||
| VC3 — `render_markdown` tests pass | PASS (7) |
|
||||
| VC4 — `fetch_thread` local-HTML tests pass | PASS (7) |
|
||||
| VC5 — `download_media` naming/idempotency tests pass | PASS (6) |
|
||||
| VC6 — `python scripts/twitter_threads/fetch_thread.py --help` works standalone | PASS (exit 0; dual-import fallback) |
|
||||
| VC7 — zero imports from `src/`/`conductor/`/`scripts.video_analysis` | PASS (grep empty across the package) |
|
||||
|
||||
## Key design decisions
|
||||
|
||||
1. **`Result[T]` instead of the `video_analysis` `_Ok|_Err` sum type.** The spec said "mirror video_analysis," but `video_analysis` uses a per-module `_Ok|_Err` union, which the higher-precedence `conductor/code_styleguides/error_handling.md` explicitly bans (AND-over-OR, not sum types). One standalone `Result[T]` (frozen+slots, `ok`/`err` classmethods, `is_ok` property, `data`/`error` fields) lives in `error_types.py` and is used package-wide.
|
||||
|
||||
2. **`render_markdown` decoupled from naming.** It takes a precomputed `media_names: dict[str, list[str]]` mapping rather than importing `download_media` — keeps the pure renderer testable in isolation. The CLIs and the pipeline test build the mapping from `media_names_for_post`.
|
||||
|
||||
3. **Dual-import (`try absolute / except relative`) in every runnable module.** Satisfies G5 (runnable both via `python -m ...` and as a bare copied script) and VC6. The `try/except ImportError` form is the sanctioned exception to the local-import ban.
|
||||
|
||||
4. **CLI entry points added to `download_media` and `render_markdown`.** The plan's Task 5.3 pipeline invokes all three modules via `-m`; `thread_data.json` is the interchange (round-tripped by `thread_to_dict`/`thread_from_dict`). An automated pipeline test exercises fetch→render + both CLIs without network.
|
||||
|
||||
5. **`gallery-dl` URL path is best-effort and network-boundary.** Per the plan, the URL/`gallery-dl` acquisition is smoke-tested manually (Task 5.3), not unit-tested; the automated suite covers the local-HTML path (Strategy C) end-to-end.
|
||||
|
||||
## Process notes / deviations (flagged for transparency)
|
||||
|
||||
- **Per-task test verification used direct `pytest` on the 5 new test files**, not `run_tests_batched.py`. Reason: the new tests are pure unit tests with no shared-subprocess state (the `batch_verification_not_isolation` directive scopes the "batch is the only truth" rule to `live_gui`/shared-state tests), and `run_tests_batched.py` in this repo has no per-file/per-group filter — its smallest unit is a whole tier (`tier-1-unit-core` = 259 files, ~13 min), which the Tier 2 conventions say is the user's post-merge job, not per-task verification. The full new-test set (34) passes together; log at `tests/artifacts/tier2_state/twitter_threads_extraction_20260705/test_run_phase5_final.log`.
|
||||
- **`get_ui_performance` sandbox gate was unavailable (headless CLI mode).** Sandbox presence was instead confirmed authoritatively by the OS-level path restriction to `C:\projects\manual_slop_tier2` and `origin` pointing at a separate clone (`C:\projects\manual_slop`).
|
||||
|
||||
## Deferred to the user
|
||||
|
||||
- **Task 5.3 — live end-to-end smoke against the 8-URL corpus** (`@NOTimothyLottes` ×6 + `@VPCOMPRESSB` ×2). This requires network access + a `cookies.txt` and is out of reach of the sandbox. The pipeline mechanics are verified by `test_twitter_threads_pipeline.py` (fetch→download(mock)→render → `thread.md`); only the live `gallery-dl` acquisition needs the user's cookies. To run it: install `gallery-dl`, export `cookies.txt`, then run the 3-stage `-m` pipeline (see `scripts/twitter_threads/README.md`) for each URL into `tests/artifacts/twitter_threads_corpus/`. The `?s=20` suffix on the `@VPCOMPRESSB` URLs is stripped automatically by `_normalize_url`.
|
||||
|
||||
## Commits (10; all with git notes)
|
||||
|
||||
| SHA | Type | Subject |
|
||||
|---|---|---|
|
||||
| `161d8da8` | feat | scaffold package + error types + typed dataclasses (first commit — TIER-2 READ acknowledgment) |
|
||||
| `0440be02` | conductor | mark Phase 1 complete |
|
||||
| `ef98f6d1` | feat | Result[T] + render_markdown.py |
|
||||
| `36de54a9` | conductor | mark Phase 2 complete |
|
||||
| `f503eb5d` | feat | download_media.py — stdlib urllib + idempotent + per-kind naming |
|
||||
| `ea33a49f` | conductor | mark Phase 3 complete |
|
||||
| `06ff9299` | feat | fetch_thread.py — html.parser + gallery-dl + URL query-strip + CLI |
|
||||
| `41656d6b` | conductor | mark Phase 4 complete |
|
||||
| `ed867317` | feat | thread_from_dict + download_media/render_markdown CLIs (end-to-end -m pipeline) |
|
||||
| `fe207c1e` | docs | README |
|
||||
|
||||
(Plus this report + the plan/state finalization commit.)
|
||||
|
||||
## Merge workflow (user-side)
|
||||
|
||||
1. In the **main repo** (not the sandbox clone): `pwsh -File scripts/tier2/fetch_tier2_branch.ps1 -TrackName twitter_threads_extraction_20260705` to pull the branch as `review/...`.
|
||||
2. Review the diff; optionally run the full batched suite: `uv run python scripts/run_tests_batched.py`.
|
||||
3. Run the live 8-URL smoke (Task 5.3) with your `cookies.txt`.
|
||||
4. On approval, `git merge --no-ff review/twitter_threads_extraction_20260705` and push (the sandbox blocks Tier 2 from pushing).
|
||||
Reference in New Issue
Block a user