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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ Guidance for AI agents (Claude Code, etc.) working in this repository.
- [`architecture/`](architecture/) (repo root) — the per-capability living truth (overview, client, middleware, decoders, errors, resilience, optional extras, testing); the promotion target on every ship. **Read the relevant file before changing that capability.**
- [`planning/README.md`](planning/README.md) — the planning conventions (two axes, change bundles, three lanes, frontmatter) + the change Index.
- [`planning/changes/<YYYY-MM-DD.NN-slug>/`](planning/changes/) — per-change bundles (`design.md` + `plan.md`, or `change.md` for the lightweight lane).
- [`planning/decisions/<YYYY-MM-DD>-<slug>.md`](planning/decisions/) — design decisions taken (esp. options rejected with a load-bearing reason), each with a Revisit trigger; listed by `just index`.
- [`planning/audits/`](planning/audits/) — findings reports + `scripts/` tooling.
- [`planning/retros/`](planning/retros/) — retrospectives.
- [`planning/releases/`](planning/releases/) — per-version release notes (also published on GitHub Releases).
- [`planning/deferred.md`](planning/deferred.md) — review-surfaced, not-yet-actionable items.
- [`planning/_templates/`](planning/_templates/) — design/plan/change templates.

**Per-feature workflow:** brainstorming → `design.md` in `planning/changes/<id>/` → writing-plans → `plan.md` in the same bundle → executing-plans (or subagent-driven-development) → requesting-code-review → finishing-a-development-branch. On ship, promote the conclusions into the affected `architecture/<capability>.md` by hand and set `status: shipped` + `pr` + `outcome` in the implementing PR — there is no folder move. The change listing is generated: run `just index`. Topic slugs are kebab-case descriptions (`msgspec-decoder-adapter`), not story IDs.
**Per-feature workflow:** brainstorming → `design.md` in `planning/changes/<id>/` → writing-plans → `plan.md` in the same bundle → executing-plans (or subagent-driven-development) → requesting-code-review → finishing-a-development-branch. On ship, promote the conclusions into the affected `architecture/<capability>.md` by hand and set `status: shipped` + `pr` + `outcome` in the implementing PR — there is no folder move. The change listing is generated: run `just index`. A design decision taken without a code change — especially a candidate rejected with a load-bearing reason — is recorded as `planning/decisions/YYYY-MM-DD-<slug>.md` (the `decision.md` template, frontmatter `status: accepted|superseded`), each with a **Revisit trigger** so future reviews don't re-litigate it; listed by `just index`. Topic slugs are kebab-case descriptions (`msgspec-decoder-adapter`), not story IDs.

## Commands

Expand Down
20 changes: 14 additions & 6 deletions planning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ into `design.md` + `plan.md`.
- **`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.
- **`decisions/<YYYY-MM-DD>-<slug>.md`** — one file per design decision taken
(especially options *rejected*), each with a revisit trigger, so reviews don't
re-litigate them; listed by `just index`.
- **`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 @@ -65,22 +68,27 @@ Templates live in [`_templates/`](_templates/).

`design.md` / `change.md`: `status` (draft|approved|shipped|superseded),
`date`, `slug`, `summary` (single line), `supersedes`, `superseded_by`, `pr`,
`outcome`. `plan.md`: `status`, `date`, `slug`, `spec`, `pr`. Files in
`architecture/` carry **no** frontmatter — living prose, dated by git.
`outcome`. `plan.md`: `status`, `date`, `slug`, `spec`, `pr`.
`decisions/*.md`: `status` (accepted|superseded), `date`, `slug`, `summary`,
`supersedes`, `superseded_by`, `pr`.
Files in `architecture/` carry **no** frontmatter — living prose, dated by git.

## Index

The change listing is **generated**, not maintained — run `just index` to
print it (grouped by `status`: In progress / Shipped / Superseded). The
frontmatter in each bundle is the single source of truth; there is no
committed copy to drift.
The listing is **generated**, not maintained — run `just index` to print it:
changes grouped by `status` (In progress / Shipped / Superseded), then
decisions newest-first. The frontmatter in each bundle / decision file is the
single source of truth; there is no committed copy to drift.

## Other

