docs/content-model.md opens with "Engine specification". It is also where the rule lives that a leading underscore makes a file unaddressable — and the human who owns this site did not know that rule, because nothing in this repository is addressed to an author. Twelve documents named docs/ while being exclusively about building the parser is a signpost pointing at the wrong room. Naming the directory for its audience makes the gap visible instead of hiding it. docs/ is now reserved and deliberately absent: an empty docs/ is an honest statement that end-user documentation does not exist, where docs/ full of parser specs was a claim that it did. HARNESS.md stays at the root. Root holds the three entry points — README.md for a human, CLAUDE.md for an agent, HARNESS.md for whoever maintains the machine — and harness/README.md is the map of the directory, so moving the guide inside would have collided with it for nothing. Mechanical and wide: 100 path references across 24 files. Every verify.sh gate that names a doc by path, the directory lists the dangling-path and ADR-number gates scan, surface.sh's output target, the Makefile, CLAUDE.md's read order, the skill, four commands, and two Go package comments. A first pass with a shell loop silently edited only four files and the rest still said docs/; the fix was to write the file list out and check the remaining count was zero rather than trust the loop's exit status. No rule, threshold, gate or obligation moved — this is a rename, and the gates demonstrated it twice: they stayed green on the new paths, and the ADR-number gate caught ADR-0082 before the entry existed. Deferred, both on the human's call: the end-user documentation site itself, which wants its own decision about where it lives and whether its claims are gated; and moving examples/ under docs/, since demo-site is a live site root that verify.sh, the coverage test and make demo all point at, and moving it would couple a rename to a design nobody has made. 31 files, +146/-106. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
233 lines
14 KiB
Markdown
233 lines
14 KiB
Markdown
# The feature loop
|
|
|
|
Seven steps, in order: **Clarify → Plan → Implement → Verify → Document → Commit → 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
|
|
`harness/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 `harness/state.md`, then `harness/README.md` — the map, always, before deciding what else to open.
|
|
|
|
**A handoff is not evidence.** If `.scratch/continue.md` says something is pending, waiting, or unfinished,
|
|
run `git log --oneline -- <the path it names>` before planning from it. The file is uncommitted, so no gate
|
|
compares it to anything, and it is written *during* a change whose commit can still move underneath it. The
|
|
log is the only record of what landed. Offering the human work that is already in `HEAD` wastes the turn
|
|
they asked the question in.
|
|
For code, read the compressed form before the source: `harness/surface.md` locates every declaration,
|
|
`go doc` gives a package's surface, a test states its contract. Disciplines: `harness/context-economy.md`.
|
|
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 | a `_WARN` threshold, a style-floor preference, a latent item's trigger, a shape sketched in `ideas/` | 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 more of something a doc already describes as built | 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 `harness/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 --quiet` — doc coupling, format, vet, build, test, dependency allowlist,
|
|
budgets. Quiet prints only failures, warnings and the verdict; use the loud form when a gate fails and
|
|
you want its neighbours, or when auditing the gates themselves.
|
|
|
|
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.
|
|
|
|
**A standing advisory is a defect.** If a warning fires on correct code and will keep firing, either the
|
|
check is wrong or the code is — resolve which, in that change. Tolerated warnings are how the whole
|
|
channel stops being read, and the count creeping from one to two is the only signal you get.
|
|
|
|
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: `harness/README.md`. Walk the propagation table above — every surface
|
|
done or explicitly n/a. Minimum:
|
|
|
|
- `harness/state.md`: inventory rows, counters, latent items added/removed — in *this* commit, never a
|
|
trailing one, which is what `verify.sh` compares (ADR-0057).
|
|
- `harness/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>' harness 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. Commit
|
|
|
|
Not optional and not asked for: the work is committed before the report, every time. `CLAUDE.md` §4
|
|
holds the rule; this is how.
|
|
|
|
**The notice, if this session did not ask for automatic commits.** Before the first commit only, on
|
|
its own line, visible:
|
|
|
|
```
|
|
**Committing this to git** — and every later batch of changes in this session. Say "stop committing"
|
|
and I will leave the tree dirty instead.
|
|
```
|
|
|
|
Once said, it is not repeated. If the human asked for automatic commits — in this session or by
|
|
having put the rule here — skip the notice entirely; repeating a notice for something already agreed
|
|
is noise that trains them to skim the next one.
|
|
|
|
**What goes in one commit.** The changes that would be reverted together (`conventions.md` "Git").
|
|
Bundle freely inside one unit: code with its test, its `state.md` row and its ADR are one commit
|
|
because reverting the code without the doc leaves a lie. Split when a second pass is independently
|
|
revertible — a fix to something you noticed afterwards, or a doc correction that was already wrong
|
|
before this change. "Which of these would I want to undo alone?" answers it faster than any rule.
|
|
|
|
**How.**
|
|
|
|
- `git status` first: an unexpected file is a stop condition, not something to sweep into `git add -A`.
|
|
- Stage what the change touched, then check `git diff --cached --stat` against the plan's file list.
|
|
- The pre-commit hook runs `verify.sh`, so a red gate blocks the commit. That is the design — fix the
|
|
gate or the code, do not reach for `--no-verify`.
|
|
- Subject imperative, under 72 characters. Body says *why*, plus the numbers the change earned
|
|
(LOC, counters, a benchmark) since `git log` is the map when no agent is available.
|
|
- Never `git push`. Publishing is the human's; committing is bookkeeping.
|
|
- Scratch files, `ideas/`, `reference/` and anything under the scratchpad stay out.
|
|
|
|
**Then reconcile the handoff — once the commit exists, never before.** If `.scratch/continue.md` carried a
|
|
planned item this change completed, delete that item now and note anything learned that changes a later
|
|
one. Doing it during Document records the plan's version of events instead of the result: an amend, a
|
|
squash or a late fix moves the commit under a handoff that already claims what it contains.
|
|
|
|
## 7. 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
|
|
Committed: the commit subjects this produced, one line each
|
|
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 | `harness/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 |
|
|
| Ending a turn with the work only in the working tree | The one copy is the one that gets lost | Commit before reporting |
|
|
| Offering work a handoff calls pending | The file states intent; only the log states what landed | `git log -- <path>` before trusting it |
|
|
| Ticking the handoff during Document | The commit can still move under it | Reconcile it after the commit exists |
|
|
| Opening a file to find out what is in it | The generated surface already answers it | `harness/surface.md`, then read the range |
|
|
| Grepping for callers before changing a signature | The compiler enumerates them exactly | Change it, then `go build ./...` |
|
|
| A shorter report that dropped a caveat, case or number | Truncation wearing compression's clothes | Restore it; cut words, never findings |
|
|
| One commit per file, or per doc touched | Shredding a single revertible unit | Bundle what would be undone together |
|
|
| Committing without ever saying you would | Consent assumed, not given | The notice, once, before the first one |
|