Feature Methodology — AI Session Definition of Done¶
This page is the contract every AI coding session follows when shipping a feature in this repo. A feature is not complete until every step below is checked.
Do them in order. The list is intentionally repetitive of itself across artifacts (plan → docs → changelog) — that redundancy is the point: it keeps human reviewers, the mkdocs site, and the OpenAPI version aligned.
1. Write the plan first — docs/feature/<date>-<name>.md¶
Before touching code, drop a plan into docs/feature/ using the filename pattern:
Example: docs/feature/2026-05-24-vendor-credits.md.
The plan is the source of truth for scope. It must include:
- Title and date — match the filename.
- Problem / motivation — one paragraph, why this is being built.
- In scope / out of scope — explicit list. Anything not in scope here cannot land in this feature.
- Data model changes — new tables, new columns, FKs, indexes. Name every alembic revision you plan to write.
- GraphQL surface — every new query, mutation, type, input, enum. Use the same shape as existing feature pages (see recurring-invoices.md for the template).
- RBAC — name the new
PathandResourceenum members, the menu wiring, and whether a backfill script is required. - Background tasks / cron — list every taskiq task that will be added and where it lives.
- Frontend contract — what the frontend has to wire up (per the frontend handoff convention).
- Open questions — anything still ambiguous. Do not start coding while these are unanswered.
The plan stays in docs/feature/ after the feature ships — it becomes the long-lived feature page. Update it as scope changes; do not delete it.
2. Detail of what is getting done¶
Inside the same plan file, expand a "What's being implemented" section that enumerates the concrete deltas:
- New / changed SQLAlchemy models (file paths).
- New / changed services and the methods they expose.
- New / changed GraphQL resolvers (file paths).
- New / changed worker tasks and their schedules.
- Any one-off scripts (
scripts/<name>.py) that must run post-deploy.
Be specific enough that a reviewer can navigate the diff from this list alone. Cross-link files with markdown links ([InvoiceService](https://github.com/codetec-inc/cifras-backend-strawberry/blob/main/app/graphql/invoices/services/invoice_service.py)) so the rendered page is clickable from the mkdocs site.
3. Alembic migrations — always required for schema changes¶
Every schema change ships with an alembic file in alembic/versions/. No exceptions, no auto-generated noise, no inline ALTERs in Python.
Filename and revision¶
- Filename:
YYYYMMDD_<short_snake_description>.py(e.g.20260524_add_vendor_credits.py). revision: a short, stable, snake_case slug —add_vendor_credits, not a random hash. Other migrations reference this slug.down_revision: the slug of the most recent migration onmain/staging(runls alembic/versions | tailto find the current tip).Create Date: ISO date in the docstring matches the filename date.
Reference template — 20260523_add_unit_cost_snapshot_to_invoice_details.py:
"""add unit_cost_snapshot to invoice_details
Revision ID: add_unit_cost_snapshot
Revises: add_inventory_dimensions
Create Date: 2026-05-23 00:00:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "add_unit_cost_snapshot"
down_revision: str | None = "add_inventory_dimensions"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
...
def downgrade() -> None:
...
Rules¶
upgradeanddowngrademust both be implemented. A migration withpassindowngradeis rejected. If a column genuinely cannot be safely dropped (e.g. data loss), say so in the docstring and still write the drop.- One concern per file. Adding three unrelated columns to three tables = three migrations. This keeps
down_revisionchains debuggable. - Add a
comment=to every new column explaining what it represents. The DB schema is documentation too. - Indexes: only add the indexes the feature actually needs for its query patterns. Don't speculatively index FKs — postgres handles those well enough until proven otherwise. If you're removing one, do it in its own migration.
- Enums /
Path/Resource: when a new module gets its own RBAC path, also add the enum entry in app/graphql/rbac/models/models.py (bothPathandResource) and wire a backfill in scripts/backfill_new_path_permissions.py (or a new sibling script) so existing tenants pick up the permission row. Reference the backfill in the plan and the changelog. - Test it locally:
task migrate-devmust apply cleanly on a fresh database and on top of the current staging snapshot. Run downgrade once locally to prove it works.
4. New resource? Wire the full chain¶
If the feature introduces a new top-level resource (a new module under app/graphql/<resource>/), the AI session is responsible for every layer:
- SQLAlchemy model under
app/graphql/<resource>/models/. - Repository under
app/graphql/<resource>/repositories/. - Service under
app/graphql/<resource>/services/. - GraphQL types / inputs / queries / mutations under
app/graphql/<resource>/. - Mount the queries/mutations into the schema (
app/graphql/schema.py). - New
PathandResourceenum members (see above). - Backfill script for
PathPermissionrows on existing tenants. - Alembic migration that creates the table(s) and any FKs.
- Tests under
tests/graphql/<resource>/covering the repo + service + at least one resolver happy path. - Worker tasks (if the resource has background work) under
app/graphql/<resource>/tasks/and registered via the--tasks-patternalready in Taskfile.yml. - A feature doc — this is the same file you started with in step 1.
The pre-flight check is: can the frontend list, create, update, delete, and (if applicable) export this resource with the schema you exported? If not, the feature is not done.
5. task all must succeed¶
Before declaring done, run:
That runs lint, typecheck-basedpy, file-length, and the two schema exports (gql, gql-admin). Every one must pass with no warnings the session introduced.
If anything fails:
- Lint / format — fix in place; don't add
# noqablanket-suppressions. - Type errors — fix at the source.
# pyright: ignore[<specific-rule>]is the last resort and must include the rule code. - File length — split the offending file; that check exists for a reason.
- Schema export — usually a missing import or an undefined type. Fix and re-run.
Then run task test for the affected area. New code lands with new tests (see step 4).
6. mkdocs page — what was implemented + future work¶
The plan file from step 1 (docs/feature/YYYY-MM-DD-<name>.md) is the mkdocs page. Before the feature ships, update it to its final form:
- "What shipped" — close the loop on the "what's being implemented" list. Anything that was in the plan but didn't ship moves to the future-additions section with a reason.
- "Future additions" — explicit, named follow-ups. Each item gets a one-line "why deferred" so the next session knows whether it's a fast follow or a someday-maybe. If a follow-up is concrete enough, file a roadmap entry under docs/roadmap/ and link to it.
- GraphQL surface — final query/mutation/type list (frontend reads this).
- Frontend impact — what the frontend has to wire up. The user's memory rule: "After big builds, write docs/feature/
.md describing the frontend contract." This section is that contract.
Then wire the page into mkdocs.yml under the right top-level section (Accounting Features, Core Operations, Roadmap, or PAC — pick the one that matches the domain). The nav entry is <Title>: feature/<filename>.md.
Build the site locally to confirm:
--strict is required — broken links must not ship.
7. Bump the version and add the changelog entry¶
Last step. Use the bump script — it edits app/version.py, pyproject.toml, scaffolds the changelog page, and wires it into mkdocs.yml:
Picking the next version (semver, per docs/changelog/index.md):
- MAJOR — breaking GraphQL schema change or a migration the frontend / integrations must coordinate on.
- MINOR — new feature, additive schema.
- PATCH — fixes and additive fields the frontend can ignore.
Fill in docs/changelog/<version>.md. Follow the shape of 1.0.1.md:
- Highlights — 1–3 bullets a non-engineer can read.
- Added — new features, grouped by theme; each links to its
feature/page. - Changed — non-additive behavior changes.
- Fixed — bug fixes.
- Migrations — every alembic revision in this release, listed in
down_revisionorder, plus any one-off scripts (with the script path). - Frontend impact — repeat the frontend contract from the feature page. Yes, intentionally duplicated.
- Versioning notes — note the bump command used and any deploy-order constraints.
Re-run task all after the bump — the version change touches app/version.py and pyproject.toml, so the lints must still pass.
Definition-of-done checklist¶
Copy this into the PR description and tick boxes as you go:
- Plan written in
docs/feature/YYYY-MM-DD-<name>.mdbefore any code change. - "What's being implemented" section enumerates every file delta.
- Alembic migration(s) added, both
upgradeanddowngradeimplemented,down_revisionpoints at the current tip. - New
Path/Resourceenum members added and backfill script updated (if a new resource). - Full module chain wired: model → repo → service → graphql → schema mount → tests.
-
task allis green. -
task testis green for the touched areas. - Feature mkdocs page reflects what actually shipped + a "Future additions" section.
- Page wired into mkdocs.yml;
uv run mkdocs build --strictis clean. -
scripts/bump_version.py <version> --changelog-stubrun. -
docs/changelog/<version>.mdfilled in with Highlights / Added / Changed / Fixed / Migrations / Frontend impact. -
task allre-run after the version bump.
If any box is unchecked, the feature is not done.