Private
Public Access
conductor(track): init twitter_threads_extraction_20260705 — standalone Twitter/X thread extraction tooling
Scripts + workflow for extracting Twitter/X posts and threads into Markdown with associated media. Mirrors the scripts/video_analysis/ pattern. Standalone requirement: zero imports from src/, conductor/, or scripts.video_analysis — copy-pasteable to another repo with only gallery-dl as the external dep. 5 modules: __init__.py, error_types.py (Result[T, ErrorInfo] + ThreadData/PostData typed dataclasses), fetch_thread.py (gallery-dl subprocess for URLs + html.parser fallback for local HTML), download_media.py (stdlib urllib, idempotent), render_markdown.py (YAML front-matter + per-post sections + ./media/ links). Reference project: C:\projects\forth\bootslop — the corpus feeds bootslop's scripts and reference-generation pipeline. Acceptance corpus: 8 threads (@NOTimothyLottes x6 + @VPCOMPRESSB x2) extracted to tests/artifacts/twitter_threads_corpus/. The ?s=20 quote-share suffix on the @VPCOMPRESSB URLs must be stripped by fetch_thread.py before acquisition (added to FR2 as URL normalization). 5 phases / 23 tasks. 8 verification criteria (VC1-VC8). TDD red-first on the pure-function modules (render_markdown, types, media naming).
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Track state for twitter_threads_extraction_20260705
|
||||
# Initialized by Tier 1 Orchestrator on 2026-07-05.
|
||||
# Standalone tooling track: scripts/twitter_threads/ + tests + README. No src/ changes.
|
||||
|
||||
[meta]
|
||||
track_id = "twitter_threads_extraction_20260705"
|
||||
name = "Twitter/X Thread Extraction Tooling"
|
||||
status = "active"
|
||||
current_phase = 0
|
||||
last_updated = "2026-07-05"
|
||||
|
||||
[blocked_by]
|
||||
# None. Standalone scripts + tests; no dependency on other tracks.
|
||||
|
||||
[blocks]
|
||||
# None. The tooling may be consumed by a future "Twitter thread analysis campaign" track.
|
||||
|
||||
[phases]
|
||||
phase_1 = { status = "pending", checkpointsha = "", name = "Scaffold + Error Types + Data Classes" }
|
||||
phase_2 = { status = "pending", checkpointsha = "", name = "render_markdown.py (TDD-first; pure function)" }
|
||||
phase_3 = { status = "pending", checkpointsha = "", name = "download_media.py (stdlib urllib + idempotent)" }
|
||||
phase_4 = { status = "pending", checkpointsha = "", name = "fetch_thread.py (gallery-dl subprocess + html.parser fallback)" }
|
||||
phase_5 = { status = "pending", checkpointsha = "", name = "README + End-to-End CLI + Verification" }
|
||||
|
||||
[tasks]
|
||||
t1_1 = { status = "pending", commit_sha = "", description = "Create scripts/twitter_threads/ + __init__.py (namespace docstring)" }
|
||||
t1_2 = { status = "pending", commit_sha = "", description = "Write error_types.py (ErrorInfo + make_error; standalone, no import from scripts.video_analysis)" }
|
||||
t1_3 = { status = "pending", commit_sha = "", description = "Write typed dataclasses (PostData, PostMetrics, ThreadData; frozen, slots)" }
|
||||
t1_4 = { status = "pending", commit_sha = "", description = "Write tests/test_twitter_threads_types.py (TDD red-first)" }
|
||||
t1_5 = { status = "pending", commit_sha = "", description = "Commit Phase 1" }
|
||||
t2_1 = { status = "pending", commit_sha = "", description = "Write failing tests for render_markdown.py (5+ tests: single post, thread, quote-tweet, media links, metrics, title)" }
|
||||
t2_2 = { status = "pending", commit_sha = "", description = "Implement render_markdown.py (YAML front-matter + per-post sections + media links + quote-tweet blockquote)" }
|
||||
t2_3 = { status = "pending", commit_sha = "", description = "Commit Phase 2" }
|
||||
t3_1 = { status = "pending", commit_sha = "", description = "Write failing tests for download_media.py (4+ tests: naming, video naming, idempotent, http error)" }
|
||||
t3_2 = { status = "pending", commit_sha = "", description = "Implement download_media.py (stdlib urllib; idempotent; typed naming <post_id>_<kind><index>.<ext>)" }
|
||||
t3_3 = { status = "pending", commit_sha = "", description = "Commit Phase 3" }
|
||||
t4_1 = { status = "pending", commit_sha = "", description = "Write failing tests for fetch_thread.py Strategy C (local HTML parse; 4+ tests)" }
|
||||
t4_2 = { status = "pending", commit_sha = "", description = "Implement fetch_thread_from_html (html.parser.HTMLParser subclass; stdlib only)" }
|
||||
t4_3 = { status = "pending", commit_sha = "", description = "Implement fetch_thread_from_url (gallery-dl subprocess wrapper; --dump-json + --write-metadata)" }
|
||||
t4_4 = { status = "pending", commit_sha = "", description = "Implement CLI dispatch + --help (argparse; URL or local HTML path input)" }
|
||||
t4_5 = { status = "pending", commit_sha = "", description = "Commit Phase 4" }
|
||||
t5_1 = { status = "pending", commit_sha = "", description = "Write README.md (prerequisites, usage, output layout, copy-to-another-repo, Strategy B/D alternatives)" }
|
||||
t5_2 = { status = "pending", commit_sha = "", description = "Verify standalone requirement (VC6 + VC7: --help works; zero src/conductor/video_analysis imports)" }
|
||||
t5_3 = { status = "pending", commit_sha = "", description = "End-to-end smoke test (manual; user provides a real X.com URL + cookies.txt)" }
|
||||
t5_4 = { status = "pending", commit_sha = "", description = "Run the full test suite for the new tests (batched verification)" }
|
||||
t5_5 = { status = "pending", commit_sha = "", description = "Commit Phase 5 + README" }
|
||||
t5_6 = { status = "pending", commit_sha = "", description = "Conductor — User Manual Verification (PAUSE for user confirmation)" }
|
||||
t5_7 = { status = "pending", commit_sha = "", description = "Mark track complete + write TRACK_COMPLETION_twitter_threads_extraction_20260705.md" }
|
||||
|
||||
[verification]
|
||||
vc1_package_exists = false
|
||||
vc2_readme_exists = false
|
||||
vc3_render_tests_pass = false
|
||||
vc4_fetch_tests_pass = false
|
||||
vc5_media_tests_pass = false
|
||||
vc6_standalone_help_works = false
|
||||
vc7_zero_internal_imports = false
|
||||
vc8_corpus_extraction_complete = false
|
||||
|
||||
[reference_project]
|
||||
path = "C:\\projects\\forth\\bootslop"
|
||||
purpose = "The Twitter thread extraction tooling feeds reference material into bootslop's scripts and reference-generation pipeline."
|
||||
corpus_urls = [
|
||||
"https://x.com/NOTimothyLottes/status/1757198624818168210",
|
||||
"https://x.com/NOTimothyLottes/status/1653570742762479620",
|
||||
"https://x.com/NOTimothyLottes/status/1917646466417381426",
|
||||
"https://x.com/NOTimothyLottes/status/1917645859791200562",
|
||||
"https://x.com/NOTimothyLottes/status/1917644904055910502",
|
||||
"https://x.com/NOTimothyLottes/status/1917642786804785230",
|
||||
"https://x.com/VPCOMPRESSB/status/1991383117571957052",
|
||||
"https://x.com/VPCOMPRESSB/status/1987744335333622188",
|
||||
]
|
||||
corpus_output_dir = "tests/artifacts/twitter_threads_corpus/"
|
||||
acceptance_criterion = "All 8 threads extract cleanly to thread.md + media/ pairs; the corpus is handed off to bootslop. The ?s=20 query suffix on the @VPCOMPRESSB URLs must be stripped by fetch_thread.py before acquisition."
|
||||
|
||||
[standalone_requirement]
|
||||
rationale = "Per user directive: scripts must be standalone tooling so they don't have lots of dependencies with the repo's codebase and could be utilized in others."
|
||||
enforcement = "VC7 — zero imports from src/, conductor/, or scripts.video_analysis. Verified via grep."
|
||||
copyable = "The scripts/twitter_threads/ directory is copy-pasteable to another repo with only gallery-dl as an external dep."
|
||||
|
||||
[acquisition_strategy]
|
||||
primary = "Strategy A: gallery-dl subprocess wrapper (URL input; --dump-json + --write-metadata)"
|
||||
fallback = "Strategy C: local HTML parse via html.parser.HTMLParser (user saves HTML via browser; script parses offline)"
|
||||
documented_alternatives = "Strategy B: snscrape (legacy; documented in README); Strategy D: tweepy/twitter-api-python (paid API; documented in README)"
|
||||
Reference in New Issue
Block a user