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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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()`,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user