Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ jobs:
# blocking unrelated work.
- run: python3 tools/pagelint.py

# Spec 018 phase 1, design § Rule 8. Rule 8 (ATTRIBUTE KIND) has one hit in
# the corpus and it is marked, so the step above cannot show the rule still
# fires. These five in-memory plants can: exit 0 only if every one behaves.
- run: python3 tools/pagelint.py --plant

# On a pull request the code rules become errors, but only for blocks that
# overlap the diff. Fixing a typo on a 700-line page therefore obliges
# nothing beyond the typo.
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,32 @@ never silenced. It still counts towards the debt and still says so in its own wo
Without that, relocating a block verbatim is indistinguishable from writing a new
one, and a page split cannot honour "move the text, do not improve it".

### Handler attributes match their handler

A sync handler attribute decorates `Handle`; its `…Async` twin decorates `HandleAsync`.
`[UsePolicy]` on `HandleAsync` **compiles**, so the compiler is no help: Brighter throws
`ConfigurationException` when it builds the pipeline, and `ValidatePipelines()` reports it
at startup. A block can therefore build, enter `blockcheck`'s baseline, and still teach
the reader a pipeline that fails. Rule 8 (`ATTRIBUTE KIND`) reads every C# block for it.

The attributes it knows are the ones Brighter ships in both forms, held as `PAIRED`
beside `APPLIES_TO` in `tools/pagelint.py`, with the command that re-derives them at a
version bump. Edit them there and nowhere else.

A block that shows the mismatch on purpose carries a marker on the line above its fence:

```markdown
<!-- pagelint: attr-mismatch-intended the Before (error) example: a sync attribute on HandleAsync is the mistake this section teaches -->
```

That is `PipelineValidation.md`'s, the one block in the corpus that needs it. The marker
silences rule 8 for **that block only**, and every honoured marker prints with its
reason. It is per block rather than per page because that page has correct examples
beside the deliberate one, and a page-wide marker would have hidden a real mismatch
among them. A marker with no reason, with no C# block after it, or that is a block's
second, is itself an error. `python3 tools/pagelint.py --plant` proves the rule still
fires, since the corpus's one hit is marked.

### The opening sentence

Every page's first sentence after the banner has to survive being read on its own.
Expand Down Expand Up @@ -543,6 +569,7 @@ failure the claim in this paragraph is meant to prevent:
| Language tag on every fence | 4 | error | error |
| "Dispatcher", not "ServiceActivator" or "Service Activator", in prose | 5 | error | error |
| `using` directives in C# blocks | 6 | warning, counted | error, unless the block marks its omission `// ...` |
| A handler attribute's kind matches its method's: sync on `Handle`, `…Async` on `HandleAsync` | 8 (`ATTRIBUTE KIND`) | error | error, unless the block is marked `attr-mismatch-intended` with a reason |
| An opening sentence exists | 7 (`SUMMARY MISSING`) | error | error |
| It is ≤ 200 characters **rendered** | 7 (`SUMMARY TOO LONG`) | error | error |
| It does not end in a colon | 7 (`SUMMARY ENDS IN COLON`) | error | error |
Expand Down
1 change: 1 addition & 0 deletions contents/PipelineValidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ An async handler must use async versions of pipeline attributes. The example bel

**Before** (error):

<!-- pagelint: attr-mismatch-intended the Before (error) example: a sync attribute on HandleAsync is the mistake this section teaches -->
```csharp
public class OrderHandler : RequestHandlerAsync<OrderCreated>
{
Expand Down
82 changes: 74 additions & 8 deletions spec/018-compile_residual/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Design § *Rule 8*. **Predicted:** every gate unmoved, except a new `pagelint --
`pagelint` reads **0 errors, 524 warnings, 162 pages** at both ends, because the marker is an HTML
comment and changes no block's text.

- [ ] **Task 1.1:** Add `PAIRED` and the `ATTRIBUTE KIND` check to `tools/pagelint.py`
- [x] **Task 1.1:** Add `PAIRED` and the `ATTRIBUTE KIND` check to `tools/pagelint.py`
- Input: design § *Rule 8* (what it reads, what it reports, `PAIRED`, the message);
`spec/017-compile_repairs/probe/attr_mismatch.py` (`scan()`, moved unchanged);
`tools/pagelint.py` `:164` (`APPLIES_TO`), `:475` (`check_code_blocks`)
Expand All @@ -121,48 +121,114 @@ comment and changes no block's text.
- Notes: `PAIRED` is re-derived at both refs as you write it (§ 1 obligation 1); the design's 13
are the expected answer