- **[`architecture/`](../architecture/)** at the repo root — the living
per-capability truth (overview, client, middleware, decoders, errors,
resilience, optional extras, testing). This is the promotion target on
every ship.
- **[decisions/](decisions/)** — design decisions taken (and alternatives
rejected), each with a revisit trigger, so reviews don't re-litigate them;
indexed by `just index`.
- **[audits/](audits/)** — findings reports (deep audit + delta audits) and
their `scripts/` tooling.
- **[deferred.md](deferred.md)** — real-but-unscheduled items with revisit
Expand Down
26 changes: 26 additions & 0 deletions planning/_templates/decision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
status: accepted # accepted | superseded
date: YYYY-MM-DD
slug: my-decision
summary: One line — shown in `just index`.
supersedes: null
superseded_by: null
pr: null # PR/commit where the decision was made or recorded
---

# One-line capitalized title

**Decision:** What was decided, in a sentence.

## Context

Why this came up; the options that were on the table.

## Decision & rationale

The call and why — including why the alternatives were rejected. Enough that a
future explorer doesn't re-litigate it.

## Revisit trigger

The concrete signal that should reopen this decision.
Empty file added planning/decisions/.gitkeep
Empty file.
43 changes: 32 additions & 11 deletions planning/index.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# ruff: noqa: INP001, D212 # planning/ is not a Python package; D212/D213 conflict differs from faststream-outbox
"""
Generate the planning change index from bundle frontmatter.
Generate the planning index from frontmatter.

Run via ``just index``. Globs ``planning/changes/*/``, reads each bundle's
``design.md`` (falling back to ``change.md``) frontmatter, and prints a
Markdown listing grouped by lifecycle status to stdout. Never writes a file:
the listing is a query over the bundles, not a committed artifact.
Run via ``just index``. Globs ``planning/changes/*/`` (each bundle's ``design.md``,
falling back to ``change.md``) and ``planning/decisions/*.md``, reads their
frontmatter, and prints a Markdown listing to stdout — changes grouped by lifecycle
status, then decisions newest-first. Never writes a file: the listing is a query over
the files, not a committed artifact.
"""

import pathlib
import sys


CHANGES_DIR = pathlib.Path(__file__).parent / "changes"
DECISIONS_DIR = pathlib.Path(__file__).parent / "decisions"
GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
("In progress", ("draft", "approved")),
("Shipped", ("shipped",)),
Expand Down Expand Up @@ -55,8 +57,23 @@ def load_bundles() -> list[dict[str, str]]:
return bundles


def load_decisions() -> list[dict[str, str]]:
"""Read frontmatter from every decision file under ``DECISIONS_DIR``."""
decisions: list[dict[str, str]] = []
if not DECISIONS_DIR.is_dir():
return decisions
for path in sorted(DECISIONS_DIR.glob("*.md")):
if path.name == "README.md" or path.name.startswith("_"):
continue
fields = parse_frontmatter(path.read_text(encoding="utf-8"))
fields["path"] = f"decisions/{path.name}"
fields["name"] = path.stem
decisions.append(fields)
return decisions


def format_row(bundle: dict[str, str]) -> str:
"""Render one bundle as a Markdown list item."""
"""Render one bundle or decision as a Markdown list item."""
slug = bundle.get("slug", "?")
path = bundle.get("path", "")
pr = bundle.get("pr") or "—"
Expand All @@ -70,24 +87,28 @@ def format_row(bundle: dict[str, str]) -> str:
return line


def render(bundles: list[dict[str, str]]) -> str:
"""Render the full grouped Markdown listing."""
out = ["# Change index", "", "_Generated by `just index` — do not edit._", ""]
def render(bundles: list[dict[str, str]], decisions: list[dict[str, str]]) -> str:
"""Render the full Markdown listing: changes by status, then decisions."""
out = ["# Planning index", "", "_Generated by `just index` — do not edit._", "", "## Changes", ""]
for title, statuses in GROUPS:
out += [f"## {title}", ""]
out += [f"### {title}", ""]
rows = sorted(
(b for b in bundles if b.get("status") in statuses),
key=lambda b: b.get("name", ""),
reverse=True,
)
out += [format_row(b) for b in rows] if rows else ["_None._"]
out.append("")
out += ["## Decisions", ""]
decision_rows = sorted(decisions, key=lambda d: d.get("name", ""), reverse=True)
out += [format_row(d) for d in decision_rows] if decision_rows else ["_None._"]
out.append("")
return "\n".join(out).rstrip() + "\n"


def main() -> int:
"""Print the listing to stdout."""
sys.stdout.write(render(load_bundles()))
sys.stdout.write(render(load_bundles(), load_decisions()))
return 0


Expand Down