Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:34:18 +06:00
co-authored by Claude Opus 5
commit 02268f9121
33 changed files with 2730 additions and 0 deletions
@@ -0,0 +1,169 @@
# The feature loop
Six steps, in order: **Clarify → Plan → Implement → Verify → Document → Report.**
`CLAUDE.md` holds the rules; this file holds the procedure.
Scale, do not skip. A one-sentence request gets one line of plan and a short report — and still gets
the conflict check, propagation, verification, the `state.md` touch and the stale-reference sweep.
Casual phrasing is not a smaller change: *"default-language files do not need the `.en` part"* is one
unambiguous sentence that rewrites filenames on disk, makes a suffix optional in code, and falsifies
every doc calling it required.
## Propagation — a change is not done until every surface agrees
One request can touch four surfaces. Name each as done or n/a before reporting; the ones people miss
are the first and the last.
| Surface | What it means |
|---|---|
| the site root | Out of reach: it lives in its own repo (ADR-0011). A disk-contract change ships a **written migration step** the author runs, plus a note on whether any bundle key or URL moves. Never claim to have migrated files you cannot see. |
| code | The engine, plus a test for the new behaviour. |
| the theme | Out of reach, like the site root (ADR-0023). This repo ships a **contract extension** in
`docs/theme-contract.md` plus a written note of what a theme must do — never the theme itself, and never
a layout or markup decision dressed as an engine feature. |
| fixtures and emitted output | Fixture sites in `testdata/`, the embedded default templates (a reference implementation of the contract), and anything reading a field or path shape you altered. |
| harness docs | Every doc, ADR, marker or example that assumes the old form. An ADR that mandates what you just made optional is not stale, it is **contradicted** — supersede it, do not quietly reword it. |
---
## 1. Clarify
Read `docs/state.md`, then `docs/README.md` — the map, always, before deciding what else to open.
Use its topic-ownership table to list the docs owning anything this change asserts a rule about,
and read those. That list is a floor: skipping it is how a rule gets written twice and the second
copy contradicts the first.
### Conflict check — every request
Before anything else, ask whether the request contradicts something already decided. Classify and
act; do not average two positions into a compromise nobody chose.
| Kind | Examples | What to do |
|---|---|---|
| **Hard** — reverses a deliberate decision | an ADR, an architecture invariant, the permalink shape, the untrusted boundary, a `_MAX` budget, a frozen contract, the dependency policy | **Stop.** Quote the line, name the file, give both paths: comply, or change the decision (new ADR, or supersede the old one). Wait. Never pick for them. |
| **Soft** — exceeds a convention or a `[spec]` shape | a `_WARN` threshold, a style-floor preference, an unbuilt `[spec]` section's suggested shape, a latent item's trigger | State it in one line, proceed with the request, record the deviation where the convention lives. Conventions are a floor, not a decision. |
| **Stale** — a doc contradicts the code | `state.md` inventory, a STATUS line, an out-of-date example | Auto-resolve: the code wins. Fix the doc in Document, mention it in the report. No question needed. |
| **None** — the request refines or extends what is written | asking for something a `[spec]` section already describes | Proceed. Say nothing about it. |
Resolve intelligently where the answer is genuinely unambiguous — a stale doc, or a request that is a
superset of what is written. Surface anything where a reasonable person could have meant either
thing, and everything in the Hard row without exception. One conflict is worth one message; a
silently reversed decision costs the trust in every other decision.
Then decide what you genuinely do not know. Ask **only** questions whose answer changes the code or
the bytes on disk. Maximum three, one message, up front, each with a **bold** default.
Never ask about: anything `conventions.md`, `content-model.md`, or an ADR already decides; naming,
formatting, file placement, test style; permission to follow the constitution; "would you like me
to also…" — that is scope creep wearing a question mark.
Do ask when: the feature has two plausible disk formats, URL shapes, or authoring ergonomics; it
touches an open question in `state.md`; success criteria are not observable from the request;
it appears to need a dependency, a new package, or a frozen-contract change.
No such questions? One line — *"No questions — assuming tag pages live at the section root and
reuse the post list template."* — and continue.
## 2. Plan
Post this before writing code. Under fifteen lines.
```
Goal: one sentence, observable from outside the program
Reduces to: Bundle | Stage | Query | View | Interaction | Effect | bundle-as-program
Success criteria: the checks that will prove it works (commands, URLs, expected output)
Files: exact paths, marked new / edit, with an estimated ±LOC each
New deps: none (anything else needs an ADR first — stop and ask)
Earn-it check: which counter this increments, and whether an extraction is due this change
Trust check: does any untrusted input reach this code? ("n/a" if not)
Docs read: the owning docs you opened ("none" only if the change asserts no rules)
Not doing: the two or three adjacent temptations you are declining
```
Wait for a go-ahead unless the user said "just do it" or the change is under ten lines in one file.
If the plan reveals a trunk, say so instead of planning and offer the leaf.
## 3. Implement
- Smallest code satisfying the success criteria. Nothing for the version after next.
- Only the planned files. No renames, reordering, reformatting beyond `gofmt`, or tidying of
neighbouring code.
- Something wrong outside the plan? One line on the Latent list in `docs/state.md`, keep moving.
That is the whole mechanism; use it instead of a drive-by fix.
- Reuse the existing shape before inventing one. Copying eight lines twice beats an abstraction at
use two; the third use is when it becomes a design.
- Halt and ask if any `CLAUDE.md §6` stop condition fires. Halting costs one message.
## 4. Verify
Run `./scripts/verify.sh` — doc coupling, format, vet, build, test, dependency allowlist, budgets.
Then **at least one piece of feature-specific evidence you actually executed**: the new test name
and its output, `curl -s localhost:PORT/path | head`, a golden-file diff, the before/after fragment
of real generated markup, or benchmark numbers.
Budget failure: shrink the change, or stop and propose an ADR raising it. Never raise it silently.
Never write "should work", "this will now…", or your own diff summarised as a result. If you could
not run something, say which and why.
## 5. Document
Same change, not later. Triggers: `docs/README.md`. Walk the propagation table above — every surface
done or explicitly n/a. Minimum:
- `docs/state.md`: inventory rows, counters, `verified against` line, latent items added/removed.
- `docs/decisions.md`: an ADR if the choice is expensive to reverse. Six lines. First open the doc
that owns the topic — if it already carries the rule, amend it there; an ADR restating an
existing doc is a duplicate, not a decision.
- Any other doc **only if the change made it wrong.** Never state one fact in two docs.
Doc edits are surgical too. Prefer deleting a stale line to appending a corrected one.
**Then sweep for what you replaced.** Any rule, value, name or path you changed may be described
elsewhere in the old terms:
```
grep -rn '<old form>' docs CLAUDE.md HARNESS.md ideas reference .claude scripts
```
`verify.sh` fails on dangling file paths, ADR numbers and `CLAUDE.md` section refs. It cannot detect
a sentence that is merely now untrue, or a concept renamed in one place — that sweep is the author's,
and skipping it is how a doc ends up contradicting the file it points at.
## 6. Report
Short — one line each, no prose unless a conflict or a stop condition needs explaining. Drop the lines
that are genuinely n/a rather than padding them.
```
Did: what now works, in the user's terms
Evidence: the command you ran and its result
Diff: files touched, ±LOC
Propagated: content / code / templates / docs — each done or n/a, with content files named
Earned: counters after this change; anything now due for extraction
Skipped: what you deliberately did not do, and the latent items you logged
Conflicts: hard ones surfaced and how they were settled, soft ones deviated from, or "none"
Swept: the old form you grepped for after a rename, or "n/a"
```
---
## Failure modes to catch in yourself
| Symptom | What it means | Do instead |
|---|---|---|
| Interface with one implementation | Anticipating, not designing | Concrete type; wait for the second |
| A parameter no caller passes | Speculative flexibility | Delete it |
| Reaching for a dependency | Usually 40 lines of stdlib | Write the 40 lines, or stop and ask |
| Wanting a `switch` on post type in the core | A View or Query is the seam | Move it out |
| Needing a mock to test | Wrong seam | `fstest.MapFS` or a `testdata` dir |
| Renaming "for clarity" mid-feature | Drive-by refactor | Latent list |
| Explaining why the boundary can bend here | It cannot (ADR-0003) | Stop and ask |
| Plan grew while implementing | Scope drift | Stop, re-plan, continue |
| Writing a rule without reading its owning doc | Guessing where you could look | `docs/README.md` topic table, then amend that doc |
| Quietly doing what was asked against a recorded decision | The conflict was real and you hid it | Quote the line, give both paths, wait |
| Changed the rule in code, left the docs describing the old one | Propagation stopped at the first surface | Walk all four surfaces |
| Made a field optional, left the ADR mandating it | Contradicted, not stale | Supersede the ADR |
| Treated a one-sentence request as a narrow change | Articulation size is not blast radius | Walk the propagation table |
| Splitting the difference between request and policy | A compromise nobody chose | Surface it; the human picks |