- [ ] **Task 1.2:** Add the `attr-mismatch-intended` opt-out and its three faults
- [x] **Task 1.2:** Add the `attr-mismatch-intended` opt-out and its three faults
- Input: design § *Rule 8*, *The opt-out*; `tools/blockcheck.py:280–300`, the binding it copies
- Output: a marker binding to the next C# block below it. A marker with no reason, a marker with no
C# block after it, and a second marker on one block are each an error. Honoured markers print
with their reasons and a count line, *"N block(s) marked attr-mismatch-intended"*

- [ ] **Task 1.3:** Add `pagelint --plant` and record its red-proof
- [x] **Task 1.3:** Add `pagelint --plant` and record its red-proof
- Input: design § *Rule 8*, the five-plant table; 1.1 and 1.2
- Output: `python3 tools/pagelint.py --plant; echo $?` → **0**, all five plants behaving. A
red-proof run, with one plant's expectation inverted in a scratch copy, → **1**, recorded here
with its output (AC1, AC3)

- [ ] **Task 1.4:** Mark `PipelineValidation.md` block 7
- [x] **Task 1.4:** Mark `PipelineValidation.md` block 7
- Input: design § *Rule 8*, *The page*
- Output: the marker line above the fence of block 7. `python3 tools/pagelint.py` → **0
errors**, *"1 block(s) marked attr-mismatch-intended"*. Recorded here: the run with the marker
removed reports `ATTRIBUTE KIND` on block 7 (AC2). `blockcheck --report` is unmoved, at 990 / 299
- Notes: changes the published site. Its sign-off is asked for in 1.8's PR

- [ ] **Task 1.5:** Run rule 8 in CI
- [x] **Task 1.5:** Run rule 8 in CI
- Input: `.github/workflows/docs.yml`, the `check` job
- Output: a bare `- run: python3 tools/pagelint.py --plant` step after the `pagelint` step, with
a comment citing design § *Rule 8*

- [ ] **Task 1.6:** Write rule 8 into `CLAUDE.md`
- [x] **Task 1.6:** Write rule 8 into `CLAUDE.md`
- Input: design § *Rule 8*, *`CLAUDE.md`*; `CLAUDE.md` § *The ledger* and § *Complete code blocks*
- Output: one ledger row, and § *Handler attributes match their handler* after *Complete code
blocks*. Then `grep -rn 'pagelint' .claude/commands/` (**2** files at `77b7113`: `implement.md`,
`review.md`), each hit read against the new rule and fixed where it quotes a rule set or count
(the *Writing Review* rule)

- [ ] **Task 1.7:** Update `tools/README.md` for rule 8
- [x] **Task 1.7:** Update `tools/README.md` for rule 8
- Input: `tools/README.md` row 2, § *What each gate actually checks*, § *The other modes*
- Output: row 2 with the phase's ref (warnings unmoved); `pagelint`'s bullet naming rule 8; *The
other modes* listing `pagelint.py --plant`. AC14's corrected-form count → **1** for any new figure

