require a counter row to say what does not count

Four of these counters were re-scoped the first time anything tested them, and
every re-scoping was a sentence about what had been wrongly included: transforms
were counting parse-phase work goldmark already orders, views were counting
output formats, effects nearly counted an in-memory swap, extensions counts
packages rather than goldmark's own extensions.

So the fix is not another counter but a required shape. The table gains a fifth
column and verify.sh fails on a row that leaves it empty — watched naming the
offending row. Checked by shape rather than by wording, because a gate that
demands a phrase gets the phrase and not the thinking.

Writing the exclusion up front is the cheapest way to find out whether a counter
measures a mechanism or a symptom, and all eight rows could state one, which is
the first evidence that the counters are now scoped right.

Queue entry G6.
This commit is contained in:
Claude Opus 5
2026-08-01 23:25:22 +06:00
committed by bdeshi
parent 8a4718d775
commit 482a862ff5
4 changed files with 54 additions and 10 deletions
+5
View File
@@ -141,6 +141,11 @@ Then `docs/README.md` for whichever topic you are actually here for.
## Why the pieces exist
**Every counter row says what does *not* count**, and `verify.sh` fails on a row that leaves that column
empty (ADR-0070). Four counters had to be re-scoped the first time anything tested them, and each fix was a
sentence about what had been wrongly included — so the sentence is now required up front. A counter that
cannot name an exclusion is measuring a symptom.
**Counters in `docs/state.md`** turn "no abstraction before its second use" into arithmetic. Every
threshold lives in that one table and nowhere else. The agent cannot argue a pipeline into existence
one transform early — it writes the next one inline and lets the count force the extraction. Most
+18
View File
@@ -1148,3 +1148,21 @@ either live in `cmd/` or duplicate the list, which is the latent item already re
`example_test.go`.
Revisit if: features need to be composed somewhere a test can import, which is that latent item and not this
gate.
## ADR-0070 — A counter row must say what does not count
Date: 2026-08-01 · Status: accepted (queue entry G6)
Decision: the counters table in `state.md` gains a required fifth column, **Does not count**, and
`verify.sh` fails on a row that leaves it empty. Every existing row now fills it.
Why: four of these counters — transforms, views, effects, extensions — had to be re-scoped the first time
something tested them, and every re-scoping was a sentence about what had been wrongly included. Transforms
were counting parse-phase work that goldmark's extender list already orders; views were counting output
formats; effects nearly counted an in-memory swap; extensions counts packages and not goldmark's own. The fix
is not another counter but a required shape, because writing the exclusion up front is the cheapest way to
find out whether a counter measures a mechanism or a symptom — and a counter that cannot name one is not
measuring anything. Checked by shape rather than wording, since a gate that demands a phrase gets the phrase
and not the thinking.
Consequence: cheap — one column, one gate, and the four re-scopings are now written down where the next
person meets the counter rather than in an ADR they would have to find. Expensive — a new counter costs a
sentence that may be genuinely hard to write, which is the point and will still feel like friction.
Revisit if: a counter is added whose exclusion is honestly "nothing" — then the column is wrong, or the
counter is.
+16 -10
View File
@@ -79,16 +79,22 @@ Dependencies: four, all allowlisted — `goldmark`, `golang.org/x/text`, `golang
Never anticipate a threshold. Increment when the code lands, then check whether the extraction is *due
this change*.
| Counter | Now | Extraction due at | What it buys |
|---|---|---|---|
| Render transforms — **page-level only** | 0 | **3** | Stage pipeline (ordered `func(ctx,*Page) error`). Parse-phase work does *not* count and must not: goldmark's extender list is already an ordered pipeline for it, so typography and shortcodes compose there (`cmd/khosra/wire.go`) and a second pipeline beside it would be pure duplication. This counts transforms over the assembled page, which nothing hosts yet — OpenGraph and JSON-LD (queue 15) are the first candidates |
| Routing cases | 11 | **2** — done | Resolver at `internal/web/resolve.go`: bundle, language prefix, pagination, tag, section-narrowed tag |
| Collection pages | 4 | **1** — done | Query primitive: `content.Query{Section, Tag, Lang}` + `Site.Run`. The fourth — a series archive — resolves through `Site.Sequence` instead: membership is structural and the sort ascends, so it shares the index but not the Query |
| Views — **per-bundle selection only** | 0 | **2** | The View layer `architecture.md` describes: `view:` in frontmatter choosing a presentation, resolved through the cascade. Nothing selects a view yet. *Output formats* are counted separately and are not it: HTML, sitemap XML and Atom are three functions with nothing to share — an interface over them would have one member and no leverage |
| Effects | 1 | **2** | Effect runner + trigger wiring (change / schedule / demand). The first and only is the derivative pass (ADR-0042), called straight from `cmd` inside `rebuilder`, so it already answers both triggers it will ever need — startup and a settled change (ADR-0048) — and one call needs no runner. Swapping the index or the theme is **not** an Effect: both re-read the site root into memory, writing no artifact and calling nothing outbound (ADR-0055) |
| Extensions | 4 | **3** — passed, and the answer is still no | Extension registry (`extensions.md`). It reached 3 once before and went back to 2 when the widows feature was deleted (ADR-0045) — a threshold reached by a feature that should not exist was never a threshold. It is 4 with `notation`, and the note below the table says why a registry still buys nothing |
| Interface implementations | | **2** | The interface itself |
| Non-stdlib dependencies | 4 direct | budget in `scripts/budgets.env` | — |
**Every row states what does **not** count.** Four of these counters had to be re-scoped on first contact —
transforms, views, effects, extensions — because each measured a symptom rather than the mechanism, and the
re-scoping was always a sentence about what had been wrongly included. Writing that sentence up front is the
cheapest way to find out whether a counter means anything, so the column is required and `verify.sh` fails on
a row that leaves it empty (ADR-0070).
| Counter | Now | Extraction due at | What it buys | Does not count |
|---|---|---|---|---|
| Render transforms — **page-level only** | 0 | **3** | Stage pipeline (ordered `func(ctx,*Page) error`). This counts transforms over the assembled page, which nothing hosts yet — OpenGraph and JSON-LD (queue 15) are the first candidates | Parse-phase work. goldmark's extender list is already an ordered pipeline for it, so typography, shortcodes and notation compose there (`cmd/khosra/wire.go`); a second pipeline beside it would be pure duplication |
| Routing cases | 11 | **2** — done | Resolver at `internal/web/resolve.go`: bundle, language prefix, pagination, tag, section-narrowed tag | An exact path a crawler asks for by name. `/robots.txt` and `/sitemap.xml` are mux entries because no bundle can collide with them, so they never reach the resolver |
| Collection pages | 4 | **1** — done | Query primitive: `content.Query{Section, Tag, Lang}` + `Site.Run` | A series archive. Membership is structural and the sort ascends, so it resolves through `Site.Sequence` — sharing the index but not the Query |
| Views — **per-bundle selection only** | 0 | **2** | The View layer `architecture.md` describes: `view:` in frontmatter choosing a presentation, resolved through the cascade. Nothing selects a view yet | Output formats. HTML, sitemap XML and Atom are three functions with nothing to share — an interface over them would have one member and no leverage |
| Effects | 1 | **2** | Effect runner + trigger wiring (change / schedule / demand). The only one is the derivative pass (ADR-0042), called from `cmd` inside `rebuilder`, so it already answers both triggers it will ever need — startup and a settled change (ADR-0048) | An in-memory swap. Replacing the index or the theme re-reads the site root into memory, writing no artifact and calling nothing outbound (ADR-0055) |
| Extensions | 4 | **3** — passed, and the answer is still no | Extension registry (`extensions.md`). It reached 3 once before and went back to 2 when the widows feature was deleted (ADR-0045) — a threshold reached by a feature that should not exist was never a threshold. The note below says why a registry still buys nothing | An upstream extension enabled in the list. `Table`, `Footnote` and `DefinitionList` are goldmark's, so they are dialect rather than features of this engine (ADR-0058) — only a package under `internal/ext/` counts |
| Interface implementations | — | **2** | The interface itself | An interface this repo did not declare. Satisfying `fs.FS`, `http.Handler` or `goldmark.Extender` is using somebody else's abstraction, which is the opposite of inventing one |
| Non-stdlib dependencies | 4 direct | budget in `scripts/budgets.env` | — | The standard library, and a dependency's own test-only modules — `go list -m all` shows those, and the gate counts `require` entries instead (`scripts/budgets.env`) |
**The Extensions counter is past its threshold, and a registry would still buy nothing.** The four features
attach in three unrelated ways: `shortcodes` and `notation` are goldmark extenders listed in `extenders()`,
+15
View File
@@ -183,6 +183,21 @@ if [ -d .git ] && command -v git >/dev/null 2>&1; then
note "docs/state.md was last updated in ${laststate:-no commit}, older than the .go change in $(git log -1 --format=%h -- '*.go')"
fi
fi
# Every counter row says what does *not* count (ADR-0070). Four counters were re-scoped on first contact,
# each time by naming something that had been wrongly included, so the column is where that thinking has to
# happen — before the counter is trusted rather than after it misleads. Checked by shape, not by wording: a
# row of the counters table must have five filled cells.
counters=$(awk -F'|' '/^\| Counter \| Now \|/{on=1; next} on && /^\|---/{next} on && /^\|/{
name=$2; gsub(/^[ \t]+|[ \t]+$/, "", name);
last=$6; gsub(/^[ \t]+|[ \t]+$/, "", last);
if (last == "") print name
} on && !/^\|/{on=0}' docs/state.md)
if [ -n "$counters" ]; then
bad "counter row with no 'does not count' (ADR-0070): $(echo "$counters" | tr '\n' ';')"
else
pass "every counter row says what does not count"
fi
# The generated surface is only worth reading if it cannot be wrong. Regenerate and compare rather
# than trusting that whoever added a function also ran the script.
if [ -x scripts/surface.sh ]; then