Conventions¶
AGENTS.md is canonical; this page expands on it for humans.
Code¶
- Python 3.14,
src/layout,uvfor everything (uv sync --locked,uv run …,uv tool install). - Full type hints;
pyrightstrict onsrc/. PreferProtocolover ABCs for interfaces. - Pydantic v2 models at boundaries (CLI input, provider I/O, settings);
dataclass(frozen=True, slots=True)for internal value objects. StrEnumfor enumerations,matchfor dispatch,pathlib.Pathfor every path.- Async only in providers, sources and batch fan-out; the CLI calls
asyncio.runonce. - Logging via
thumbforge.logging.get_logger(__name__); neverprint()outsidecli/_render.py. - Errors: raise a
ThumbforgeErrorsubclass;cli/_errors.pyowns exit codes (PLAN.md§5.1). - Secrets: env var or keyring only.
- Every schema change ships with an Alembic migration.
Formatting and linting¶
rufflints and formats Python (line-length = 100).prettierformats Markdown, YAML and JSON only, via the pre-commit hook. No Node project in the repo.pre-commit run --all-filesmust be green before pushing.
Commits and pushing¶
- Conventional Commits:
feat(scope): …,fix(scope): …,docs: …,chore(ci): …,refactor: …,test: …. Scope = package (providers,batch,cli, …) or area (ci,adr). - One roadmap task = one commit; name the ROADMAP task in the subject or body.
- Update docs, specs and
AGENTS.mdin the same commit when behaviour or structure changes. - Solo workflow, no pull requests: run both local CI legs (
scripts/ci-local.sh,scripts/ci-local.cmd) and read your own diff before pushing, thengit fetch origin && git rebase origin/main && git push origin HEAD:main. History stays linear: rebase, never merge, never force-push. - Check the remote run after the push (
gh run list --branch main --limit 3); a red run is fixed forward immediately.
Knowledge graph¶
Regenerate after any structural source change and commit graphify-out/ (see AGENTS.md):
graphify extract . --code-only && graphify cluster-only . --no-label
CI runs an advisory drift check (graphify drift (advisory), continue-on-error, not one of
the required status checks) that re-extracts and compares the declaration node set —
never a byte diff, since graph.json embeds built_at_commit. It answers one question: was a
symbol added, removed or renamed without re-running the extract. A failure names the symbols.
It deliberately ignores everything else in the graph, because everything else varies between
machines without the source changing. Measured over five consecutive false alarms (issue #26): CI
resolves imports less completely than a local run and invents placeholder nodes
(thumbforge_core_errors where a local extract has src_thumbforge_core_errors); external symbol
nodes depend on what is installed; *_rationale_<line> ids embed a line number, so moving a comment
renames the node. Edges are dropped entirely — they have no environment-independent form, since CI
can be missing an edge between two fully resolved nodes when it resolved that import to a module
placeholder instead. The cost is that a changed call edge between unchanged symbols goes unnoticed,
which is acceptable given the check already cannot see the staleness class described below.
Reproduce it locally by diffing a copy of the committed graph against a fresh extract; keep scratch
files under the ignored .pytest_tmp/:
mkdir -p .pytest_tmp
cp graphify-out/graph.json .pytest_tmp/committed-graph.json
graphify extract . --code-only
uv run python scripts/check_graph_drift.py .pytest_tmp/committed-graph.json graphify-out/graph.json
Regenerate as the last step before git add, after formatters have run — otherwise their edits
shift line spans and reintroduce drift.
Rebuild from scratch when counts diverge¶
graphify extract is incremental and never prunes symbols deleted from source, so the
committed graph accumulates staleness that neither the incremental extract nor the CI drift check
can see — the check compares incrementally on top of whatever is already committed, so a phantom
present in both sides cancels out. A clean rebuild is the ground truth:
rm -rf graphify-out
graphify extract . --code-only && graphify cluster-only . --no-label
Measured at 3ff0f30, the incremental graph had drifted to 612 nodes/1179 edges against 577/1282
from scratch. The clean build has fewer nodes but more edges, and is strictly more accurate: it
had dropped a phantom node for the AssetStoreError alias deleted two commits earlier, added the
test_ids.py/test_render.py module nodes the stale graph was missing, and resolved 11 unresolved
import placeholders (imports_from thumbforge_cli_errors) into real cross-module edges
(imports src_thumbforge_cli_errors_handle_errors).
Because AGENTS.md points every agent at GRAPH_REPORT.md first, a stale graph misleads later
work. Rebuild clean after deleting or renaming exported symbols, and whenever node/edge counts
diverge noticeably from the committed graph. Note that a clean rebuild is a large diff and shifts
ids, so give it its own commit rather than burying it in a feature change.
Documentation¶
- Decisions → ADR (
docs/adr/, use0000-template.md; supersede, never edit an Accepted decision). - Behaviour → phase spec (
docs/specs/). - Spike results →
docs/spikes/<topic>.md; close the item inOPEN_QUESTIONS.md. - Terms → Glossary (
docs/developers/glossary.md). - Docs are published with zensical to GitHub Pages on every push to
main.