- [ ] **Task 1.8:** Close phase 1
- [x] **Task 1.8:** Close phase 1
- Input: § 1 obligations 6, 7, 19; the phase's prediction
- Output: § *Phase 1 as executed*, with every gate's figure against the prediction, and the
reading criteria (AC9/AC10: **no block's text changed**, so none applies, stated). Then the PR,
asking for sign-off (one page changed) and for deletion of its head ref by name


### Phase 1 as executed — 2026-10-05

**Measured at phase start, `94ad0ed`.** `master` moved after tasks were approved: #195, a bugfix
outside the programme, changed six pages and added `bugfixes/0001-unmapped-type-guidance/`. Two
figures moved with it. The prediction above was made at `1b6f7f9` and stands as written:

| Figure | Said (`1b6f7f9`) | Measured (`94ad0ed`), method 1 | Method 2 | Cause |
|---|---:|---|---|---|
| `pagelint` warnings | 524 | summary line → **512 / 66 pages** | `grep -c 'USING DIRECTIVES'` → **512** | #195 repaired 12 debt blocks on its six pages |
| `linkcheck` files | 165 | **166** | a worktree at `019ef8e` → **165** | #195's `bugfix.md` is inside the walk |
| `blockcheck` | 990 / 299 / 674 / 17 | `--report` rows → **990 / 299 / 674 / 17** | `grep -vc '^#' baseline.tsv` → **299** | unmoved |
| `PAIRED` | 13 | the derivation at Brighter `10.7.0` → **13** | at Brighter `origin/master` → the same 13 | — |

`tools/README.md` rows 1 and 2 now carry `94ad0ed`'s figures with their refs.

**Rule 8's first red run** (1.1), the whole repo before the marker, exit 1:

```text
contents/PipelineValidation.md:250: ATTRIBUTE KIND: [RejectMessageOnError] is the sync attribute, on HandleAsync. Use [RejectMessageOnErrorAsync], or mark the block <!-- pagelint: attr-mismatch-intended <reason> --> if the mismatch is the point

1 errors, 512 warnings (using-directive debt: 512 blocks across 66 pages) across 162 pages.
```

`--changed origin/master` reports the same error, exit 1, so the rule is an error at both levels.

**The marker's three faults** (1.2), each planted on `PipelineValidation.md` and reverted. Each is
an error, and the first and third leave the block checked:

```text
contents/PipelineValidation.md:248: ATTRIBUTE KIND MARKER: the block at line 249 already has a marker, at line 247. The block below is still checked
contents/PipelineValidation.md:420: ATTRIBUTE KIND MARKER: no C# block follows it, so it marks nothing
contents/PipelineValidation.md:247: ATTRIBUTE KIND MARKER: no reason given; write <!-- pagelint: attr-mismatch-intended <reason> -->. The block below is still checked
```

**`--plant`'s red-proof** (1.3, AC1, AC3): a scratch copy with the matched-pair plant expecting 1
hit rather than 0, exit **1**. The real one exits **0**, 5 of 5. `--plant x` exits 2.

```text
FAILED matched pair, sync on Handle: ATTRIBUTE KIND x0, expected x1
4 of 5 plants behave.
```

**The page** (1.4, AC2): with the marker, `pagelint` reports **0 errors** and *"1 block(s) marked
attr-mismatch-intended"*. Without it, the red run above. `blockcheck --report` gives
`PipelineValidation.md`'s 16 verdicts identically with and without the marker, and the corpus stays
at 990 / 299.

**Gates against the prediction** (*every gate unmoved; `pagelint` 0 errors at both ends*):

| Gate | Predicted | Measured | Why any difference |
|---|---|---|---|
| `linkcheck` | unmoved | 166, 0 broken | 165 → 166 is #195, before the phase |
| `pagelint` | 0 errors, 524 warnings | **0 errors, 512 warnings, 162 pages**; 1 marked | 524 → 512 is #195; the phase moved none |
| `pagelint --plant` | exit 0 | exit **0**, 5 of 5 | — |
| `symbolcheck` | unmoved | 22 entries, 161 pages, 3 silenced, 0 found | — |
| `blockcheck` | 990 / 299 | **990 / 299 / 674 / 17** | — |

**Reading criteria** (obligation 19): the marker is an HTML comment above a fence, so **no block's
text changed**. Neither AC9 nor AC10 applies.

**Commands that cite what changed** (1.6): `grep -rn 'pagelint' .claude/commands/` → `implement.md`
(`:69` rule 6, `:80` the gate commands) and `review.md` (`:2`, `:12`, `:25`, `:81`). None quotes a
rule count that rule 8 moves. The ledger's three review-only conventions are still three, and
`review.md`'s error filter keeps the new marker lines.

---

## Phase 2 — Instruments *(15 tasks, one PR, no page changes)*
Expand Down
20 changes: 13 additions & 7 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ not be in a command.
`412fd34` is `master` as of 2026-09-13 — spec 014 phase 4, PR #161. Re-derive these before
quoting them; the command is in the row so that you can.

**`linkcheck` reads 165 here and read 164 at `fc77c42`, one merge earlier.** The file you are
reading is the +1: `tools/` is inside `linkcheck`'s walk, so this file entered its corpus the day
it was written. Both numbers are true at their refs, which is what the refs are for.
**`linkcheck`'s file count moves whenever a file lands anywhere it walks, not only under
`contents/`.** It read 164 at `fc77c42`; this file made it 165, because `tools/` is inside the
walk; and `bugfixes/0001-unmapped-type-guidance/bugfix.md` made it 166 at `94ad0ed`. Each number is
true at its ref, which is what the refs are for.

**Two rows carry a second ref, `3be2a78` — spec 015 phase 4, the triage repairs.** `symbolcheck`
went from 5 watchlist entries to **22** when the triage's seventeen confirmed-dead names were
Expand Down Expand Up @@ -80,8 +81,8 @@ package added. `pagelint` fell 537 → **524**, on blocks the repairs gave their

| # | Gate | Command | Expected at `412fd34` |
|---:|---|---|---|
| 1 | `linkcheck` | `python3 tools/linkcheck.py` | **165 files, 0 broken** |
| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 524 warnings, 162 pages** — at `bd95ee0`; it read **537** at `5981b12`, **616** at `ca6a0b2`, **658** at `2223fcb`, **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` |
| 1 | `linkcheck` | `python3 tools/linkcheck.py` | **166 files, 0 broken** — at `94ad0ed`; it read **165** at `bd95ee0` |
| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 512 warnings, 162 pages**, with **1 block marked `attr-mismatch-intended`** — at `94ad0ed` plus spec 018 phase 1, which moved no warning; it read **524** at `bd95ee0`, **537** at `5981b12`, **616** at `ca6a0b2`, **658** at `2223fcb`, **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` |
| 3 | shape | `python3 tools/urlmap.py --check-shape` | **161 pages, 12 sections, widest 12 of 20, deepest 4 of 4** |
| 4 | redirects | `python3 tools/urlmap.py --check-redirects` | **77 entries, 7858 bytes** |
| 5 | `versioncheck` | `python3 tools/versioncheck.py` | **0 stale pins of 18, across 5 pages** |
Expand Down Expand Up @@ -169,6 +170,7 @@ python3 tools/linkcheck.py contents/Glossary.md # specific files; skips the
python3 tools/pagelint.py contents/Glossary.md # specific pages
python3 tools/pagelint.py --changed origin/master # strict on code blocks overlapping the diff
python3 tools/pagelint.py --fix # repair banners, fences, descriptions
python3 tools/pagelint.py --plant # rule 8's five in-memory plants; exit 0 only if all behave
python3 tools/symbolcheck.py contents/Glossary.md # specific pages
python3 tools/symbolcheck.py --census # open-world report, never a gate, always exit 0
python3 tools/symbolcheck.py --verify-list # is every watchlist row still dead?
Expand Down Expand Up @@ -232,14 +234,18 @@ that file is a diff someone reads, not a command someone reruns until the build
`EMPTY TARGET`, and `ORPHAN` — a page under `contents/` that `SUMMARY.md` never links to.
Orphans are reported only on a whole-repo run.
**Its corpus is the repository, not the published tree**: it walks everything except `.git`,
`.github`, `.claude`, `.repomix`, `spec/` and `node_modules`, which is why its file count (165)
`.github`, `.claude`, `.repomix`, `spec/` and `node_modules`, which is why its file count (166)
is higher than `pagelint`'s page count (162) and why *this file* is in it.
It resolves a link to `CLAUDE.md` but has no index of its headings, so
**an anchored link into `CLAUDE.md` reports `MISSING ANCHOR` even when the heading exists.**
Cite `CLAUDE.md` sections as prose here, not as anchored links.
- **`pagelint`** — the authoring conventions, rule by rule, over `contents/` plus the root
`README.md`. `CLAUDE.md` § *Enforcement* carries the ledger that maps every convention to its
rule and back; do not restate it here.
rule and back; do not restate it here. **Rule 8, `ATTRIBUTE KIND`**, is the one rule that reads
what a block *means* rather than how it is formed: a handler attribute of the wrong kind for its
method compiles and fails at pipeline build. Its one corpus hit is marked, so CI runs `--plant`
beside the whole-repo step to show the rule still fires — `CLAUDE.md` § *Handler attributes
match their handler*.
- **`urlmap --check-shape`** — the navigation's shape: at least 2 pages per section, at most 20
top-level entries, at most 4 URL segments, and no `SUMMARY.md` heading with leading whitespace.
- **`urlmap --check-redirects`** — every redirect in `.gitbook.yaml` resolves to a file that
Expand Down
Loading
Loading