diff --git a/CLAUDE.md b/CLAUDE.md index 3f6af98..d1d12e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,26 +98,15 @@ class MyGroup(Group): ## Workflow -Planning follows a portable two-axis convention (shared with -`faststream-outbox`); full details in [`planning/README.md`](planning/README.md). - -- **`architecture/`** (repo root) is the **truth home** — living capability - prose, the promotion target on every ship. The `## Architecture` section - above is quick orientation; `architecture/` holds the authoritative, - up-to-date account. -- **`planning/changes//`** are change - bundles: `design.md` + `plan.md` (full lane), or `change.md` (lightweight). - Tiny changes (typo, dep bump, CI tweak) skip bundles entirely. -- **`planning/decisions/-.md`** — when something is decided - *not* to be done (a rejected option with a load-bearing reason), record it - here, one file each with a revisit trigger, so it isn't re-litigated; listed - by `just index`. (`deferred.md` is the flat backlog of real-but-unscheduled - work.) -- Templates live in [`planning/_templates/`](planning/_templates/). -- **Shipping a change** hand-edits the affected - `architecture/.md` and sets `status: shipped` + `pr:` + - `outcome:` **in the implementing PR** — there is no folder move. The - change listing is generated: run `just index`. +Planning uses a portable two-axis 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` to choose a lane, create a bundle, and ship — that file +is the authoritative spec. Run `just check-planning` to validate bundles and +`just index` to print the change listing. The `## Architecture` section above +is quick orientation; `architecture/` holds the authoritative account. + - **Cutting a release (maintainers)** is tag-driven via [`.github/workflows/release.yml`](.github/workflows/release.yml): write the notes at `planning/releases/.md` (used verbatim as the GitHub Release diff --git a/Justfile b/Justfile index 7128aed..cf86b79 100644 --- a/Justfile +++ b/Justfile @@ -15,6 +15,7 @@ lint-ci: uv run ruff format --check uv run ruff check --no-fix uv run ty check + uv run python planning/index.py --check test *args: uv run --no-sync pytest {{ args }} @@ -41,3 +42,7 @@ docs-build: # Print the planning change index (grouped by status) to stdout. index: uv run python planning/index.py + +# Validate planning bundles + decisions (frontmatter, lanes, spec links); CI runs this. +check-planning: + uv run python planning/index.py --check diff --git a/planning/README.md b/planning/README.md index e51a3d2..87c16ec 100644 --- a/planning/README.md +++ b/planning/README.md @@ -4,6 +4,35 @@ Specs, plans, and change history for `modern-di`. The living truth about *what the system does now* lives in [`architecture/`](../architecture/) at the repo root; this directory records *how it got there*. +## Quick path (start here) + +> The fast lane for making a change. The full reference is in +> [Conventions](#conventions) below — read it only when this isn't enough. + +**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`) +2. Purely mechanical: typo · dep bump · linter/formatter/CI tweak · + mechanical rename · single-line config → **Tiny** (no bundle, 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`) + +Ambiguous between two? Take the heavier. A `change.md` that outgrows its lane +splits into `design.md` + `plan.md`. + +**2. Create the bundle** (Full / Lightweight only): +`planning/changes/YYYY-MM-DD.NN-/`, where `.NN` is a zero-padded +intra-day counter. Copy the matching template from +[`_templates/`](_templates/). + +**3. Ship in the implementing PR:** hand-edit the affected +`architecture/.md`, fill `outcome:` in +the bundle frontmatter, and run `just check-planning` before pushing. + ## Conventions > This section is the portable convention — identical across the @@ -33,8 +62,8 @@ A change is a folder `changes/YYYY-MM-DD.NN-/`: - `` — kebab-case description, not a story ID. `summary` is written when the change is created (it is the change's -one-liner). The implementing PR then sets `status: shipped` and fills `pr` -and `outcome` **in the branch**, alongside the code and the `architecture/` +one-liner). The implementing PR fills `outcome` +**in the branch**, alongside the code and the `architecture/` promotion — no post-merge bookkeeping, no folder move. ### Three lanes @@ -66,18 +95,21 @@ Templates live in [`_templates/`](_templates/). ### Frontmatter -`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`. `decisions/*.md`: -`status` (accepted|superseded), `date`, `slug`, `summary`, `supersedes`, -`superseded_by`, `pr`. Files in `architecture/` carry **no** frontmatter — -living prose, dated by git. +`design.md` / `change.md`: `date`, `slug`, `summary` (single line), `outcome`. +`plan.md`: `date`, `slug`, `spec`. `decisions/*.md`: `status` +(accepted|superseded), `date`, `slug`, `summary`, `supersedes`, `superseded_by`. +Files in `architecture/` carry **no** frontmatter — living prose, dated by git. + +**`outcome`** is filled at ship time: one line, ~1–3 sentences (≤ ~300 chars), +stating the realized result — what shipped and its effect (deviations from the +plan included), written so a future reader grasps the consequence without +opening the diff. It is distinct from `summary`, which is the pre-ship intent +one-liner. ## Index 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 +changes 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 diff --git a/planning/_templates/change.md b/planning/_templates/change.md index 7ffec26..ab6bc1f 100644 --- a/planning/_templates/change.md +++ b/planning/_templates/change.md @@ -1,12 +1,8 @@ --- -status: draft date: YYYY-MM-DD slug: my-change summary: One line — shown in the generated index. Fill at ship time. -supersedes: null -superseded_by: null -pr: null -outcome: null +outcome: Realized result — filled at ship time (~1–3 sentences); see README "Frontmatter". --- # Change: One-line capitalized title diff --git a/planning/_templates/decision.md b/planning/_templates/decision.md index 940fb37..e3801b8 100644 --- a/planning/_templates/decision.md +++ b/planning/_templates/decision.md @@ -5,7 +5,6 @@ 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 diff --git a/planning/_templates/design.md b/planning/_templates/design.md index b9e11c9..861ebd2 100644 --- a/planning/_templates/design.md +++ b/planning/_templates/design.md @@ -1,12 +1,8 @@ --- -status: draft date: YYYY-MM-DD slug: my-change summary: One line — shown in the generated index. Fill at ship time. -supersedes: null -superseded_by: null -pr: null -outcome: null +outcome: Realized result — filled at ship time (~1–3 sentences); see README "Frontmatter". --- # Design: One-line capitalized title diff --git a/planning/_templates/plan.md b/planning/_templates/plan.md index f2b90e8..202ff6a 100644 --- a/planning/_templates/plan.md +++ b/planning/_templates/plan.md @@ -1,9 +1,7 @@ --- -status: draft date: YYYY-MM-DD slug: my-change -spec: my-change -pr: null +spec: design.md --- # — implementation plan diff --git a/planning/changes/2026-06-05.01-bug-hunt-audit/design.md b/planning/changes/2026-06-05.01-bug-hunt-audit/design.md index f9a644d..cd403eb 100644 --- a/planning/changes/2026-06-05.01-bug-hunt-audit/design.md +++ b/planning/changes/2026-06-05.01-bug-hunt-audit/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: bug-hunt-audit summary: Four-dimension bug-hunt audit harness and report; 18 findings actioned in 2.15.0. -supersedes: null -superseded_by: null -pr: null outcome: Four-dimension bug-hunt audit harness; report in audits/2026-06-05-bug-hunt-audit-report.md; 18 findings actioned in 2.15.0 (#188–#197). --- diff --git a/planning/changes/2026-06-05.01-bug-hunt-audit/plan.md b/planning/changes/2026-06-05.01-bug-hunt-audit/plan.md index 11c0811..4dbe2b4 100644 --- a/planning/changes/2026-06-05.01-bug-hunt-audit/plan.md +++ b/planning/changes/2026-06-05.01-bug-hunt-audit/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: bug-hunt-audit spec: design.md -pr: null --- # Bug-Hunt Audit Implementation Plan diff --git a/planning/changes/2026-06-05.02-singleton-rlock/design.md b/planning/changes/2026-06-05.02-singleton-rlock/design.md index 82f5e11..e2b4a42 100644 --- a/planning/changes/2026-06-05.02-singleton-rlock/design.md +++ b/planning/changes/2026-06-05.02-singleton-rlock/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: singleton-rlock summary: RLock-guarded singleton creation to eliminate re-entrant deadlock; shipped in 2.15.0. -supersedes: null -superseded_by: null -pr: null outcome: RLock guards singleton creation; shipped in 2.15.0. --- diff --git a/planning/changes/2026-06-05.02-singleton-rlock/plan.md b/planning/changes/2026-06-05.02-singleton-rlock/plan.md index 8a74fd8..466541d 100644 --- a/planning/changes/2026-06-05.02-singleton-rlock/plan.md +++ b/planning/changes/2026-06-05.02-singleton-rlock/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: singleton-rlock spec: design.md -pr: null --- # Singleton RLock Fix Implementation Plan diff --git a/planning/changes/2026-06-05.03-validate-rework/design.md b/planning/changes/2026-06-05.03-validate-rework/design.md index 12f6f95..b9bbdad 100644 --- a/planning/changes/2026-06-05.03-validate-rework/design.md +++ b/planning/changes/2026-06-05.03-validate-rework/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: validate-rework summary: Reworked validate() for transitive cycle/scope checks; shipped in 2.15.0. -supersedes: null -superseded_by: null -pr: null outcome: validate() reworked for transitive cycle/scope checks; shipped in 2.15.0. --- diff --git a/planning/changes/2026-06-05.03-validate-rework/plan.md b/planning/changes/2026-06-05.03-validate-rework/plan.md index 329fb6a..b36e591 100644 --- a/planning/changes/2026-06-05.03-validate-rework/plan.md +++ b/planning/changes/2026-06-05.03-validate-rework/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-05 slug: validate-rework spec: design.md -pr: null --- # `Container.validate()` Rework Implementation Plan diff --git a/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/design.md b/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/design.md index 3a1c6e6..4d008d9 100644 --- a/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/design.md +++ b/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-07 slug: mkdocs-github-pages-migration summary: Docs hosting moved to GitHub Pages at modern-di.modern-python.org. -supersedes: null -superseded_by: null -pr: null outcome: Docs hosting moved to GitHub Pages at modern-di.modern-python.org. --- diff --git a/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/plan.md b/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/plan.md index 6d46f7b..f8d23d9 100644 --- a/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/plan.md +++ b/planning/changes/2026-06-07.01-mkdocs-github-pages-migration/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-07 slug: mkdocs-github-pages-migration spec: design.md -pr: null --- # MkDocs to GitHub Pages Migration Implementation Plan diff --git a/planning/changes/2026-06-08.01-scheduled-dep-check/design.md b/planning/changes/2026-06-08.01-scheduled-dep-check/design.md index 82cad2e..2f9ccf8 100644 --- a/planning/changes/2026-06-08.01-scheduled-dep-check/design.md +++ b/planning/changes/2026-06-08.01-scheduled-dep-check/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-08 slug: scheduled-dep-check summary: Weekly scheduled dependency-check GitHub Actions workflow. -supersedes: null -superseded_by: null -pr: null outcome: Weekly scheduled dependency-check workflow (.github/workflows/scheduled.yml). --- diff --git a/planning/changes/2026-06-08.01-scheduled-dep-check/plan.md b/planning/changes/2026-06-08.01-scheduled-dep-check/plan.md index f889350..f2dcfe4 100644 --- a/planning/changes/2026-06-08.01-scheduled-dep-check/plan.md +++ b/planning/changes/2026-06-08.01-scheduled-dep-check/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-08 slug: scheduled-dep-check spec: design.md -pr: null --- # Scheduled Dependency-Breakage Check Implementation Plan diff --git a/planning/changes/2026-06-09.01-docs-improvements/design.md b/planning/changes/2026-06-09.01-docs-improvements/design.md index be130b0..c9a0e79 100644 --- a/planning/changes/2026-06-09.01-docs-improvements/design.md +++ b/planning/changes/2026-06-09.01-docs-improvements/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-09 slug: docs-improvements summary: Docs-site improvements: recipes section, concept pages, refreshed Quick Start. -supersedes: null -superseded_by: null -pr: null outcome: Docs-site improvements shipped. --- diff --git a/planning/changes/2026-06-09.02-migration-guide-from-that-depends/design.md b/planning/changes/2026-06-09.02-migration-guide-from-that-depends/design.md index 75359f4..1de90e3 100644 --- a/planning/changes/2026-06-09.02-migration-guide-from-that-depends/design.md +++ b/planning/changes/2026-06-09.02-migration-guide-from-that-depends/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-09 slug: migration-guide-from-that-depends summary: Migration guide from that-depends covering all provider types and conceptual shifts. -supersedes: null -superseded_by: null -pr: null outcome: docs/migration/from-that-depends.md published. --- diff --git a/planning/changes/2026-06-12.01-code-docs-audit/design.md b/planning/changes/2026-06-12.01-code-docs-audit/design.md index 81cb084..e669d83 100644 --- a/planning/changes/2026-06-12.01-code-docs-audit/design.md +++ b/planning/changes/2026-06-12.01-code-docs-audit/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-12 slug: code-docs-audit summary: Full code+docs audit harness; produced the 57-finding report. -supersedes: null -superseded_by: null -pr: null outcome: Full code+docs audit harness; produced the 57-finding report in audits/2026-06-12-code-docs-audit-report.md. --- diff --git a/planning/changes/2026-06-12.01-code-docs-audit/plan.md b/planning/changes/2026-06-12.01-code-docs-audit/plan.md index 6d7efc1..724f9da 100644 --- a/planning/changes/2026-06-12.01-code-docs-audit/plan.md +++ b/planning/changes/2026-06-12.01-code-docs-audit/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-12 slug: code-docs-audit spec: design.md -pr: null --- # Code & Docs Audit Implementation Plan diff --git a/planning/changes/2026-06-12.02-audit-fixes/change.md b/planning/changes/2026-06-12.02-audit-fixes/change.md index 3940304..a8511d3 100644 --- a/planning/changes/2026-06-12.02-audit-fixes/change.md +++ b/planning/changes/2026-06-12.02-audit-fixes/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-12 slug: audit-fixes summary: First batch of code+docs audit fixes. Plan-only; spec = the audit report. spec: ../../../audits/2026-06-12-code-docs-audit-report.md -pr: "202" +outcome: Fixed bugs B-1..B-11/X-1, dead code Q-2..Q-4, pinning tests Q-10..Q-15, and doc drift D-3/D-6..D-14 from the 2026-06-12 code+docs audit; shipped in PR #202. --- # Audit Fixes Implementation Plan diff --git a/planning/changes/2026-06-13.01-audit-fixes-round2/change.md b/planning/changes/2026-06-13.01-audit-fixes-round2/change.md index cd4b02c..bcc2422 100644 --- a/planning/changes/2026-06-13.01-audit-fixes-round2/change.md +++ b/planning/changes/2026-06-13.01-audit-fixes-round2/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-13 slug: audit-fixes-round2 summary: Round-2 fixes for the 21 deferred code+docs audit findings. Plan-only; spec = the audit report. spec: ../../../audits/2026-06-12-code-docs-audit-report.md -pr: "203" +outcome: Fixed 21 deferred code+docs audit findings (Q-1/Q-5/Q-9/X-2..X-5, Q-6..Q-8/X-6, D-2/D-4/D-5/G-1..G-11) including None-injection and coverage-gate move; shipped in PR #203. --- # Audit Fixes — Round 2 Implementation Plan diff --git a/planning/changes/2026-06-13.01-docs-ux-audit/design.md b/planning/changes/2026-06-13.01-docs-ux-audit/design.md index 12867d2..c8a23e4 100644 --- a/planning/changes/2026-06-13.01-docs-ux-audit/design.md +++ b/planning/changes/2026-06-13.01-docs-ux-audit/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-13 slug: docs-ux-audit summary: Reader-experience audit producing a 70-finding report (16 Medium, 54 Low). -supersedes: null -superseded_by: null -pr: 212 outcome: Produced the 70-finding reader-experience report (0 High, 16 Medium, 54 Low) in audits/2026-06-13-docs-ux-audit-report.md. All 16 Mediums fixed in PR #212; 54 Lows catalogued for later. --- diff --git a/planning/changes/2026-06-13.01-docs-ux-audit/plan.md b/planning/changes/2026-06-13.01-docs-ux-audit/plan.md index f819f80..2be7edd 100644 --- a/planning/changes/2026-06-13.01-docs-ux-audit/plan.md +++ b/planning/changes/2026-06-13.01-docs-ux-audit/plan.md @@ -1,7 +1,7 @@ --- -status: shipped date: 2026-06-13 slug: docs-ux-audit +spec: design.md --- # Plan: Docs UX & Consistency Audit diff --git a/planning/changes/2026-06-13.02-alias-scope-transparency/change.md b/planning/changes/2026-06-13.02-alias-scope-transparency/change.md index b6b37b9..adf5f9d 100644 --- a/planning/changes/2026-06-13.02-alias-scope-transparency/change.md +++ b/planning/changes/2026-06-13.02-alias-scope-transparency/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-13 slug: alias-scope-transparency summary: Deprecate decorative `Alias(scope=...)`; `validate()` checks scope transitively via `effective_scope` (X-4). Plan-only; spec = the code-docs audit report. spec: ../../../audits/2026-06-12-code-docs-audit-report.md -pr: "207" +outcome: validate() now checks scope transitively via effective_scope through aliases (X-4); decorative Alias(scope=) deprecated; enforces_dependency_scope stopgap retired; shipped as 2.17.0 in PR #207. --- # Alias Scope Transparency (X-4) Implementation Plan diff --git a/planning/changes/2026-06-13.02-docs-ux-fixes/design.md b/planning/changes/2026-06-13.02-docs-ux-fixes/design.md index 447ec21..c688637 100644 --- a/planning/changes/2026-06-13.02-docs-ux-fixes/design.md +++ b/planning/changes/2026-06-13.02-docs-ux-fixes/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-13 slug: docs-ux-fixes summary: Fixed all 16 Medium findings from the docs UX audit (runnable examples, accuracy, nav). -supersedes: null -superseded_by: null -pr: 212 outcome: All 16 Medium findings fixed and merged in PR #212 (#11b7b70). mkdocs --strict green; runnable snippets verified; O-5/O-6 confirmed against sibling repos. Architecture pages (scopes.md, providers.md) hand-edited as part of the fixes. --- diff --git a/planning/changes/2026-06-13.02-docs-ux-fixes/plan.md b/planning/changes/2026-06-13.02-docs-ux-fixes/plan.md index 620b8d4..1999fc1 100644 --- a/planning/changes/2026-06-13.02-docs-ux-fixes/plan.md +++ b/planning/changes/2026-06-13.02-docs-ux-fixes/plan.md @@ -1,3 +1,9 @@ +--- +date: 2026-06-13 +slug: docs-ux-fixes +spec: design.md +--- + # Docs UX Fixes Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. diff --git a/planning/changes/2026-06-13.03-portable-planning-convention/design.md b/planning/changes/2026-06-13.03-portable-planning-convention/design.md index 5f5f191..884e4c7 100644 --- a/planning/changes/2026-06-13.03-portable-planning-convention/design.md +++ b/planning/changes/2026-06-13.03-portable-planning-convention/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-13 slug: portable-planning-convention summary: Adopted the two-axis planning convention (architecture/ truth + changes/ bundles) from faststream-outbox. -supersedes: null -superseded_by: null -pr: "210" outcome: Adopted the two-axis convention — architecture/ truth home (README + 6 capability docs) + planning/changes/{active,archive}/ bundles; migrated 11 historical spec/plan pairs into archive bundles; added portable README, _templates/, CLAUDE.md Workflow. Also aligned CLAUDE.md Architecture with the code. --- diff --git a/planning/changes/2026-06-14.01-docs-ux-lows/change.md b/planning/changes/2026-06-14.01-docs-ux-lows/change.md index 429c06a..fefd7bf 100644 --- a/planning/changes/2026-06-14.01-docs-ux-lows/change.md +++ b/planning/changes/2026-06-14.01-docs-ux-lows/change.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-14 slug: docs-ux-lows summary: Fixed all 53 Low findings from the docs UX audit (cross-links, imports, terminology, docstrings). -supersedes: null -superseded_by: null -pr: 213 outcome: All 53 Low findings fixed and merged in PR #213 (b81c23f). mkdocs --strict green, lint clean, 199 tests / 100% coverage. D-10 was already resolved by X-3 (#212); R-2/D-24 already satisfied there. Executed via parallel file-scoped subagents + central verification. --- diff --git a/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/design.md b/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/design.md index 17ad848..94affdf 100644 --- a/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/design.md +++ b/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-14 slug: set-context-cross-scope-staleness summary: Resolve ContextProvider params live so late set_context always propagates across scopes. -supersedes: null -superseded_by: null -pr: 216 outcome: Shipped. ContextProvider values now resolve live on every resolve; invalidate_compiled_kwargs deleted (net simplification). Late set_context propagates across scopes for non-cached factories; cached-singleton limitation diff --git a/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/plan.md b/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/plan.md index 0dc0ab5..3e9f749 100644 --- a/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/plan.md +++ b/planning/changes/2026-06-14.02-set-context-cross-scope-staleness/plan.md @@ -1,9 +1,7 @@ --- -status: shipped date: 2026-06-14 slug: set-context-cross-scope-staleness -spec: set-context-cross-scope-staleness -pr: 216 +spec: design.md --- # set-context-cross-scope-staleness — implementation plan diff --git a/planning/changes/2026-06-14.03-audit-doc-rulings-batch1/change.md b/planning/changes/2026-06-14.03-audit-doc-rulings-batch1/change.md index ce3dfdb..0ac59d6 100644 --- a/planning/changes/2026-06-14.03-audit-doc-rulings-batch1/change.md +++ b/planning/changes/2026-06-14.03-audit-doc-rulings-batch1/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-14 slug: audit-doc-rulings-batch1 summary: Action batch-1 rulings from the 2026-06-14 deep audit (B-4 pin, B-5/S-1/S-2 doc notes, A-1 comment + nogil caveat; A-2 closed). Doc/test/comment-only. Plan-only; spec = the audit report. spec: ../../../audits/2026-06-14-deep-audit-report.md -pr: 217 +outcome: Actioned batch-1 doc/test/comment rulings from 2026-06-14 deep audit (B-4 pin, B-5/S-1/S-2 doc notes, A-1 comment + nogil caveat; A-2 closed with no action); shipped in PR #217. --- # audit-doc-rulings-batch1 — implementation plan diff --git a/planning/changes/2026-06-14.04-audit-fixes-batch2/change.md b/planning/changes/2026-06-14.04-audit-fixes-batch2/change.md index d723d73..0342899 100644 --- a/planning/changes/2026-06-14.04-audit-fixes-batch2/change.md +++ b/planning/changes/2026-06-14.04-audit-fixes-batch2/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-14 slug: audit-fixes-batch2 summary: B-3 (gapped custom-enum child-scope derivation) + P-1 (drop the per-resolve throwaway `CacheItem` alloc via a `get` fast path, keeping atomic `setdefault` on creation). Plan-only; spec = the audit report. spec: ../../../audits/2026-06-14-deep-audit-report.md -pr: 218 +outcome: Fixed B-3 (gapped custom-enum child-scope derivation) and P-1 (eliminate per-resolve throwaway CacheItem alloc via get fast path) from 2026-06-14 deep audit; shipped in PR #218. --- # audit-fixes-batch2 — implementation plan diff --git a/planning/changes/2026-06-14.05-audit-fixes-batch3/change.md b/planning/changes/2026-06-14.05-audit-fixes-batch3/change.md index 775f319..036128b 100644 --- a/planning/changes/2026-06-14.05-audit-fixes-batch3/change.md +++ b/planning/changes/2026-06-14.05-audit-fixes-batch3/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-14 slug: audit-fixes-batch3 summary: R-1 (`AbstractProvider.display_name` dedupes the bound-type-or-repr idiom across ~5 sites) + R-2 minimal (public `fetch_context_value`, drop the `SLF001` reach-in). Plan-only; spec = the audit report. spec: ../../../audits/2026-06-14-deep-audit-report.md -pr: 219 +outcome: Added AbstractProvider.display_name deduping R-1 (~5 sites) and public fetch_context_value dropping the SLF001 reach-in (R-2) from 2026-06-14 deep audit; shipped in PR #219. --- # audit-fixes-batch3 — implementation plan diff --git a/planning/changes/2026-06-14.06-audit-fixes-batch4-5/change.md b/planning/changes/2026-06-14.06-audit-fixes-batch4-5/change.md index 81b8d0d..3c5db22 100644 --- a/planning/changes/2026-06-14.06-audit-fixes-batch4-5/change.md +++ b/planning/changes/2026-06-14.06-audit-fixes-batch4-5/change.md @@ -1,10 +1,9 @@ --- -status: shipped date: 2026-06-14 slug: audit-fixes-batch4-5 summary: Final audit cleanup: test hardening (P-6 compile-once pin, R-3 behavioral singleton assert, X-2 structured suggestion/path asserts) + DX/docs (X-3 exception docstrings, X-4 `exceptions` export, X-5 `ResolutionStep` docs). Closes every actionable finding. Plan-only; spec = audit report. spec: ../../../audits/2026-06-14-deep-audit-report.md -pr: 220 +outcome: Closed every actionable 2026-06-14 audit finding (P-6/R-3/X-2 test hardening; X-3/X-4/X-5 DX/docs); only won't-fix R-4/R-5/R-6 and the A-1 nogil follow-up remain. --- # audit-fixes-batch4-5 — implementation plan diff --git a/planning/changes/2026-06-23.01-wiring-plan-extraction/design.md b/planning/changes/2026-06-23.01-wiring-plan-extraction/design.md index 0183226..330ec8c 100644 --- a/planning/changes/2026-06-23.01-wiring-plan-extraction/design.md +++ b/planning/changes/2026-06-23.01-wiring-plan-extraction/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-23 slug: wiring-plan-extraction summary: Extract a WiringPlan deep module from Factory so the kwarg-binding decision lives in one place, testable without a Container. -supersedes: null -superseded_by: null -pr: 226 outcome: | Shipped. Factory's kwarg-wiring decision moved to a pure WiringPlan module (modern_di/wiring.py): one matcher, one absent-value table (absent_disposition), diff --git a/planning/changes/2026-06-23.01-wiring-plan-extraction/plan.md b/planning/changes/2026-06-23.01-wiring-plan-extraction/plan.md index 22bbddc..32883aa 100644 --- a/planning/changes/2026-06-23.01-wiring-plan-extraction/plan.md +++ b/planning/changes/2026-06-23.01-wiring-plan-extraction/plan.md @@ -1,9 +1,7 @@ --- -status: draft date: 2026-06-23 slug: wiring-plan-extraction -spec: wiring-plan-extraction -pr: null +spec: design.md --- # wiring-plan-extraction — implementation plan diff --git a/planning/changes/2026-06-23.02-inline-error-messages/design.md b/planning/changes/2026-06-23.02-inline-error-messages/design.md index 8b9172d..ff127a6 100644 --- a/planning/changes/2026-06-23.02-inline-error-messages/design.md +++ b/planning/changes/2026-06-23.02-inline-error-messages/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-23 slug: inline-error-messages summary: Inline the single-use error templates into their exception classes and delete the errors.py seam. -supersedes: null -superseded_by: null -pr: 227 outcome: | Shipped. 17 single-use message templates inlined as f-strings into their exception classes; SUGGESTION_HEADER kept as a module constant in exceptions.py; diff --git a/planning/changes/2026-06-23.02-inline-error-messages/plan.md b/planning/changes/2026-06-23.02-inline-error-messages/plan.md index 744eb57..81b1a72 100644 --- a/planning/changes/2026-06-23.02-inline-error-messages/plan.md +++ b/planning/changes/2026-06-23.02-inline-error-messages/plan.md @@ -1,9 +1,7 @@ --- -status: draft date: 2026-06-23 slug: inline-error-messages -spec: inline-error-messages -pr: null +spec: design.md --- # inline-error-messages — implementation plan diff --git a/planning/changes/2026-06-23.03-suggester/design.md b/planning/changes/2026-06-23.03-suggester/design.md index f04e113..f830753 100644 --- a/planning/changes/2026-06-23.03-suggester/design.md +++ b/planning/changes/2026-06-23.03-suggester/design.md @@ -1,11 +1,7 @@ --- -status: shipped date: 2026-06-23 slug: suggester summary: Extract the shared difflib fuzzy-match primitive into a directly-testable suggester module. -supersedes: null -superseded_by: null -pr: 228 outcome: | Shipped. The two difflib.get_close_matches sites (registry type suggestions, factory kwarg "did you mean") now delegate to modern_di/suggester.close_matches; diff --git a/planning/changes/2026-06-23.03-suggester/plan.md b/planning/changes/2026-06-23.03-suggester/plan.md index 949e5f4..3835a39 100644 --- a/planning/changes/2026-06-23.03-suggester/plan.md +++ b/planning/changes/2026-06-23.03-suggester/plan.md @@ -1,9 +1,7 @@ --- -status: draft date: 2026-06-23 slug: suggester -spec: suggester -pr: null +spec: design.md --- # suggester — implementation plan diff --git a/planning/changes/2026-06-25.01-agent-friendly-planning-convention/design.md b/planning/changes/2026-06-25.01-agent-friendly-planning-convention/design.md new file mode 100644 index 0000000..8a8b4ac --- /dev/null +++ b/planning/changes/2026-06-25.01-agent-friendly-planning-convention/design.md @@ -0,0 +1,247 @@ +--- +date: 2026-06-25 +slug: agent-friendly-planning-convention +summary: Make the planning convention agent-friendly — progressive-disclosure restructure (Quick-path on-ramp, deterministic lane decision, de-duplicated text) plus a `just check-planning` validator. +outcome: Shipped an agent-friendly planning convention — a `just check-planning` validator wired into `lint-ci`, a Quick-path on-ramp with a first-match lane decision, and CLAUDE.md de-duplicated to a pointer. Mid-flight the maintainer also dropped `pr` and `status`/supersession from change bundles, making a defined `outcome` the sole required lifecycle field. +--- + +# Design: Make the planning convention agent-friendly + +## Summary + +The portable two-axis planning convention works, but it was written for human +readers and is awkward for an agent to consume: the lane rules are a prose table +the agent must self-apply before it knows the final diff, the convention text is +duplicated across CLAUDE.md and `planning/README.md`, and every invariant +(frontmatter keys, shipped-bundle completeness, no committed scratch) is enforced +only by diligence. This change restructures the convention text into three +explicit progressive-disclosure tiers and adds a machine-checkable validator. It +does **not** alter the two-axis model, the three lanes, frontmatter fields, or +`architecture/`. Scope is the **portable** convention: the changes are written to +be copied verbatim to sibling modern-python repos (rollout there is a separate +per-repo step). + +## Motivation + +A 2026-current best-practice sweep (Anthropic context-engineering guidance, the +SDD tooling convergence around spec-kit/Kiro, the AGENTS.md + agent-skills +progressive-disclosure pattern) all point the same way: orient an agent with a +minimal entry point, push detail behind it, and replace prose diligence with +machine-checkable guardrails. Read against that bar, four concrete frictions +stand out (numbering matches the evaluation that produced this spec): + +1. **No machine-checkable validation.** Frontmatter keys, valid `status`, + shipped bundles having an `outcome`, `plan.md`'s `spec:` resolving, and "no + scratch files committed" are all enforced by hand. The 2026-06-23 retro + records these exact failures (a subagent force-committed + `.superpowers/sdd/*-report.md`; decisions-lane frontmatter churn). `index.py` + already parses frontmatter but never validates it. +2. **The convention is duplicated.** CLAUDE.md `## Workflow` re-describes the + lanes/bundles/decisions that `planning/README.md` "Conventions" already + defines. An agent reads the lane rules twice, and the two copies can drift — + worse across the portable set. +4. **Lane selection is prose criteria, not a procedure.** Choosing a lane is the + single most frequent decision an agent makes, but the rule is a criteria table + it must apply *before* the final LOC/file count exists. Agents follow explicit + first-match decision trees far more reliably than threshold prose. +5. **No cheap on-ramp.** There is no ~15-line "you are here → pick a lane → + create a bundle → ship" path; an agent must read the full ~100-line Conventions + section even for a tiny change. That is the missing top tier of progressive + disclosure. + +(Findings 3, 6, 7, 8 from the evaluation — promotion-enforcement heuristic, +EARS-style acceptance criteria, a consolidated finishing checklist, an `.NN` +helper — are deliberately deferred; see Non-goals.) + +## Non-goals + +- Not changing the two-axis model (`architecture/` truth + `changes/` bundles). +- Not changing the three lanes themselves, their thresholds, or frontmatter + fields — only how the lane choice is *presented*. +- Not touching `architecture/`, `_templates/` content, `audits/`, `retros/`, + `releases/`, or `decisions/` structure. +- Not implementing findings 3/6/7/8 (promotion heuristic, EARS acceptance + criteria, finishing checklist, `.NN` helper). Each can come later. +- Not rolling the changes into sibling repos in this PR — see Operations. + +## Design + +### 1. Three progressive-disclosure tiers (findings 2, 4, 5) + +The convention text is reorganized into three tiers, each loaded only when +needed: + +**Tier 1 — Quick path (the agent on-ramp).** A new section at the *top* of +`planning/README.md`, above the existing "Conventions". ~15 lines. It contains +the lane decision procedure, the minimal create→ship steps, and a pointer down to +the full reference. An agent doing a routine change reads only this; it never +needs the full spec for a tiny/lightweight change. + +The lane decision becomes a deterministic, first-match-wins list (finding 4), +replacing the criteria table as the *primary* presentation: + +``` +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) +2. Purely mechanical: typo · dep bump · linter/formatter/CI tweak · + mechanical rename · single-line config → Tiny (no bundle, commit only) +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) +Ambiguous between two? Take the heavier. A change.md that outgrows its lane +splits into design.md + plan.md. +``` + +Full-triggers-first ordering encodes "heavier wins on ambiguity": any Full +trigger short-circuits before the lighter lanes are considered. + +**Tier 2 — Authoritative reference.** The existing "Conventions" section of +`planning/README.md` remains the single source of truth (bundle anatomy, +frontmatter spec, artifacts-at-a-glance, three-lane detail). **No content moves +out of it.** The existing three-lane table stays here as the detailed reference; +the Quick-path list is the procedural front door to it. + +**Tier 3 — Templates.** `_templates/` is unchanged — loaded only when actually +writing an artifact. + +**De-duplication (finding 2).** CLAUDE.md `## Workflow` currently re-describes +lanes/bundles/decisions. That duplicated prose is deleted and replaced by a +~6-line pointer to the Quick path. CLAUDE.md *keeps* the genuinely repo-specific +release-cutting process (tag-driven publish), which is not in the README. After +this, the lane rules exist in exactly one place. + +### 2. `just check-planning` validator (finding 1) + +Extend `planning/index.py` with a `--check` mode that reuses its existing +`parse_frontmatter` / `load_bundles` / `load_decisions`, exposed as a +`just check-planning` recipe and wired into `just lint-ci`. It collects all +violations, prints them, and exits non-zero if any. Keeping it inside `index.py` +holds the portable surface to a single script. + +Checked invariants (all portable): + +- **Bundle shape:** each bundle dir has a `design.md` or `change.md`; the dir + contains **only** known artifacts (`design.md`, `plan.md`, `change.md`) — + catches the retro's committed-scratch footgun. +- **Frontmatter completeness:** required keys present per artifact type — + `design.md`/`change.md`: `date`, `slug`, `summary`, `outcome`; `plan.md`: + `date`, `slug`, `spec`; `decisions/*.md`: `status`, `date`, `slug`, `summary`. +- **Field validity:** a decision's `status` ∈ {`accepted`, `superseded`}; `date` + matches `YYYY-MM-DD`; the bundle dir name matches `YYYY-MM-DD.NN-slug` and its + `slug` field agrees with the dir slug. +- **Link integrity:** `plan.md`'s `spec:` path resolves to an existing file + (relative to the bundle dir). + +(The field set above is the post-`status`/`pr` state — see the Updates section +below for how it got here.) + +The validator reports *all* violations in one run (not fail-fast) so an agent or +human fixes the whole set at once. + +### 3. Bootstrapping note + +Running `--check` against the current repo may surface pre-existing violations in +historical bundles (e.g. a missing `outcome`, an unexpected file). Part of this +change is bringing the existing bundles to green — either by fixing the +frontmatter or, where a historical bundle is legitimately irregular, by making +the check's rule precise enough not to flag it. The executor records any such +fixes; the gate ships only once `just check-planning` is clean. + +## Updates (2026-06-25, during execution) + +### `pr` dropped from the convention + +The original plan required `status: shipped ⇒ pr + outcome` and backfilled `pr` +across 15 historical bundles. Doing that backfill exposed why `pr` is the wrong +field to enforce: it is the only frontmatter value that is hand-supplied, +knowable only *after* the PR exists (so it is always a second edit and blocks +marking a bundle shipped until then), **and** already recoverable from git (the +merge commit that shipped the bundle). `outcome` has none of those problems — it +is the irreplaceable, human-authored "what resulted", reconstructable from +nothing else. + +So `pr` is dropped from the convention entirely: + +- The validator enforces `status: shipped ⇒ outcome` only. +- `pr` is removed from the three `_templates/` frontmatter blocks, from the + `README.md` frontmatter spec, and from the `format_row` index rendering (the + listing now shows `(date)`, not `(#pr, date)`). PR traceability lives in git + history and the `outcome` line. +- The `pr:` line is stripped from every existing bundle and decision + frontmatter, reverting the now-pointless backfill (the `outcome` backfill + stays — it was needed regardless). + +This supersedes the "backfill all 15" ruling for `pr`; the equivalent work for +`outcome` stands. + +Because `outcome` is now the sole enforced lifecycle field, it gets a written +definition in `README.md`'s Frontmatter section (and a guiding placeholder in +the `_templates/`): filled at ship time, one line / ~1–3 sentences (≤ ~300 +chars), stating the realized result — what shipped and its effect, deviations +from the plan included — written so a future reader grasps the consequence +without opening the diff, and distinct from the pre-ship intent in `summary`. + +### `status` and supersession dropped from change bundles + +On `main`, a change bundle is essentially always `shipped` — the in-progress +states (`draft`/`approved`) only exist on an unmerged feature branch, and +"superseded" was derivable from `superseded_by`. So the `draft → shipped` flip +was ceremony with no payoff, and supersession was judged not useful for changes. +Both are removed from change bundles: + +- `status`, `supersedes`, and `superseded_by` are removed from the change-bundle + frontmatter spec, the `_templates/` (design/change/plan), and every existing + change bundle. A change spec's frontmatter is now `date`, `slug`, `summary`, + `outcome`; a `plan.md`'s is `date`, `slug`, `spec`. +- The validator requires `outcome` on **every** change spec (no longer gated on + `status: shipped`); it drops the `status`-validity and supersession checks for + changes. +- The generated index drops the status grouping: changes render as a flat + newest-first list. `format_row` is unchanged (the supersession suffixes simply + never fire for changes). +- **Decisions are unchanged** — `decisions/*.md` keep `status` + (`accepted`/`superseded`) and `supersedes`/`superseded_by`, where the + distinction is load-bearing. The validator still checks them. + +## Operations + +Sibling modern-python repos (e.g. `faststream-outbox`) share the portable +convention. After this lands here, the Quick-path section and the `index.py +--check` additions are copied to each sibling and wired into their `just lint-ci`. +That rollout is per-repo and out of scope for this PR; it is tracked as a +follow-up (a `deferred.md` entry or a decision note, executor's choice). + +## Out of scope + +Findings 3 (architecture/-promotion heuristic warning), 6 (EARS-style per-task +acceptance criteria), 7 (consolidated finishing-a-change checklist), and 8 (`.NN` +counter helper) are explicitly excluded. They were considered and ruled out for +this change; 6 in particular risks reading as ceremony and needs separate +agreement. + +## Testing + +- `just check-planning` exits 0 on the cleaned repo and non-zero with a clear, + itemized message when fed a deliberately broken bundle (validated with a + throwaway fixture during execution, not committed). +- `just lint-ci` runs `check-planning` as part of the suite and stays green. +- `just index` still renders the listing unchanged (the `--check` addition must + not alter default/stdout behavior). +- A human read-through confirms the lane decision in the Quick path yields the + same lane as the existing table on a handful of past changes (no semantic + drift in the lane rules — only their presentation). + +## Risk + +- **Validator too strict, flags legitimate historical bundles.** *Mitigation:* + the bootstrapping step (Design §3) reconciles existing bundles before the gate + is wired into `lint-ci`; rules are tightened to be precise, not blanket. + Likelihood medium, impact low (noisy CI, not broken code). +- **De-dup drops a CLAUDE.md detail that was only there.** *Mitigation:* diff + the removed prose against `planning/README.md` "Conventions" before deleting; + anything not already covered there stays in CLAUDE.md (this is how the + release-cutting process is retained). Likelihood low, impact low. +- **Portable drift after copy-out.** The Quick path and `--check` now exist in + multiple repos and can diverge. *Mitigation:* same as today — the portable + section is copied, not abstracted; the Operations follow-up notes the sync + points. Likelihood low, impact low. diff --git a/planning/changes/2026-06-25.01-agent-friendly-planning-convention/plan.md b/planning/changes/2026-06-25.01-agent-friendly-planning-convention/plan.md new file mode 100644 index 0000000..0000f77 --- /dev/null +++ b/planning/changes/2026-06-25.01-agent-friendly-planning-convention/plan.md @@ -0,0 +1,643 @@ +--- +date: 2026-06-25 +slug: agent-friendly-planning-convention +spec: design.md +--- + +# agent-friendly-planning-convention — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps use +> checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the portable planning convention agent-friendly — add a +`just check-planning` validator, bring all existing bundles to green, and +restructure the convention text into a Quick-path on-ramp + de-duplicated +pointer. + +**Architecture:** Extend `planning/index.py` with a `--check` mode (reusing its +`parse_frontmatter`) exposed as `just check-planning` and wired into +`just lint-ci`. Add a Quick-path section atop `planning/README.md` (the agent +on-ramp, with a deterministic first-match lane decision) and shrink the +duplicated CLAUDE.md `## Workflow` prose to a pointer. Backfill frontmatter on +15 historical bundles and standardize `plan.md`'s `spec:` to a bundle-relative +path. + +**Tech Stack:** Python 3.10+ stdlib only (`pathlib`, `re`, `sys`); `just`; `uv`. + +**Spec:** [`design.md`](./design.md) + +**Branch:** `feat/agent-friendly-planning-convention` (already created). + +**Commit strategy:** Per-task commits. + +## Global Constraints + +- **Line length: 120 chars.** `ruff` runs with `select = ["ALL"]`; `ty` for + types. `just lint` must pass clean. +- **Do NOT add a committed test that imports `planning/index.py`.** It is + currently never imported, so the 100% coverage gate (`--cov=.`) does not + measure it. Importing it from `tests/` would pull `render`/`load_*`/`main` + into the coverage set and break the gate. The validator is verified via the + **CLI only** (`just check-planning`) against throwaway fixtures and the real + repo. Throwaway fixtures are removed before committing — never committed. +- **`spec:` is a bundle-relative path** (Fork 1 ruling): always resolvable from + the bundle directory (e.g. `design.md`, `../../../audits/x.md`). +- **`outcome` is required on every change spec.** Two fields were dropped from + change bundles mid-execution (see the spec's "Updates (2026-06-25)" section): + `pr` (not a field, not enforced, not rendered) and `status` + + `supersedes`/`superseded_by` (so there is no `shipped` gate — `outcome` is just + always required, and the index is a flat newest-first list). A change spec's + frontmatter is `date`/`slug`/`summary`/`outcome`; a `plan.md`'s is + `date`/`slug`/`spec`. **Decisions keep `status` + supersession.** The Task 2 + `pr` backfill is reverted; the `outcome` backfill stands. (Task bodies below + still describe the original `pr`/`status` workflow — they are the historical + execution record; this constraint is the authoritative correction.) +- This change does **not** promote to `architecture/` — it is process/tooling, + like the original `portable-planning-convention` adoption. Its "promotion" is + the README + CLAUDE.md edits it already makes. +- `just check-planning` must report **all** violations in one run (not + fail-fast). + +--- + +### Task 1: Add the `--check` validator and `just check-planning` recipe + +**Files:** +- Modify: `planning/index.py` (add `import re`, validation constants/functions, + `--check` branch in `main`) +- Modify: `justfile` (add `check-planning` recipe) + +**Interfaces:** +- Produces: `check() -> list[str]` (returns violation strings; reused by `main`); + CLI `python planning/index.py --check` (exit 1 + itemized stderr on + violations, exit 0 + `planning: OK` otherwise). `just check-planning` wraps it. +- Consumes: existing `parse_frontmatter`, `CHANGES_DIR`, `DECISIONS_DIR`. + +- [ ] **Step 1: Add `import re` to the imports block** + + In `planning/index.py`, the current imports are: + + ```python + import pathlib + import sys + ``` + + Change to: + + ```python + import pathlib + import re + import sys + ``` + +- [ ] **Step 2: Add validation constants** after the existing `GROUPS = (...)` + tuple: + + ```python + VALID_STATUS = {"draft", "approved", "shipped", "superseded"} + VALID_DECISION_STATUS = {"accepted", "superseded"} + DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") + BUNDLE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\.\d{2}-(?P.+)$") + ALLOWED_BUNDLE_FILES = {"design.md", "plan.md", "change.md"} + SPEC_REQUIRED = ("status", "date", "slug", "summary") + PLAN_REQUIRED = ("status", "date", "slug", "spec") + DECISION_REQUIRED = ("status", "date", "slug", "summary") + ``` + +- [ ] **Step 3: Add the validation functions** just above `def main()`: + + ```python + def _require(fields: dict[str, str], keys: tuple[str, ...], rel: str, violations: list[str]) -> None: + """Append a violation for each required key that is absent or empty.""" + for key in keys: + if not fields.get(key): + violations.append(f"{rel}: missing or empty frontmatter key '{key}'") + + + def _check_common( + fields: dict[str, str], allowed_status: set[str], dir_slug: str | None, rel: str, violations: list[str] + ) -> None: + """Validate status/date/slug fields shared by every artifact type.""" + status = fields.get("status", "") + if status and status not in allowed_status: + violations.append(f"{rel}: invalid status '{status}' (allowed: {', '.join(sorted(allowed_status))})") + date = fields.get("date", "") + if date and not DATE_RE.match(date): + violations.append(f"{rel}: date '{date}' is not YYYY-MM-DD") + slug = fields.get("slug", "") + if dir_slug and slug and slug != dir_slug: + violations.append(f"{rel}: slug '{slug}' does not match directory slug '{dir_slug}'") + + + def _check_spec_file(path: pathlib.Path, rel: str, dir_slug: str | None, violations: list[str]) -> None: + """Validate a design.md / change.md spec file.""" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, SPEC_REQUIRED, rel, violations) + _check_common(fields, VALID_STATUS, dir_slug, rel, violations) + if fields.get("status") == "shipped": + for key in ("pr", "outcome"): + if not fields.get(key): + violations.append(f"{rel}: status is 'shipped' but '{key}' is empty") + + + def _check_plan_file( + path: pathlib.Path, bundle: pathlib.Path, rel: str, dir_slug: str | None, violations: list[str] + ) -> None: + """Validate a plan.md file, including that its spec: link resolves.""" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, PLAN_REQUIRED, rel, violations) + _check_common(fields, VALID_STATUS, dir_slug, rel, violations) + spec = fields.get("spec", "") + if spec and not (bundle / spec).resolve().exists(): + violations.append(f"{rel}: spec link '{spec}' does not resolve to a file") + + + def _check_bundle(bundle: pathlib.Path, violations: list[str]) -> None: + """Validate one change bundle directory.""" + rel = f"changes/{bundle.name}" + match = BUNDLE_RE.match(bundle.name) + dir_slug = match.group("slug") if match else None + if match is None: + violations.append(f"{rel}: directory name is not 'YYYY-MM-DD.NN-slug'") + for child in sorted(bundle.iterdir()): + if child.name not in ALLOWED_BUNDLE_FILES: + violations.append( + f"{rel}/{child.name}: unexpected file in bundle (allowed: {', '.join(sorted(ALLOWED_BUNDLE_FILES))})" + ) + design = bundle / "design.md" + change = bundle / "change.md" + plan = bundle / "plan.md" + if not design.exists() and not change.exists(): + violations.append(f"{rel}: bundle has neither design.md nor change.md") + for spec_file in (design, change): + if spec_file.exists(): + _check_spec_file(spec_file, f"{rel}/{spec_file.name}", dir_slug, violations) + if plan.exists(): + _check_plan_file(plan, bundle, f"{rel}/plan.md", dir_slug, violations) + + + def _check_decision(path: pathlib.Path, violations: list[str]) -> None: + """Validate one decision file.""" + rel = f"decisions/{path.name}" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, DECISION_REQUIRED, rel, violations) + _check_common(fields, VALID_DECISION_STATUS, None, rel, violations) + + + def check() -> list[str]: + """Validate every bundle and decision; return the list of violation strings.""" + violations: list[str] = [] + for bundle in sorted(CHANGES_DIR.iterdir()): + if bundle.is_dir(): + _check_bundle(bundle, violations) + if DECISIONS_DIR.is_dir(): + for path in sorted(DECISIONS_DIR.glob("*.md")): + if path.name == "README.md" or path.name.startswith("_"): + continue + _check_decision(path, violations) + return violations + ``` + +- [ ] **Step 4: Add the `--check` branch to `main`** + + Current: + + ```python + def main() -> int: + """Print the listing to stdout.""" + sys.stdout.write(render(load_bundles(), load_decisions())) + return 0 + ``` + + Replace with: + + ```python + def main() -> int: + """Print the listing to stdout, or validate bundles with --check.""" + if "--check" in sys.argv[1:]: + violations = check() + if violations: + sys.stderr.write(f"planning: {len(violations)} violation(s)\n") + for violation in violations: + sys.stderr.write(f" - {violation}\n") + return 1 + sys.stdout.write("planning: OK\n") + return 0 + sys.stdout.write(render(load_bundles(), load_decisions())) + return 0 + ``` + +- [ ] **Step 5: Add the `check-planning` recipe to `justfile`** + + After the existing `index:` recipe (the last recipe in the file), add: + + ```just + # Validate planning bundles + decisions (frontmatter, lanes, spec links); CI runs this. + check-planning: + uv run python planning/index.py --check + ``` + +- [ ] **Step 6: Verify the index still renders unchanged** + + Run: `just index` + Expected: the same Markdown listing as before (changes by status, then + decisions). The `--check` addition must not alter default stdout. + +- [ ] **Step 7: Prove detection with a throwaway fixture (NOT committed)** + + Create a deliberately-broken throwaway bundle: + + ```bash + mkdir -p planning/changes/2099-01-01.99-zzz-throwaway-fixture + printf -- '---\nstatus: bogus\ndate: not-a-date\nslug: wrong-slug\n---\n# x\n' \ + > planning/changes/2099-01-01.99-zzz-throwaway-fixture/design.md + ``` + + Run: `just check-planning` + Expected: exit 1; output includes lines for the fixture — + `invalid status 'bogus'`, `date 'not-a-date' is not YYYY-MM-DD`, + `slug 'wrong-slug' does not match directory slug 'zzz-throwaway-fixture'`, and + `missing or empty frontmatter key 'summary'`. + + Then remove it: + + ```bash + rm -rf planning/changes/2099-01-01.99-zzz-throwaway-fixture + ``` + +- [ ] **Step 8: Confirm the validator runs against the real repo** + + Run: `just check-planning` + Expected: exit 1 with ~24 violations in historical bundles (these are fixed in + Task 2). This confirms the validator works end-to-end. Do **not** wire it into + `lint-ci` yet — that happens after Task 2 turns the repo green. + +- [ ] **Step 9: Lint and commit** + + Run: `just lint` — must pass clean (address any ruff/ty findings inline). + Then: + + ```bash + git add planning/index.py justfile + git commit -m "feat(planning): add just check-planning bundle validator + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 2: Bring all historical bundles to green, then wire into lint-ci + +**Files:** +- Modify (backfill `pr`): the 8 `design.md` listed below. +- Modify (backfill `outcome`): the 7 `change.md` listed below. +- Modify (`spec:` → path): 6 `plan.md` + `planning/_templates/plan.md`. +- Modify: `justfile` (`lint-ci` recipe). + +**Interfaces:** +- Consumes: `just check-planning` from Task 1 as the gate. +- Produces: a repo where `just check-planning` exits 0. + +- [ ] **Step 1: Backfill `pr:` on the 8 shipped `design.md` bundles** + + Set the `pr:` frontmatter field (currently `null`) in each, using this mapping + (derived from `planning/releases/*.md` + merge commits): + + | bundle `design.md` | `pr:` | + |---|---| + | `2026-06-05.01-bug-hunt-audit` | `188-197` | + | `2026-06-05.02-singleton-rlock` | `188` | + | `2026-06-05.03-validate-rework` | `189` | + | `2026-06-07.01-mkdocs-github-pages-migration` | `198` | + | `2026-06-08.01-scheduled-dep-check` | `200` | + | `2026-06-09.01-docs-improvements` | `198` | + | `2026-06-09.02-migration-guide-from-that-depends` | `198` | + | `2026-06-12.01-code-docs-audit` | `202-203` | + + **Uncertain ones — verify first:** `docs-improvements` and + `migration-guide-from-that-depends` have no dedicated merged PR (their docs + shipped with the GitHub-Pages migration). Confirm with + `gh pr list --state merged --search ""` or + `git log --oneline --all -- `; if a dedicated PR exists, use it, + otherwise keep `198` (the docs-migration PR they rode in with). Note the + choice in the commit message. + + Edit example (`bug-hunt-audit`): change `pr: null` → `pr: "188-197"` (quote + range values so the index renders `#188-197`). + +- [ ] **Step 2: Backfill `outcome:` on the 7 shipped `change.md` bundles** + + Each already has `pr:`; only `outcome:` is missing. Write a one-line + `outcome:` for each, condensed from that bundle's own `summary:` / body (the + result is already described in-file — do not invent new facts): + + - `2026-06-12.02-audit-fixes` + - `2026-06-13.01-audit-fixes-round2` + - `2026-06-13.02-alias-scope-transparency` + - `2026-06-14.03-audit-doc-rulings-batch1` + - `2026-06-14.04-audit-fixes-batch2` + - `2026-06-14.05-audit-fixes-batch3` + - `2026-06-14.06-audit-fixes-batch4-5` + + Worked example (`audit-fixes-batch4-5`, whose summary already states the + result) — add under the `pr: 220` line: + + ```yaml + outcome: Closed every actionable 2026-06-14 audit finding (P-6/R-3/X-2 test hardening; X-3/X-4/X-5 DX/docs); only won't-fix R-4/R-5/R-6 and the A-1 nogil follow-up remain. + ``` + +- [ ] **Step 3: Standardize `plan.md` `spec:` to a bundle-relative path** + + Set `spec: design.md` in each of these `plan.md` files (each has a sibling + `design.md`, so it resolves): + + - `2026-06-13.01-docs-ux-audit/plan.md` — currently missing `spec:` + - `2026-06-14.02-set-context-cross-scope-staleness/plan.md` — bare slug + - `2026-06-23.01-wiring-plan-extraction/plan.md` — bare slug + - `2026-06-23.02-inline-error-messages/plan.md` — bare slug + - `2026-06-23.03-suggester/plan.md` — bare slug + + For `2026-06-13.02-docs-ux-fixes/plan.md` (missing `status`/`date`/`slug`/ + `spec`), set the full frontmatter, mirroring its sibling `design.md`'s + `status`/`pr` and keeping the existing body: + + ```yaml + --- + status: shipped + date: 2026-06-13 + slug: docs-ux-fixes + spec: design.md + pr: + --- + ``` + + (Read `2026-06-13.02-docs-ux-fixes/design.md` to copy its exact `status` and + `pr`.) + +- [ ] **Step 4: Fix the template so new plans inherit the path convention** + + In `planning/_templates/plan.md` frontmatter, change `spec: my-change` to + `spec: design.md`. + +- [ ] **Step 5: Run the gate to green** + + Run: `just check-planning` + Expected: exit 0, `planning: OK`. If any violation remains, fix it and re-run + (the validator lists all violations each run). + +- [ ] **Step 6: Wire `check-planning` into `lint-ci`** + + Current `lint-ci` recipe: + + ```just + lint-ci: + uv run eof-fixer . --check + uv run ruff format --check + uv run ruff check --no-fix + uv run ty check + ``` + + Add the check as a final line: + + ```just + lint-ci: + uv run eof-fixer . --check + uv run ruff format --check + uv run ruff check --no-fix + uv run ty check + uv run python planning/index.py --check + ``` + +- [ ] **Step 7: Verify lint-ci is green and commit** + + Run: `just lint-ci` + Expected: all checks pass, including `planning: OK`. + + ```bash + git add planning/changes planning/_templates/plan.md justfile + git commit -m "fix(planning): backfill historical bundle frontmatter; gate in lint-ci + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 3: Add the Quick-path on-ramp to planning/README.md + +**Files:** +- Modify: `planning/README.md` (insert a new section between the intro + paragraph and `## Conventions`) + +- [ ] **Step 1: Insert the Quick-path section** + + In `planning/README.md`, immediately after the intro paragraph (the line + ending "...records *how it got there*.") and before `## Conventions`, insert: + + ```markdown + ## Quick path (start here) + + > The fast lane for making a change. The full reference is in + > [Conventions](#conventions) below — read it only when this isn't enough. + + **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`) + 2. Purely mechanical: typo · dep bump · linter/formatter/CI tweak · + mechanical rename · single-line config → **Tiny** (no bundle, 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`) + + Ambiguous between two? Take the heavier. A `change.md` that outgrows its lane + splits into `design.md` + `plan.md`. + + **2. Create the bundle** (Full / Lightweight only): + `planning/changes/YYYY-MM-DD.NN-/`, where `.NN` is a zero-padded + intra-day counter. Copy the matching template from + [`_templates/`](_templates/). + + **3. Ship in the implementing PR:** hand-edit the affected + `architecture/.md`, set `status: shipped` + `pr:` + `outcome:` in + the bundle frontmatter, and run `just check-planning` before pushing. + ``` + +- [ ] **Step 2: Verify the gate still passes (README is not a bundle, but confirm nothing regressed)** + + Run: `just check-planning` + Expected: exit 0, `planning: OK`. + +- [ ] **Step 3: Verify the docs build is unaffected** + + Run: `just docs-build` + Expected: build succeeds. (`docs_dir: docs` excludes `planning/`, so this only + confirms no collateral breakage.) + +- [ ] **Step 4: Commit** + + ```bash + git add planning/README.md + git commit -m "docs(planning): add Quick-path on-ramp with first-match lane decision + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 4: De-duplicate CLAUDE.md `## Workflow` down to a pointer + +**Files:** +- Modify: `CLAUDE.md` (`## Workflow` section, currently the intro + 6 bullets) + +- [ ] **Step 1: Replace the duplicated Workflow prose with a pointer** + + In `CLAUDE.md`, replace the entire `## Workflow` body — from the + "Planning follows a portable two-axis convention..." intro through the + `- **Templates live in...**` and `- **Shipping a change**...` bullets, but + **keeping** the `- **Cutting a release (maintainers)**...` bullet verbatim — + with: + + ```markdown + ## Workflow + + Planning uses a portable two-axis 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` to choose a lane, create a bundle, and ship — that file + is the authoritative spec. Run `just check-planning` to validate bundles and + `just index` to print the change listing. The `## Architecture` section above + is quick orientation; `architecture/` holds the authoritative account. + + - **Cutting a release (maintainers)** is tag-driven via + [`.github/workflows/release.yml`](.github/workflows/release.yml): write the + notes at `planning/releases/.md` (used verbatim as the GitHub Release + body), then push a bare semver tag off green `main` — + `git tag 2.19.2 && git push origin 2.19.2`. The workflow runs `just publish` + (the tag sets the version via `uv version`; no `pyproject.toml` bump) to PyPI, + then creates the GitHub Release — PyPI first, so a failed publish creates no + Release. Pre-releases use the PEP 440 form (`2.0.0rc1`, not `2.0.0-alpha.5`). + PyPI is irreversible; there is no CI gate (a tag is the commitment point). + ``` + +- [ ] **Step 2: Confirm no unique content was lost** + + Read the removed bullets against `planning/README.md` "Conventions". Confirm + every removed fact (architecture/ truth home, bundle layout, decisions lane, + templates, shipping/promotion) is present there. The only repo-specific bullet + — release-cutting — is retained above. If anything removed is NOT in the + README, restore it to CLAUDE.md. + +- [ ] **Step 3: Commit** + + ```bash + git add CLAUDE.md + git commit -m "docs: de-duplicate CLAUDE.md Workflow to a pointer at the Quick path + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 5: Record the sibling-repo rollout and finish the branch + +**Files:** +- Modify: `planning/deferred.md` (append the rollout follow-up) +- Modify: `planning/changes/2026-06-25.01-agent-friendly-planning-convention/design.md` + and `plan.md` (ship frontmatter, once the PR number is known) + +- [ ] **Step 1: Append the sibling-rollout follow-up to `deferred.md`** + + Add at the end of `planning/deferred.md`: + + ```markdown + ## Roll the agent-friendly planning updates into sibling repos — from 2026-06-25 + + The Quick-path on-ramp (`planning/README.md`) and the `index.py --check` + validator (`just check-planning`, wired into `just lint-ci`) shipped here in + [2026-06-25.01-agent-friendly-planning-convention](changes/2026-06-25.01-agent-friendly-planning-convention/design.md). + They are written to be copied verbatim. Sibling modern-python repos (e.g. + `faststream-outbox`) still carry the older prose-table convention and the + `spec: ` plan template. + + **Revisit trigger:** next time a sibling repo's planning convention is touched, + or in a dedicated sync pass — copy the Quick-path section and the `--check` + additions, switch that repo's `_templates/plan.md` to `spec: design.md`, and + wire `check-planning` into its `just lint-ci`. + ``` + +- [ ] **Step 2: Full verification** + + Run: `just lint-ci` — all checks pass including `planning: OK`. + Run: `just test-ci` — full suite green, 100% coverage (unchanged; no test + imports `index.py`). + Run: `just docs-build` — succeeds. + +- [ ] **Step 3: Commit the follow-up** + + ```bash + git add planning/deferred.md + git commit -m "docs(planning): defer sibling-repo rollout of the agent-friendly updates + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +- [ ] **Step 4: Push and open the PR** + + ```bash + git push -u origin feat/agent-friendly-planning-convention + gh pr create --fill + ``` + +- [ ] **Step 5: Ship this bundle's frontmatter (in-branch, after the PR number is known)** + + Per the convention, set this bundle's lifecycle fields in the implementing PR. + In both `design.md` and `plan.md` frontmatter set `status: shipped`; in + `design.md` also fill `outcome:` with a one-line result. (`pr` is no longer a + field — see Global Constraints.) Then: + + ```bash + git add planning/changes/2026-06-25.01-agent-friendly-planning-convention + git commit -m "docs(planning): mark agent-friendly-planning-convention shipped + + Co-Authored-By: Claude Opus 4.8 (1M context) " + git push + ``` + + Run `just check-planning` once more before this push — with `status: shipped` + the bundle now requires `outcome`, so the gate confirms it is set. + This change does **not** edit `architecture/` (process/tooling; see Global + Constraints). Watch PR CI to green. + +--- + +## Self-Review + +**Spec coverage:** +- Design §1 (three tiers: Quick path / Conventions / templates) → Task 3 + (Quick-path tier 1), Task 4 (de-dup → pointer); Conventions + `_templates/` + left intact per design. ✓ +- Design §1 lane decision procedure (finding 4) → Task 3 Step 1. ✓ +- Design §2 (`--check` validator + recipe + lint-ci wiring) → Task 1 + Task 2 + Step 6. ✓ All five invariant classes (bundle shape, frontmatter completeness, + field validity, lifecycle completeness, link integrity) are in Task 1 Step 3. ✓ +- Design §3 (bootstrapping existing bundles to green) → Task 2 Steps 1–5. ✓ +- Operations (sibling rollout follow-up) → Task 5 Step 1. ✓ +- Fork 1 ruling (spec = path; fix template) → Task 2 Steps 3–4. ✓ +- Fork 2 ruling (backfill all 15) → Task 2 Steps 1–2. ✓ + +**Placeholder scan:** No "TBD"/"handle edge cases". The only deliberately +deferred values are the two genuinely-PR-less docs bundles (Task 2 Step 1), +which name the exact command to resolve them and a documented fallback — not a +placeholder. The `outcome:` backfills name their source (each bundle's own +summary) with a worked example. + +**Type consistency:** `check()`, `parse_frontmatter`, `_check_*` signatures match +between Task 1 Step 3 (definitions) and Step 4 (`main` calls `check()`). The +`--check` CLI contract used in Task 1 Steps 7–8 and Task 2 Step 5 matches the +`main` implementation. `spec: design.md` is used consistently across Task 2 +Steps 3–4 and the validator's resolution check. diff --git a/planning/decisions/2026-06-23-sync-async-close-separate.md b/planning/decisions/2026-06-23-sync-async-close-separate.md index 309deac..79483d0 100644 --- a/planning/decisions/2026-06-23-sync-async-close-separate.md +++ b/planning/decisions/2026-06-23-sync-async-close-separate.md @@ -5,7 +5,6 @@ slug: sync-async-close-separate summary: Keep sync/async close paths separate — the divergence is intrinsic, not duplication. supersedes: null superseded_by: null -pr: 229 --- # Keep sync and async `close` paths separate diff --git a/planning/deferred.md b/planning/deferred.md index b483aa6..07e784d 100644 --- a/planning/deferred.md +++ b/planning/deferred.md @@ -19,3 +19,26 @@ replaced the old set-`kwargs_compiled`-after-the-bucket-fields sequence — but options: build and publish `wiring_plan` under the existing container lock, or publish the reference behind an explicit barrier/atomic. Until then, document modern-di as GIL-assuming for plan compilation. See [2026-06-14 audit A-1](audits/2026-06-14-deep-audit-report.md). + +## Roll the agent-friendly planning updates into sibling repos — from 2026-06-25 + +This branch reshaped the portable convention; sibling modern-python repos (e.g. +`faststream-outbox`) still carry the older form. Copy these over and wire them in: + +- **Quick-path on-ramp** atop `planning/README.md` (first-match lane decision + + create/ship steps). +- **`just check-planning`** validator (`planning/index.py --check`), wired into + `just lint-ci`. +- **`spec:` is a bundle-relative path** — switch `_templates/plan.md` from + `spec: ` to `spec: design.md`. +- **`pr` dropped from the convention** — remove it from `_templates/`, the + README frontmatter spec, `format_row` rendering, the validator, and existing + bundle/decision frontmatter. +- **`outcome` definition** in the README Frontmatter section + a template + placeholder. + +Shipped here in +[2026-06-25.01-agent-friendly-planning-convention](changes/2026-06-25.01-agent-friendly-planning-convention/design.md). + +**Revisit trigger:** next time a sibling repo's planning convention is touched, +or in a dedicated sync pass. diff --git a/planning/index.py b/planning/index.py index 8e89d0f..ec6c2dc 100644 --- a/planning/index.py +++ b/planning/index.py @@ -5,21 +5,24 @@ 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: +then decisions, newest-first. Never writes a file: the listing is a query over the files, not a committed artifact. """ import pathlib +import re 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",)), - ("Superseded", ("superseded",)), -) +VALID_DECISION_STATUS = {"accepted", "superseded"} +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +BUNDLE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\.\d{2}-(?P.+)$") +ALLOWED_BUNDLE_FILES = {"design.md", "plan.md", "change.md"} +SPEC_REQUIRED = ("date", "slug", "summary", "outcome") +PLAN_REQUIRED = ("date", "slug", "spec") +DECISION_REQUIRED = ("status", "date", "slug", "summary") def parse_frontmatter(text: str) -> dict[str, str]: @@ -31,6 +34,8 @@ def parse_frontmatter(text: str) -> dict[str, str]: for line in lines[1:]: if line.strip() == "---": break + if line[:1] in (" ", "\t"): + continue key, sep, value = line.partition(": ") if not sep: continue @@ -76,10 +81,9 @@ def format_row(bundle: dict[str, str]) -> str: """Render one bundle as a Markdown list item.""" slug = bundle.get("slug", "?") path = bundle.get("path", "") - pr = bundle.get("pr") or "—" date = bundle.get("date", "") summary = bundle.get("summary") or "(no summary)" - line = f"- **[{slug}]({path})** (#{pr}, {date}) — {summary}" + line = f"- **[{slug}]({path})** ({date}) — {summary}" if bundle.get("supersedes"): line += f" _(supersedes {bundle['supersedes']})_" if bundle.get("superseded_by"): @@ -88,26 +92,111 @@ def format_row(bundle: dict[str, str]) -> str: def render(bundles: list[dict[str, str]], decisions: list[dict[str, str]]) -> str: - """Render the full Markdown listing: changes by status, then decisions.""" + """Render the full Markdown listing: changes then decisions, newest-first.""" out = ["# Planning index", "", "_Generated by `just index` — do not edit._", "", "## Changes", ""] - for title, statuses in GROUPS: - 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", ""] + change_rows = sorted(bundles, key=lambda b: b.get("name", ""), reverse=True) + out += [format_row(b) for b in change_rows] if change_rows else ["_None._"] + 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 _require(fields: dict[str, str], keys: tuple[str, ...], rel: str, violations: list[str]) -> None: + """Append a violation for each required key that is absent or empty.""" + violations.extend(f"{rel}: missing or empty frontmatter key '{key}'" for key in keys if not fields.get(key)) + + +def _check_common(fields: dict[str, str], dir_slug: str | None, rel: str, violations: list[str]) -> None: + """Validate date/slug fields shared by every artifact type.""" + date = fields.get("date", "") + if date and not DATE_RE.match(date): + violations.append(f"{rel}: date '{date}' is not YYYY-MM-DD") + slug = fields.get("slug", "") + if dir_slug and slug and slug != dir_slug: + violations.append(f"{rel}: slug '{slug}' does not match directory slug '{dir_slug}'") + + +def _check_spec_file(path: pathlib.Path, rel: str, dir_slug: str | None, violations: list[str]) -> None: + """Validate a design.md / change.md spec file.""" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, SPEC_REQUIRED, rel, violations) + _check_common(fields, dir_slug, rel, violations) + + +def _check_plan_file( + path: pathlib.Path, bundle: pathlib.Path, rel: str, dir_slug: str | None, violations: list[str] +) -> None: + """Validate a plan.md file, including that its spec: link resolves.""" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, PLAN_REQUIRED, rel, violations) + _check_common(fields, dir_slug, rel, violations) + spec = fields.get("spec", "") + if spec and not (bundle / spec).resolve().exists(): + violations.append(f"{rel}: spec link '{spec}' does not resolve to a file") + + +def _check_bundle(bundle: pathlib.Path, violations: list[str]) -> None: + """Validate one change bundle directory.""" + rel = f"changes/{bundle.name}" + match = BUNDLE_RE.match(bundle.name) + dir_slug = match.group("slug") if match else None + if match is None: + violations.append(f"{rel}: directory name is not 'YYYY-MM-DD.NN-slug'") + violations.extend( + f"{rel}/{child.name}: unexpected file in bundle (allowed: {', '.join(sorted(ALLOWED_BUNDLE_FILES))})" + for child in sorted(bundle.iterdir()) + if child.name not in ALLOWED_BUNDLE_FILES + ) + design = bundle / "design.md" + change = bundle / "change.md" + plan = bundle / "plan.md" + if not design.exists() and not change.exists(): + violations.append(f"{rel}: bundle has neither design.md nor change.md") + for spec_file in (design, change): + if spec_file.exists(): + _check_spec_file(spec_file, f"{rel}/{spec_file.name}", dir_slug, violations) + if plan.exists(): + _check_plan_file(plan, bundle, f"{rel}/plan.md", dir_slug, violations) + + +def _check_decision(path: pathlib.Path, violations: list[str]) -> None: + """Validate one decision file.""" + rel = f"decisions/{path.name}" + fields = parse_frontmatter(path.read_text(encoding="utf-8")) + _require(fields, DECISION_REQUIRED, rel, violations) + _check_common(fields, None, rel, violations) + status = fields.get("status", "") + if status and status not in VALID_DECISION_STATUS: + violations.append(f"{rel}: invalid status '{status}' (allowed: {', '.join(sorted(VALID_DECISION_STATUS))})") + + +def check() -> list[str]: + """Validate every bundle and decision; return the list of violation strings.""" + violations: list[str] = [] + for bundle in sorted(CHANGES_DIR.iterdir()): + if bundle.is_dir(): + _check_bundle(bundle, violations) + if DECISIONS_DIR.is_dir(): + for path in sorted(DECISIONS_DIR.glob("*.md")): + if path.name == "README.md" or path.name.startswith("_"): + continue + _check_decision(path, violations) + return violations + + def main() -> int: - """Print the listing to stdout.""" + """Print the listing to stdout, or validate bundles with --check.""" + if "--check" in sys.argv[1:]: + violations = check() + if violations: + sys.stderr.write(f"planning: {len(violations)} violation(s)\n") + for violation in violations: + sys.stderr.write(f" - {violation}\n") + return 1 + sys.stdout.write("planning: OK\n") + return 0 sys.stdout.write(render(load_bundles(), load_decisions())) return 0