Phase 0 — Infrastructure¶
Status: Implemented (P0.1–P0.7)
ROADMAP tasks: P0.1, P0.2, P0.3, P0.4, P0.5, P0.6, P0.7
ADRs: docs/adr/0001-python-and-uv.md, docs/adr/0009-quality-tooling.md, docs/adr/0016-zensical-docs-site.md
Scope¶
Phase 0 produces a repository that a coding agent can work in safely before a single line of feature code exists: package skeleton, quality gates, agent instructions, docs site, GitHub automation, and the graphify knowledge graph. The only runtime code allowed is the __version__ shim needed for thumbforge --version.
Deliverables, grouped by task:
- P0.1 bootstrap —
pyproject.toml,uv.lock,src/thumbforge/__init__.py,src/thumbforge/__main__.py,.editorconfig,.gitignore,LICENSE(MIT, decision D5),README.md. - P0.2 quality tooling —
.pre-commit-config.yaml,.prettierrc,.prettierignore,[tool.*]tables,tests/conftest.py,tests/test_version.py. - P0.3 agent instructions —
AGENTS.md,CLAUDE.md,GEMINI.md. - P0.4 docs —
docs/index.md,docs/developers/architecture.md,docs/developers/conventions.md,docs/developers/testing.md,docs/developers/glossary.md,docs/ROADMAP.md,docs/adr/0000-template.md, ADRs 0001–0016,zensical.toml. - P0.5 GitHub —
.github/PULL_REQUEST_TEMPLATE.md,.github/ISSUE_TEMPLATE/{feature,bug,spike}.yml,.github/CODEOWNERS,.github/labels.yml,.github/workflows/{ci,docs,release}.yml,docs/maintainers/branching-and-releases.md. - P0.6 graphify —
graphify-out/committed,.agents/skills/committed, hooks per spike S9. - P0.7 specs —
docs/specs/README.md,phase-0-infra.md,phase-1-skeleton.md(this tree).
Non-goals¶
- No settings, logging, DB, CLI sub-commands — Phase 1 (
phase-1-skeleton.md). - No provider or yt-dlp code; no network access anywhere in Phase 0.
- No PyPI publish on tag until decision D5 confirms publishing;
release.ymlships with the publish job present but the workflow is only triggered by tags, which nobody pushes before D5. - Python 3.15 in the CI matrix — spike S12.
Interfaces¶
File inventory¶
| Path | Purpose | Task |
|---|---|---|
pyproject.toml |
Project metadata, dependencies, [project.scripts] thumbforge = "thumbforge.cli.app:main", all [tool.*] tables below |
P0.1/P0.2 |
uv.lock |
Locked resolution; CI runs uv sync --locked |
P0.1 |
src/thumbforge/__init__.py |
__version__ = importlib.metadata.version("thumbforge") |
P0.1 |
src/thumbforge/__main__.py |
from thumbforge.cli.app import main; main() — in Phase 0 cli/app.py contains only the root Typer app with --version |
P0.1 |
.editorconfig |
indent_style = space, indent_size = 4 (py) / 2 (yaml, toml, json, md), end_of_line = lf, insert_final_newline = true, charset = utf-8 |
P0.1 |
.gitignore |
Python/uv defaults plus site/, graphify-out/cost.json, .coverage, htmlcov/, *.log, .venv/ |
P0.1 |
LICENSE |
MIT, copyright holder from D5 | P0.1 |
README.md |
One-screen pitch, install (uv tool install thumbforge), links to docs site |
P0.1 |
.pre-commit-config.yaml |
Hook list below | P0.2 |
.prettierrc |
Prettier options below | P0.2 |
.prettierignore |
Prettier exclusions below | P0.2 |
tests/conftest.py |
Empty fixtures module; establishes tests/ as the pytest root |
P0.2 |
tests/test_version.py |
Asserts thumbforge --version via typer.testing.CliRunner prints thumbforge 0.0.0 |
P0.2 |
AGENTS.md |
Canonical instructions for coding agents | P0.3 |
CLAUDE.md, GEMINI.md |
Single line: Read AGENTS.md; it is the canonical instruction file. |
P0.3 |
docs/index.md |
Docs site landing page; links to every top-level doc | P0.4 |
docs/developers/architecture.md |
PLAN.md §2 (layout, dependency rule, data-flow diagram) extracted |
P0.4 |
docs/developers/conventions.md |
Code conventions (mirrors AGENTS.md Conventions, longer form) |
P0.4 |
docs/developers/testing.md |
Test layers, markers, fixture recording procedure | P0.4 |
docs/developers/glossary.md |
hero, iteration, run, asset, template, provider profile, idempotency key, part number | P0.4 |
docs/ROADMAP.md |
Phase/task list | P0.4 |
docs/adr/0000-template.md, 0001–0016 |
Decision records | P0.4 |
docs/specs/ |
This directory | P0.7 |
zensical.toml |
Docs site config below | P0.4 |
.github/PULL_REQUEST_TEMPLATE.md |
PR checklist below | P0.5 |
.github/ISSUE_TEMPLATE/feature.yml, bug.yml, spike.yml |
Issue forms | P0.5 |
.github/CODEOWNERS |
* @<owner> (D5) |
P0.5 |
.github/labels.yml |
Label list below; applied by a manual gh label script documented in docs/maintainers/branching-and-releases.md |
P0.5 |
.github/workflows/ci.yml |
Lint/type/test/docs on PR and push | P0.5 |
.github/workflows/docs.yml |
Build + deploy docs to GitHub Pages | P0.5 |
.github/workflows/release.yml |
Tag → build → PyPI (proposal) | P0.5 |
docs/maintainers/branching-and-releases.md |
Branch protection, labels script, release procedure | P0.5 |
graphify-out/graph.html, GRAPH_REPORT.md, graph.json |
Knowledge graph (committed); cost.json ignored |
P0.6 |
.agents/skills/ |
Written by graphify install --project --platform agents |
P0.6 |
pyproject.toml¶
[project]
name = "thumbforge"
version = "0.0.0"
description = "Generate consistent, spec-compliant YouTube thumbnails from a hero image and a playlist."
readme = "README.md"
requires-python = ">=3.14"
license = "MIT"
dependencies = [] # Phase 1 adds typer, rich, pydantic-settings, platformdirs, sqlalchemy, alembic, structlog, tenacity, keyring
[project.scripts]
thumbforge = "thumbforge.cli.app:main"
[project.entry-points."thumbforge.providers"]
# populated in Phase 3: fake = "thumbforge.providers.fake:FakeProvider"
[dependency-groups]
dev = [
"ruff",
"pyright",
"pytest",
"pytest-asyncio",
"pytest-cov",
"pre-commit",
"import-linter",
"zensical",
]
[build-system]
requires = ["uv_build"]
build-backend = "uv_build"
[tool.ruff]
line-length = 100
target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM", "TCH", "RUF"]
[tool.pyright]
strict = ["src"]
pythonVersion = "3.14"
venvPath = "."
venv = ".venv"
[tool.pytest.ini_options]
asyncio_mode = "auto"
markers = ["integration", "golden"]
testpaths = ["tests"]
addopts = "-m 'not integration'"
[tool.coverage.run]
source = ["thumbforge"]
branch = true
[tool.coverage.report]
fail_under = 80
show_missing = true
[tool.importlinter]
root_package = "thumbforge"
# contracts are added in Phase 1 (phase-1-skeleton.md)
Notes: ruff handles both lint and format for Python; pyright is the type checker (decision D3, ADR 0009). Version pinning for zensical and graphifyy is exact in uv.lock / uv tool install because both are pre-1.0 (PLAN.md §9).
.prettierrc¶
{ "proseWrap": "preserve", "printWidth": 100 }
.prettierignore¶
graphify-out/
site/
uv.lock
CHANGELOG.md
.pre-commit-config.yaml hook list¶
| Order | Repo | Hook | Notes |
|---|---|---|---|
| 1 | astral-sh/ruff-pre-commit |
ruff |
args: [--fix] |
| 2 | astral-sh/ruff-pre-commit |
ruff-format |
|
| 3 | RobertCraigie/pyright-python |
pyright |
uses project .venv |
| 4 | pre-commit/mirrors-prettier |
prettier |
types_or: [markdown, yaml, json]; rev pinned per spike S13 |
| 5 | local | uv-lock-check |
entry: uv lock --check, language: system, pass_filenames: false, files: pyproject.toml |
| 6 | pre-commit/pre-commit-hooks |
end-of-file-fixer |
|
| 7 | pre-commit/pre-commit-hooks |
trailing-whitespace |
args: [--markdown-linebreak-ext=md] |
| 8 | compilerla/conventional-pre-commit |
conventional-pre-commit |
stages: [commit-msg] |
No repo-level package.json; prettier is bootstrapped by pre-commit's Node environment. If spike S13 finds the mirror unusable on Windows, hook 4 is replaced by uv run --with mdformat mdformat on Markdown only, and YAML/JSON formatting is dropped.
zensical.toml¶
[project]
site_name = "Thumbforge"
site_url = "https://<owner>.github.io/thumbforge/" # D5
docs_dir = "docs"
site_dir = "site"
strict = true # warnings, including broken links and anchors, fail the build
nav = [
{ "Home" = "index.md" },
{ "User guide" = [ "user-guide/index.md", … ] },
{ "Developers" = [ "developers/index.md", "developers/architecture.md", …, "specs/README.md", … ] },
{ "Maintainers" = [ "maintainers/index.md", "maintainers/branching-and-releases.md", "ROADMAP.md", …, "adr/index.md", … ] },
]
The nav is grouped by audience (users, developers, maintainers); the complete list lives in zensical.toml, and every page under docs/ must appear in it. docs/specs/, docs/spikes/, docs/adr/ and docs/ROADMAP.md keep their paths whatever their nav group, because source docstrings and tooling reference them. A moved page gets an entry in [project.plugins.redirects.redirect_maps]. The theme uses system fonts (font = false) and brand colours from docs/stylesheets/extra.css.
Mermaid rendering of ADR/spec diagrams is spike S14; if a pymdownx.superfences custom fence is required it is added under [project.markdown_extensions] in the same PR that closes S14.
.github/workflows/ci.yml outline¶
name: ci
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python: ["3.14"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with: { python-version: "${{ matrix.python }}", enable-cache: true }
- run: uv sync --locked
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run pyright
- run: uv run pytest --cov --cov-fail-under=80
- run: uv run zensical build
graph-drift:
runs-on: ubuntu-latest
continue-on-error: true # warning-only until spike S9 decides
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv tool install graphifyy
- run: graphify update . && git diff --exit-code graphify-out/graph.json
pytest runs with addopts = "-m 'not integration'", so no network or provider tests run in CI by default. Python 3.15 is added to matrix.python when spike S12 closes.
.github/workflows/docs.yml outline¶
name: docs
on:
push:
branches: [main]
permissions: { contents: read, pages: write, id-token: write }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --locked --only-group dev
- run: uv run zensical build
- uses: actions/upload-pages-artifact@v3
with: { path: site }
deploy:
needs: build
runs-on: ubuntu-latest
environment: github-pages
steps:
- id: deployment
uses: actions/deploy-pages@v4
.github/workflows/release.yml proposal¶
name: release
on:
push:
tags: ["v*.*.*"]
permissions: { contents: write, id-token: write }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: astral-sh/setup-uv@v5
- run: uv build
- uses: orhun/git-cliff-action@v4
with: { args: --latest --strip header }
id: cliff
- uses: softprops/action-gh-release@v2
with: { body: "${{ steps.cliff.outputs.content }}", files: dist/* }
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist }
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist }
- uses: pypa/gh-action-pypi-publish@release/v1 # trusted publishing; no token
Version source is [project] version in pyproject.toml; the tag must equal v${version} (checked by a step before uv build). git-cliff reads Conventional Commits (enforced by the conventional-pre-commit hook) to generate CHANGELOG.md; CHANGELOG.md is prettier-ignored because git-cliff owns its formatting.
.github/labels.yml¶
| Label | Colour | Use |
|---|---|---|
type:feature |
#0e8a16 |
New behaviour |
type:bug |
#d73a4a |
Defect |
type:spike |
#fbca04 |
Time-boxed investigation (OPEN_QUESTIONS.md S-items) |
type:docs |
#0075ca |
Docs / ADR / spec only |
type:infra |
#5319e7 |
CI, tooling, repo config |
phase:0 … phase:8 |
#c5def5 |
ROADMAP phase |
provider:antigravity |
#bfd4f2 |
Antigravity adapter |
provider:fake |
#bfd4f2 |
FakeProvider |
needs:decision |
#e99695 |
Blocked on a D-item in OPEN_QUESTIONS.md |
.github/CODEOWNERS¶
* @<owner>
<owner> is resolved by decision D5.
Issue templates¶
feature.yml: fields ROADMAP task id, Spec link, Acceptance criteria, ADRs affected; labeltype:feature.bug.yml: fields Command run, Expected, Actual,thumbforge --version, OS, log excerpt from<state_dir>/logs/thumbforge.log; labeltype:bug.spike.yml: fields Spike id (S1–S14), Question, Verification command, What the answer changes, Time box; labeltype:spike.
.github/PULL_REQUEST_TEMPLATE.md¶
## What
<one paragraph>
## Links
- ROADMAP task: P?.?
- Spec: docs/specs/…#…
- Issue: #…
## Checklist
- [ ] One concern only
- [ ] Spec updated if behaviour differs from it
- [ ] Tests at the right layer (unit / contract / integration / golden)
- [ ] `pre-commit run --all-files` green locally
- [ ] Docs and AGENTS.md updated if structure changed
- [ ] `graphify update .` run and graphify-out/ committed (minus cost.json)
Branch protection (main)¶
- Pull request required; direct pushes disabled.
- 1 approving review (CODEOWNERS satisfy this when D5 sets a real owner).
- Required status checks:
ci / test (ubuntu-latest, 3.14),ci / test (windows-latest, 3.14).graph-driftis not required. - Linear history required (squash or rebase merges only).
- Force-push and deletion of
maindisabled. - Conversation resolution required before merge.
PR discipline¶
- One concern per PR; a PR that touches two ROADMAP tasks is split.
- Every PR links its spec and issue in the template; a PR with no spec link is returned without review.
- CI must be green on both OS legs before review starts.
- Docs, ADRs and
AGENTS.mdare updated in the same PR as the change they describe. - Commit messages follow Conventional Commits (
feat(providers): …,fix(cli): …,docs: …,chore(infra): …), enforced by the commit-msg hook.
graphify integration (P0.6)¶
Steps, executed in this order and recorded in docs/maintainers/branching-and-releases.md:
uv tool install graphifyy(pin exact version once spike S9 records it).graphify install --project --platform agents→ writes.agents/skills/and appends a section toAGENTS.md.graphify .→graphify-out/{graph.html,GRAPH_REPORT.md,graph.json,cost.json}(PowerShell:graphify ., not/graphify .).- Commit
graphify-out/exceptcost.json(already in.gitignore). - Ongoing:
graphify update .before each PR; CIgraph-driftjob runsgraphify update . && git diff --exit-code graphify-out/graph.jsonas warning-only. - Spike S9 decides whether
graphify hook install(post-commit + post-checkout background rebuild) is enabled, whether the drift check moves to pre-commit, or whether it stays manual. Until S9 closes, step 5 is the rule.
Behaviour¶
uv sync --lockedinstalls the runtime (empty) and dev groups fromuv.lock; a stale lock fails.uv run thumbforge --versionprintsthumbforge 0.0.0and exits0;uv run thumbforgewith no arguments prints Typer help and exits0;uv run thumbforge nopeexits2.pre-commit run --all-filesruns the eight hooks above and is green on the empty package.uv run zensical buildrenders every page in thenavlist plus the ADR and spec directories intosite/with zero warnings.- On push to
main,docs.ymldeployssite/to GitHub Pages. - On PR,
ci.ymlruns thetestmatrix (2 jobs) andgraph-drift(advisory).
Acceptance criteria¶
Per file/task:
- P0.1 —
uv sync --lockedexits0;uv run thumbforge --versionprintsthumbforge 0.0.0;git ls-filesshowsLICENSE,.editorconfig,.gitignore,README.md,uv.lock,pyproject.toml;python -c "import thumbforge; print(thumbforge.__version__)"prints0.0.0. - P0.2 —
pre-commit run --all-filesexits0;uv run ruff check .,uv run ruff format --check .,uv run pyrighteach exit0;uv run pytest --cov --cov-fail-under=80exits0withtests/test_version.pypassing;.prettierrccontains exactly the JSON above;.prettierignorecontains exactly the four entries above. - P0.3 —
AGENTS.mdexists with the sections listed inPLAN.md's plan (Project map, Commands, Before you start, Conventions, Definition of done, How to pick up a task, Never do);CLAUDE.mdandGEMINI.mdare the single pointer line. - P0.4 —
uv run zensical buildexits0with noWARNINGlines;site/adr/0016-zensical-docs-site/index.htmlandsite/specs/phase-0-infra/index.htmlexist; every ADR 0001–0016 is present withStatus:set. - P0.5 —
ci.ymlruns green on both matrix legs on the bootstrap PR;docs.ymldeploys on the first merge tomainand the Pages URL servesindex.html;release.ymlpassesactionlintbut is not exercised;gh label listshows every label fromlabels.ymlafter running the script indocs/maintainers/branching-and-releases.md; branch protection matches the list above (verified viagh api repos/<owner>/thumbforge/branches/main/protection). - P0.6 —
graphify-out/graph.jsonandGRAPH_REPORT.mdare committed;graphify query "where is the provider registry"returns an answer referencingdocs/developers/architecture.md;git check-ignore graphify-out/cost.jsonexits0. - P0.7 —
docs/specs/README.md,phase-0-infra.md,phase-1-skeleton.mdexist and are in the docs nav.
Test plan¶
- Unit:
tests/test_version.pyonly (CliRunner, no network). - Contract / integration / golden: none in Phase 0; markers are registered so later phases do not change
pyproject.toml. - Tooling:
pre-commit run --all-files,actionlint .github/workflows/*.ymlrun once locally on the bootstrap PR.
Open spikes¶
- S9 graphify on Windows/uv — decides hooks vs CI vs manual for P0.6; if graphify fails on Windows, P0.6 becomes a documented manual
uv tool run --from graphifyy graphify update .step and thegraph-driftCI job is removed. - S12 Python 3.15 GA — adds
"3.15"to the CI matrix. - S13 prettier via pre-commit on Windows — pins the
mirrors-prettierrev, or swaps hook 4 formdformat. - S14 zensical build of the ADR/spec tree with mermaid — may add a superfence config to
zensical.toml; records the exact zensical version to pin.