Roadmap¶
Each task is one commit on main. Task lines follow P<phase>.<n> <title> — deps: […] — AC: <observable criteria>. A task may start only when its deps are merged. Every task links to the phase spec in docs/specs/ and, where noted, to a spike in OPEN_QUESTIONS.md.
Branch naming (optional, worktree-local): feat/P3.2-fake-provider, infra/P0.5-github, spike/S1-agy-image-tool.
Resolved ordering notes¶
Spec decisions that override the task lines below where they differ:
runs showis built minimally inside P6.1; P7.4 extends it.- Built-in template loading (
sync_builtins) belongs to P4.4; P4.3 tests load the built-ins straight from their files. - P7.4 follows P7.2, and P8.4 follows P7.4.
- Spike S10 fallback: P5.4 ships a path/table fallback plus a real two-column
Table.grid, and does not wait on the spike.
Phase 0 — Infrastructure (no runtime code)¶
Spec: docs/specs/phase-0-infra.md
- P0.1 Repo bootstrap — deps: none — AC:
uv init --package --python 3.14layout committed;pyproject.tomlhasrequires-python = ">=3.14", namethumbforge,[project.scripts] thumbforge = "thumbforge.cli.app:main";.editorconfig,.gitignore,LICENSE(MIT);uv sync --lockedpasses;uv run thumbforge --versionprints0.0.0. - P0.2 Quality tooling — deps: P0.1 — AC: ruff, pyright, pytest, pytest-asyncio, coverage, import-linter as dev deps;
.pre-commit-config.yamlwith ruff, ruff-format, pyright, mirrors-prettier (markdown/yaml/json),uv lock --check, end-of-file-fixer, conventional-pre-commit;.prettierrc+.prettierignore;pre-commit run --all-filesgreen on the empty package. Blocked on spike S13 for the prettierrev. - P0.3 Agent instruction files — deps: P0.1 — AC:
AGENTS.mdper the draft;CLAUDE.mdandGEMINI.mdeach contain only the pointer line. - P0.4 Docs skeleton + zensical site — deps: P0.1 — AC:
docs/developers/architecture.md,docs/developers/conventions.md,docs/developers/testing.md,docs/developers/glossary.md,docs/adr/0000-template.md, ADRs 0001–0016,docs/specs/README.md;zensical.tomlwith nav covering ARCHITECTURE/ROADMAP/CONVENTIONS/TESTING/GLOSSARY/adr/specs;uv run zensical buildsucceeds with zero warnings. Blocked on spike S14 for mermaid rendering. - P0.5 GitHub setup — deps: P0.2 — AC:
.github/PULL_REQUEST_TEMPLATE.md(spec link, checklist, test evidence); issue templatesfeature.yml,bug.yml,spike.yml;.github/labels.yml+ sync workflow;CODEOWNERS;ci.ymlrunninguv sync --locked,ruff check,ruff format --check,pyright,pytest --cov --cov-fail-under=80,uv run zensical buildon3.14×ubuntu-latest, windows-latest;docs.ymldeploying the zensical site to GitHub Pages on push tomain;release.yml(tagv*→uv build→ PyPI trusted publishing,git-cliffchangelog);docs/maintainers/branching-and-releases.mddocuments branch protection (PR required, 1 review, CI green, linear history, no force-push). Needs decision D5. - P0.6 graphify integration — deps: P0.3, spike S9 — AC:
graphify-out/committed (minuscost.json),.agents/skills/present,AGENTS.md"Before you start" points atgraphify-out/GRAPH_REPORT.md; regeneration method (hook / CI / manual) recorded inAGENTS.md. - P0.7 Phase 0 + Phase 1 specs — deps: P0.4 — AC:
docs/specs/phase-0-infra.mdanddocs/specs/phase-1-skeleton.mdcomplete with acceptance criteria; later phase specs present as outlines.
Phase 1 — Skeleton (config, DB, logging, errors)¶
Spec: docs/specs/phase-1-skeleton.md
- P1.1 Settings — deps: P0.7 — AC:
settings.py(pydantic-settings, TOML atuser_config_dir("thumbforge")/config.toml, env prefixTHUMBFORGE_, nested delimiter__);thumbforge config init|show|path|set;thumbforge --json config showprints JSON; secret-looking TOML keys rejected withSettingsError. - P1.2 Logging — deps: P1.1 — AC:
logging.pywithconfigure_logging/get_logger(structlog, processor chain and stdlib bridge per PLAN.md §7.3);thumbforge -vv db statusprints console-rendered lines;thumbforge --json db status 2>err.jsonlyields one JSON object per line; rotating JSON log file underuser_state_dir. - P1.3 DB engine + Alembic — deps: P1.1 — AC:
storage/db.py(WAL,foreign_keys=ON),storage/models.pywith all tables from PLAN.md §3, initial Alembic migration,thumbforge db init|upgrade|status|path|vacuum;db initcreates the DB at Alembic head. - P1.4 Asset store — deps: P1.3 — AC:
AssetStore.put/path_for/verify; identical bytes dedupe; tmp→fsync→rename write path; unit tests withtmp_path. - P1.5 Errors + root CLI — deps: P1.2 — AC:
core/errors.pyhierarchy andcli/_errors.pymapping per PLAN.md §5.1/§7.1; root Typer app with global flags; unknown command exits2; a raisedNotFoundErrorexits3withhinton stderr. - P1.6 Import-linter contracts — deps: P1.5 — AC:
[tool.importlinter]contracts encode PLAN.md §2.2;uv run lint-importsgreen and wired into CI.
Phase 2 — YouTube fetch¶
Spec: docs/specs/phase-2-youtube-fetch.md
- P2.1 MetadataSource protocol + models — deps: P1.5 — AC:
MetadataSourceProtocol (added assources/base.py, moved tocore/sources.pyin P2.3 socore.servicescan consume it);core/models.pyChannelMeta,PlaylistMeta,VideoMeta; URL classifier (video | playlist | channel) with unit tests. - P2.2 YtDlpSource with recorded fixtures — deps: P2.1, spike S11 — AC:
sources/ytdlp.pyusingextract_flat="in_playlist"; fixture recorder markedintegrationwritestests/fixtures/ytdlp/*.json; unit tests replay fixtures with no network. - P2.3 Repositories + fetch/video/playlist commands — deps: P1.3, P2.2 — AC: upsert semantics for channel/playlist/video/playlist_item;
thumbforge fetch <url>,video list|show,playlist list|show [--videos];--refreshre-fetches;--jsonoutput. - P2.4 Playlist renumber — deps: P2.3 — AC:
thumbforge playlist renumber <playlist> --start N [--skip-ids …]rewritespart_numbersequentially, skipped videos get NULL;playlist show --videosreflects it.
Phase 3 — Provider layer (Fake + Antigravity)¶
Spec: docs/specs/phase-3-providers.md
- P3.1 Provider protocol + registry — deps: P1.5 — AC:
core/providers.pyImageProviderProtocol and boundary models (placed incorebecausecore.servicesconsumes them);providers/registry.pymergingBUILTINwith thethumbforge.providersentry-point group. Observable:uv run pytest -q tests/unit/test_provider_registry.pypasses; and real discovery is proven out-of-tree by building a throwaway distribution whosepyproject.tomldeclares[project.entry-points."thumbforge.providers"] smoke = "tfplug:SmokeProvider", thenuv pip install <dist> && uv run python -c "from thumbforge.providers import registry; print(registry.keys())"prints['smoke'],registry.get('smoke', {...})returns that class with its config, aBUILTINentry of the same key raisesProviderRegistryError(exit 4), anduv pip uninstall tfplugreturnskeys()to[]. - P3.2 FakeProvider + contract tests — deps: P3.1 — AC:
providers/fake.pyregistered inBUILTIN;uv run pytest -q tests/contract/ tests/unit/test_fake_provider.pypasses. Deterministic to the byte for an identical request; honours the requested size exactly (a property of the fake, not of the interface — the contract suite only asserts the ratio is within 5%);[[FAIL_TRANSIENT]]/[[FAIL_PERMANENT]]raise the retryable and non-retryable errors and leave no file behind;params.delay_msis awaited so four 120 ms calls gathered finish in well under 480 ms. The contract suite is parametrised overregistry.keys(), so P3.4's provider is held to it automatically under-m integration. - P3.3 Antigravity spikes S1–S8 — deps: none — AC:
docs/spikes/antigravity.mdrecords commands, raw outputs, and conclusions for each spike; ADR 0013 status updated. - P3.4 AntigravityProvider — deps: P3.1, P3.3 — AC:
providers/antigravity.pyregistered inBUILTIN, drivingagyheadlessly per ADR 0013 as revised by the spikes. Observable:uv run pytest -q tests/unit/test_antigravity_provider.pypasses with the subprocess injected (every error branch, including the print timeout that reports SUCCESS, mapped to a retryable error before the missing-output case); anduv run pytest -m integration tests/contract/drives real generations, giving5 passed, 3 skippedwithagyon PATH and skipping cleanly without it. - P3.5 Provider commands — deps: P3.1, P3.2, P3.4 — AC:
thumbforge provider list|check|models|set-key;set-keystores in keyring only and is the single writer of a secret. Observable:provider check antigravityprints binary, version, inherited plugins and anauthcheck resolved fromagy models;provider check nopeexits 3;provider models fakeexits 0 reporting no model selection while an unhealthy provider exits 4 rather than printing an empty list;uv run pytest -q tests/unit/test_cli_provider.pycovers all four commands with no network and no real keyring.
Phase 4 — Templates¶
Spec: docs/specs/phase-4-templates.md
- P4.1 Layout spec schema — deps: P1.5 — AC: Pydantic
LayoutSpecincore/layout.py(canvas, title box, part badge, font, colours, safe margins; incorebecauseimagingandcore.servicesconsume it) and TOML loading intemplates/schema.py;template validate PATHreports schema errors with exit2. - P4.2 Prompt rendering — deps: P4.1 — AC: Jinja2 environment with
StrictUndefined,autoescape=False; context = video/playlist/part/vars; missing variable →TemplateError;template render NAME --video <id> [--part N]prints the prompt. - P4.3 Builtin templates — deps: P4.2 — AC:
bold-title,minimal,series-partsshipped asNAME.toml+NAME.j2package data undertemplates/builtin/and included in the wheel, located bytemplates/builtins.py; each layout loads withload_layout, overlays the fixture title without truncation, and its prompt renders to a committed golden intests/templates/golden/. - P4.4 Template commands + versioning — deps: P4.3, P1.3 — AC: builtins loaded into the database as
is_builtin=1version 1 ondb init;template list|show|new|import; importing a changed spec creates version+1;spec_hashstable for identical content.
Phase 5 — Imaging¶
Spec: docs/specs/phase-5-imaging.md
- P5.1 Fit/crop — deps: P1.5 — AC:
imaging/fit.pyresizes/crops any input to the configured 16:9 target (default 1920×1080) with centre crop; unit tests on synthetic images. - P5.2 Text overlay + golden tests — deps: P4.1, P5.1 — AC:
imaging/overlay.pyrenders title and "Part N" badge from the layout spec with the bundled OFL font;golden-marked snapshot tests compare againsttests/golden/overlay/*.pngwith exact pixels. - P5.3 Compliance check + final render — deps: P5.2 — AC:
imaging/compliance.pycheckenforces 16:9 ±1 px, width ≥ 1280, JPEG/PNG, ≤ 2 MB default (--max-bytesup to 50 MB), sRGB and returns aComplianceReport(core/models.py);imaging/finalize.pyrender_finalruns fit → overlay → encode, lowering JPEG quality (floor 60) until withinmax_bytes, and returns the bytes with the report. Storing the report inasset.compliance_report_jsonand raisingComplianceError(exit5) belong to P6.1. - P5.4 Rich preview — deps: P5.1, spike S10 — AC:
cli/_render.pygrid preview viarich-pixels; degrades to a table when the terminal cannot render.
Phase 6 — Hero thumbnail + iterations¶
Spec: docs/specs/phase-6-hero.md
- P6.1 HeroService +
thumb generate— deps: P2.3, P3.2, P4.4, P5.3 — AC: creates hero run + N iterations, runs provider under a semaphore, finalizes through the injectedrender_final, stores raw and final assets withcompliantandcompliance_report_json(non-compliant finals stored, iterationfailed; exit5when every failure is a compliance failure); exit6when some iterations fail; works end-to-end withfake. - P6.2
thumb pick|show|export— deps: P6.1 — AC: exactly one picked iteration per run;showgrid;export --to PATH [--raw]. - P6.3
thumb iterate— deps: P6.2 — AC: child run withparent_run_id, reference = picked (or given) iteration's raw asset,--prompt-appendand--varapplied;runs showdisplays lineage.
Phase 7 — Playlist batch¶
Spec: docs/specs/phase-7-batch.md
- P7.1 BatchService — deps: P6.2 — AC: idempotency key per PLAN.md §6; existing
completedkeys skipped;asyncio.Semaphore(min(--concurrency, max_concurrency));--onlyand--max-imageshonoured;--dry-runprints the plan without provider calls. - P7.2 Resume / cancel / interrupt — deps: P7.1 — AC: SIGINT marks run
paused, in-flight iterationsfailedwitherror_text="interrupted", exit130;runs resumeregenerates only non-completed items (test: 20-item playlist interrupted after 7 resumes with exactly 13 provider calls); stalerunningiterations older thanstale_after_sretried;runs cancelrefuses completed runs. - P7.3
batchcommand + progress — deps: P7.2 — AC: Rich progress with per-item title; summary table; exit6on partial,4when all fail;--reference final|raw. - P7.4
runscommands — deps: P7.1 — AC:runs list|show|resume|cancel|delete [--assets]; delete refuses runs referenced by a batch (exit2).
Phase 8 — Polish¶
Spec: docs/specs/phase-8-polish.md
- P8.1 YouTube Data API source — deps: P2.3, decision D7 — AC: optional extra
thumbforge[api];fetch --source apibehind the sameMetadataSource; key via env/keyring; missing extra or key →SourceErrorwith install/set-key hint. - P8.2 Install docs — deps: P6.1 — AC:
docs/user-guide/getting-started.mdcoversuv tool install thumbforgeand first run;uv tool install .putsthumbforgeon PATH. - P8.3 Shell completion — deps: P8.2 — AC:
thumbforge --install-completiondocumented and smoke-tested on PowerShell, bash, zsh. - P8.4 Cost report — deps: P7.3 — AC:
thumbforge runs cost <run>aggregatesiteration.cost_json(tokens, credits,no_cost_datacount);runs showgains a cost column;--jsonsupported. - P8.5 README + first release — deps: P8.3, P8.4 — AC: README quick start reproduces the three PLAN.md examples using
fake;git-cliffchangelog; tagv0.1.0runsrelease.ymlgreen.