Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`just` (task runner) + `uv` (package manager); the [`Justfile`](Justfile) is the source of truth for recipes — run `just --list` or read it. The non-obvious bits:

- `just test [args]` — full suite in docker compose (Postgres 17). Args forward **unquoted**, so a spaced `-k` expression (`-k "a or b"`) word-splits and fails (`file or directory not found: or`) — run one keyword per invocation, or a single substring matching all targets. `tests/test_unit.py` + `tests/test_fake.py` need no Postgres (`uv run pytest tests/test_unit.py` works directly); `tests/test_integration.py` needs Postgres at `POSTGRES_DSN` (default `postgresql+asyncpg://outbox:outbox@localhost:5432/outbox`; `pg_engine` skips if unreachable). Coverage is on with `--cov-fail-under=100` — partial runs fail that gate; pass `--no-cov` or `--cov-fail-under=0` when iterating.
- `just lint` / `just lint-ci` — autofix vs non-mutating; `lint-ci` also runs the planning-bundle validator.
- `just lint` / `just lint-ci` — autofix vs non-mutating; `lint-ci` also runs the planning-change validator.
- `just docs-serve` / `just docs-build` — local hot-reload at `http://127.0.0.1:8000` / one-shot strict `mkdocs build`.

## Workflow

Planning uses a portable convention — `architecture/` (repo root) is the living **truth home** and promotion target; `planning/changes/` holds the per-change bundles. Start at the [Quick path](planning/README.md#quick-path-start-here) in `planning/README.md` (the authoritative spec) to pick a lane — **Full** (`design.md` + `plan.md`), **Lightweight** (single `change.md`), or **Tiny** (just a commit) — and ship. `just check-planning` validates bundles; `just index` prints the change + decision listing; `planning/_templates/` are copy-and-fill starting points. A design decision taken **without** a code change — especially a **rejected** option with a load-bearing reason — goes in `planning/decisions/YYYY-MM-DD-<slug>.md` (`status: accepted|superseded`) with a **Revisit trigger** so future reviews don't re-litigate it.
Planning uses a portable convention — `architecture/` (repo root) is the living **truth home** and promotion target; `planning/changes/` holds the per-change files. Start at the [Quick path](planning/README.md#quick-path-start-here) in `planning/README.md` (the authoritative spec) to pick a lane — **Full** (design template), **Lightweight** (change template), or **Tiny** (just a commit) — and ship. `just check-planning` validates changes; `just index` prints the change + decision listing; `planning/_templates/` are copy-and-fill starting points. A design decision taken **without** a code change — especially a **rejected** option with a load-bearing reason — goes in `planning/decisions/YYYY-MM-DD-<slug>.md` (`status: accepted|superseded`) with a **Revisit trigger** so future reviews don't re-litigate it.

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ lint-ci:
index:
uv run python planning/index.py

# Validate planning bundles + decisions (frontmatter, lanes, spec links); CI runs this.
# Validate planning changes + decisions (frontmatter, lanes, spec links); CI runs this.
check-planning:
uv run python planning/index.py --check

Expand Down
2 changes: 1 addition & 1 deletion architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ These files carry **no frontmatter** — they are prose, dated by git.
## Promotion rule

Shipping a change hand-edits the affected capability file(s) here to match the
new reality, in the same PR as the code. The change bundle stays in place under
new reality, in the same PR as the code. The change file stays in place under
[`../planning/changes/`](../planning/changes/) — no folder move.
2 changes: 1 addition & 1 deletion planning/.convention-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
2.0.0
96 changes: 66 additions & 30 deletions planning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,22 @@ at the repo root; this directory records *how it got there*.
**1. Choose a lane — first matching rule wins:**

1. Any of: needs design judgment · new file/module · public-API change ·
cross-cutting or multi-file · non-trivial test design → **Full**
(`design.md` + `plan.md`)
cross-cutting or multi-file · non-trivial test design → **Full** (design template)
2. Purely mechanical: typo · dep bump · linter/formatter/CI tweak ·
mechanical rename · single-line config → **Tiny** (no bundle, conventional
mechanical rename · single-line config → **Tiny** (no change file, conventional
commit)
3. Small-but-real, none of the above: ≲30 LOC net · ≤2 files · no new file ·
no public-API change · one straightforward test → **Lightweight**
(`change.md`)
no public-API change · one straightforward test → **Lightweight** (change template)

Ambiguous between two? Take the heavier. A `change.md` that outgrows its lane
splits into `design.md` + `plan.md`.
Ambiguous between two? Take the heavier. A lightweight change file that outgrows its lane is rewritten from the design template.

**2. Create the bundle** (Full / Lightweight only):
`planning/changes/YYYY-MM-DD.NN-<slug>/`, where `.NN` is a zero-padded
intra-day counter. Copy the matching template from
**2. Create the change file** (Full / Lightweight only):
`planning/changes/YYYY-MM-DD.NN-<slug>.md`, where `.NN` is a zero-padded
intra-day counter — copied from the matching template (design or change) in
[`_templates/`](_templates/).

**3. Ship in the implementing PR:** hand-edit the affected
`architecture/<capability>.md`, finalize the bundle's `summary:` to the
`architecture/<capability>.md`, finalize the change file's `summary:` to the
realized result, and run `just check-planning` before pushing.

## Conventions
Expand All @@ -43,20 +40,41 @@ realized result, and run `just check-planning` before pushing.

### Two axes, never mixed

- **`architecture/` (repo root) — the present.** One file per capability,
living prose, updated in the same PR that ships the change. The truth home.
- **`planning/changes/` — the past-and-pending.** One folder per change,
- **`architecture/` (repo root) — the present.** One file per capability, plus
a single `glossary.md` (the ubiquitous language); living prose, updated in the
same PR that ships the change. The truth home.
- **`planning/changes/` — the past-and-pending.** One file per change,
kept in place after ship.

A change **promotes** its conclusions into the affected
`architecture/<capability>.md` by hand **in the implementing PR, alongside the
code** — the edit rides in the same diff and is reviewed with it, never applied
as a separate post-merge step. That hand-edit is what keeps `architecture/`
true; the bundle stays in `changes/` as the *why*.
true; the change file stays in `changes/` as the *why*.

### Change bundles
### Glossary

A change is a folder `changes/YYYY-MM-DD.NN-<slug>/`:
`architecture/glossary.md` is the project's **ubiquitous language** — one page
defining the domain terms that code, specs, and capability pages all share. Like
the capability files beside it, it is living prose with **no frontmatter**, dated
by git, and authored lazily: it appears when the first term is worth pinning down.

Each entry is a term, a one-or-two-sentence definition of what it *is* (not what
it does), and an optional `_Avoid_:` line naming the synonyms to reject:

```md
**Timer**:
A scheduled future delivery, identified by a timer id.
_Avoid_: job, task, alarm
```

Keep it a glossary, not a spec — no implementation detail. A change that
introduces or sharpens a term updates `glossary.md` in the same PR, the same way
a behavior change promotes into a capability file.

### Change files

A change is a file `changes/YYYY-MM-DD.NN-<slug>.md`:

- `YYYY-MM-DD` — proposal date; `.NN` — zero-padded intra-day counter
(`.01`, `.02`, …) that breaks same-date ties so the timeline sorts stably.
Expand All @@ -65,25 +83,44 @@ A change is a folder `changes/YYYY-MM-DD.NN-<slug>/`:
`summary` is written when the change is created (the intent one-liner) and
**finalized at ship** to state the realized result — set in the implementing
PR, alongside the code and the `architecture/` promotion. No post-merge
bookkeeping, no folder move. `date` and `slug` are never written — they are
read from the bundle's directory name.
bookkeeping, no file move. `date` and `slug` are never written — they are
read from the file name.

### Three lanes

| Lane | Artifacts | Use when |
|------|-----------|----------|
| **Full** | `design.md` + `plan.md` | design judgment; new file/module; public-API change; cross-cutting/multi-file; non-trivial test design |
| **Lightweight** | `change.md` | small-but-real: ≲30 LOC net, ≤2 files, no new file, no public-API change, single straightforward test |
| **Full** | one change file from the design template | design judgment; new file/module; public-API change; cross-cutting/multi-file; non-trivial test design |
| **Lightweight** | one change file from the change template | small-but-real: ≲30 LOC net, ≤2 files, no new file, no public-API change, single straightforward test |
| **Tiny** | none — conventional commit | typo, dep bump, linter/formatter/CI tweak, mechanical rename, single-line config |

Heavier lane wins on ambiguity. A `change.md` that outgrows its lane splits
into `design.md` + `plan.md`.
Heavier lane wins on ambiguity. A lightweight change file that outgrows its lane is rewritten from the design template.

### Plans are ephemeral

The executable plan — task checklists, embedded code, commit sequences,
whatever the executor needs — is a working artifact, not history. Keep it out
of `changes/` and out of version control (git-ignored scratch, e.g.
`.superpowers/`). Once the change ships, the diff and the PR are the record
of execution; a committed plan duplicates them. `check-planning` rejects
anything in `changes/` that is not a flat change file.

### Lean specs

The change file is the single home of a change's rationale:

- The PR body summarizes and links to the change file — it never restates it.
- Rejected alternatives live in `decisions/` and are referenced, not retold.
- Show a sketch when the design needs code; never the full diff-to-be.
- Delete template sections that don't apply — an empty section is ceremony.
- Most designs fit well under ~700 words; length must buy information.

### Artifacts at a glance

- **`design.md`** — the spec: the *thinking* (why, design, trade-offs, scope).
- **`plan.md`** — the plan: the *sequencing* (the executor's task checklist).
- **`change.md`** — both, condensed, for the lightweight lane.
- **design template** — the spec: the *thinking* (why, design, trade-offs,
scope); the change file it produces is the single home of rationale (see
[Lean specs](#lean-specs)).
- **change template** — the condensed spec for the lightweight lane.
- **`releases/<semver>.md`** — per-release user-facing notes.
- **`audits/<date>-<slug>.md`** — findings from a code/docs/bug-hunt sweep;
spawns fix changes.
Expand All @@ -97,11 +134,10 @@ Templates live in [`_templates/`](_templates/).

### Frontmatter

`date` and `slug` are **derived from the directory / file name** — never
`date` and `slug` are **derived from the file name** — never
repeated in frontmatter. So:

- `design.md` / `change.md`: `summary` (single line) only.
- `plan.md`: **no frontmatter** — its identity is the bundle directory.
- `changes/*.md`: `summary` (single line) only.
- `decisions/*.md`: `status` (accepted|superseded), `summary`, and optional
`supersedes` / `superseded_by`.
- Files in `architecture/` carry **no** frontmatter — living prose, dated by git.
Expand All @@ -114,7 +150,7 @@ only field the index renders.

The listing is **generated**, not maintained — run `just index` to print it: a
flat, newest-first list of changes, then decisions newest-first. The frontmatter
in each bundle / decision file is the single source of truth; there is no
in each change / decision file is the single source of truth; there is no
committed copy to drift.

## Other
Expand Down
4 changes: 2 additions & 2 deletions planning/_templates/change.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ summary: One line — shown in the generated index. Written at creation; finaliz
# Change: One-line capitalized title

**Lane:** lightweight — ≲30 LOC net, ≤2 files, no new file, no public-API
change, a single straightforward test. If it outgrows this, split into
`design.md` + `plan.md`.
change, a single straightforward test. If it outgrows this, rewrite it from
the design template.

## Goal

Expand Down
34 changes: 12 additions & 22 deletions planning/_templates/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ summary: One line — shown in the generated index. Written at creation; finaliz

# Design: One-line capitalized title

<!-- The single home of this change's rationale: the PR body summarizes and
links here, never restates. Delete any section that doesn't apply — an empty
section is ceremony. Most designs fit well under ~700 words. -->

## Summary

One paragraph. What changes, at the level a reader needs to decide if this
Expand All @@ -12,37 +16,23 @@ spec is worth reading in full.
## Motivation

Why now. What is broken or missing. Concrete observations / numbers, not
abstract complaints. Link to memory entries or earlier specs when relevant.

## Non-goals

What is deliberately out of scope and (when nontrivial) why. Each item is
a sentence; one line each.
abstract complaints.

## Design

### 1. <First piece>

What changes, in enough detail that a reader who has not seen the codebase
can follow. Code samples / diagrams welcome.
can follow. Sketches and interface fragments welcome; never the full
diff-to-be. Reference rejected alternatives in `decisions/` instead of
retelling them.

### 2. <Second piece>

...

## Operations

Out-of-repo steps (DNS, infra, external account changes). Omit if none.

## Out of scope
## Non-goals

Already covered above under Non-goals if appropriate. Repeat-list of
explicitly-excluded follow-ups belongs here when the list is long.
What is deliberately out of scope and (when nontrivial) why. One line each.

## Testing

How we know it landed correctly. New pytest? Smoke check on live URL?
Lint pass? Be specific.
How we know it landed correctly. Be specific: the command and the expected
signal.

## Risk

Expand Down
15 changes: 15 additions & 0 deletions planning/_templates/glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Glossary

The project's ubiquitous language — the domain terms that code, specs, and
capability pages share. Living prose, no frontmatter, dated by git. Each entry is
a term, what it *is* (not what it does), and the synonyms to avoid. No
implementation detail; this is a glossary, not a spec.

**Term**:
A one-or-two-sentence definition of what it is.
_Avoid_: rejected-synonym, another-one

**Another term**:
Define what it is, tightly. Group related terms under `##` subheadings when
natural clusters emerge; a flat list is fine when they don't.
_Avoid_: …
46 changes: 0 additions & 46 deletions planning/_templates/plan.md

This file was deleted.

9 changes: 4 additions & 5 deletions planning/_templates/release.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# modern-di <version> — <one-line headline>
# <project> <version> — <one-line headline>

<One or two sentences: what this release is and its headline change. Say up
front if there are no behavior/API changes.>
Expand Down Expand Up @@ -30,10 +30,9 @@ Context a reader needs for the headline change. Omit for small releases.

## Downstream

What integrations (FastAPI, Litestar, FastStream, Typer, `modern-di-pytest`)
must do — e.g. bump their `modern-di` floor — or "No action needed" when there
is no API change.
What dependents must do — e.g. bump their version floor — or "No action
needed" when there is no API change. Omit if the project has no downstreams.

## Internals

- Coverage / tooling notes (e.g. 100% line coverage across Python 3.10–3.14).
- Coverage / tooling notes.
Loading