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
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ canonical `/llms.txt` prints after the page's title, and what a search engine sh
as the page's snippet — all from the same string. A reader meets it before they meet
the page.

So it must exist, be **at most 200 characters as rendered**, not end in a colon, and
not be identical to another page's.
So it must exist, be **at most 200 characters as rendered**, end in terminal
punctuation, not end in a colon, and not be identical to another page's.

**Rendered, not as typed.** `[Configuration](/contents/BrighterBasicConfiguration.md)`
is fourteen characters to a reader and fifty-four in the source. Measuring the source
Expand Down Expand Up @@ -399,6 +399,7 @@ failure the claim in this paragraph is meant to prevent:
| Convention | Rule | Repo-wide | `--changed` |
|---|---|---|---|
| One H1 per file, before the banner | 1's precondition (`NO H1`) | error | error |
| …and no *second* H1 | 1's precondition (`EXTRA H1`) | error | error |
| Banner present as the first non-blank line after the H1 | 1 | error | error |
| Banner matches `BANNER_RE` — type in vocabulary, *Applies to* present | 2 | error | error |
| Heading qualification, across pages (`##`, allowlist exempt) | 3a | error | error |
Expand All @@ -409,6 +410,7 @@ failure the claim in this paragraph is meant to prevent:
| 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 |
| It ends in terminal punctuation | 7 (`SUMMARY NOT A SENTENCE`) | error | error |
| It is unique across pages | 7 (`SUMMARY NOT UNIQUE`) | error | error |
| `description:` front matter equals it | 7 (`DESCRIPTION MISMATCH`) | error | error |
| That front matter is a quoted single line | 7 (`DESCRIPTION UNREADABLE`) | error | error |
Expand Down
5 changes: 2 additions & 3 deletions contents/BuildingAnAsyncPipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ An async pipeline wraps an async request handler in the same orthogonal operatio

This uses Brighter's [command processor](/contents/CommandsCommandDispatcherandProcessor.md#the-command-processor-pattern) features rather than its [command dispatcher](/contents/CommandsCommandDispatcherandProcessor.md#command-dispatcher) features.

# Implementing a Pipeline
## Implementing an Async Pipeline

The first step in building a pipeline is to decide that we want an orthogonal operation in our pipeline. Let us assume that we want to do command sourcing.

Expand All @@ -25,8 +25,7 @@ The limitation here is that you can only make assumptions about the type you rec
Although it is possible to implement the
[IHandleRequestsAsync](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter/IHandleRequestsAsync.cs)
interface directly, we recommend deriving your handler from
[RequestHandlerAsync\<T\>
\<https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter/RequestHandlerAsync.cs\>]{.title-ref}\_\_.
[RequestHandlerAsync\<T\>](https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter/RequestHandlerAsync.cs).

Let us assume that we want to log all requests travelling through the pipeline. (We provide this for you in the Brighter.CommandProcessor packages so this for illustration only). We could implement a generic
handler as follows:
Expand Down
53 changes: 52 additions & 1 deletion spec/010-information_architecture/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3056,7 +3056,7 @@ its place immediately, since a prose-only PR of this size is exactly the run tha
indistinguishable from a real one. **No URL moves and no redirect is owed**: every rewrite
changes a link's *target*, not any page's path.

- [ ] **Task 11.8:** `BuildingAnAsyncPipeline.md` has two H1s
- [x] **Task 11.8:** `BuildingAnAsyncPipeline.md` has two H1s
- Input: the page, lines 1 and 8
- Output: one H1, and the demoted heading unique and qualified
- Notes: No rule sees it. `NO H1` fires only on *no* H1 and rule 3b starts at `##`, so the
Expand All @@ -3066,6 +3066,57 @@ changes a link's *target*, not any page's path.
demoting is safe, but a demoted H1 becomes a `##` that must then be unique across pages
and qualified by its subject. **Consider whether the rule should exist**: this is one
page today, and one page is how the last un-enforced convention started.
- **Done 2026-08-22.** Demoted, qualified, and the rule exists. See below.

### Task 11.8 as executed — 2026-08-22

**The rule should exist, and the argument for it is the demotion itself.** `NO H1` fires on
*none* and rule 3b starts at `##`, so a second H1 was checked by nothing — and what a second
H1 buys its author is **exemption from every rule a section heading obeys**. Demoting this one
proves it: as `# Implementing a Pipeline` it was fine, and as `## Implementing a Pipeline` it
would have **collided with `BuildingAPipeline.md:52`**, which carries that heading already.
The page escaped rule 3a by claiming to be a title. It is now
`## Implementing an Async Pipeline`, unique and qualified; nothing linked the old anchor,
checked before the edit.

**`EXTRA H1` is an error repo-wide from the same commit**, which the corpus permits: one page
carried the defect and it is fixed here, so the rule lands at zero — the same shape as rule 4,
which became a repo-wide error once Task 7.2 had cleared its debt. `CLAUDE.md`'s ledger gains
the row in the same commit, because a rule in only one of the two places is how the next round
of decay begins.

> **And enumerating the ledger for that row found the gap it was meant to prevent — for the
> third time, in the third direction.** Spec 011's acceptance pass found `NO H1` in the linter
> and missing from `CLAUDE.md`. Phase 10 found it in `CLAUDE.md` and missing from the linter's
> own docstring, and its write-up says *"enumerate **both** directions"*. **`SUMMARY NOT A
> SENTENCE` was in neither** — added to the code by that very session's sweep, four defects
> deep in a list, and never written down anywhere else. The session that drew the lesson is the
> session that broke it again, in the same file, within the same phase. **A parity check is
> only true of the moment it ran**; what makes it hold is running it whenever a label is added,
> which is now Task 11.4's job and this row's precedent.

**One more thing on this page that no tool sees, and it is the last of the 2022 conversion.**

```text
[RequestHandlerAsync\<T\>
\<https://…/RequestHandlerAsync.cs\>]{.title-ref}\_\_.
```

That is reStructuredText that `pandoc` did not finish converting, and a reader sees it
verbatim — braces, backslashes and all — where a link should be. **`linkcheck.py` cannot fail
it because it is not a link**, which is the same sentence as the `.html` finding and the
`(#)` one, for the third time in this phase. Repaired to the ordinary markdown form the
*preceding line* already uses, with the target confirmed against
`Brighter/src/Paramore.Brighter/RequestHandlerAsync.cs`.

**No rule was added for it**, deliberately. `{.title-ref}` occurs **once** in 142 pages, the
conversion that produced it happened once in 2022, and nothing in this repository can produce
another. A rule guarding a door nobody can open is a rule the next reader has to understand
before they may ignore it. The `.html` links earned one because 23 of them survived; this
earned a fix.

**Gates after:** linkcheck **144 files, clean**; pagelint **0 errors** / 791 warnings / 142
pages; `--check-shape` 0; `--check-redirects` 0 at 77 entries.

- [ ] **Task 11.9:** `README.md`'s front matter is enforced by nothing
- Input: `README.md`; `pagelint.py`'s `load_pages()`
Expand Down
26 changes: 22 additions & 4 deletions tools/pagelint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Rules, and what each is for:

NO H1 a page with no title for a banner to sit under
EXTRA H1 a second H1: a section heading written a level too high
BANNER MISSING no page banner on the first non-blank line after the H1
BANNER MALFORMED a banner is there, but not in the fixed grammar
HEADING NOT UNIQUE a `##` text that also appears on another page
Expand All @@ -19,6 +20,7 @@
SUMMARY MISSING no prose after the banner to summarise the page with
SUMMARY TOO LONG an opening sentence over 200 characters, rendered
SUMMARY ENDS IN COLON an opening sentence promising a list an index drops
SUMMARY NOT A SENTENCE an opening sentence with no terminal punctuation
SUMMARY NOT UNIQUE two pages that introduce themselves identically
DESCRIPTION MISMATCH `description:` front matter that is not that sentence
DESCRIPTION UNREADABLE front matter this tool will not guess the meaning of
Expand Down Expand Up @@ -316,27 +318,43 @@ def check_banner(page):
return [error(page.rel, 1, 'NO H1',
'page has no H1; every page needs a title before a banner')]

# NO H1's mirror, and it was invisible for the same reason in reverse: that
# rule fires only on *none*, and rule 3b starts at `##`, so a second H1 was
# checked by nothing at all. One page carried one. A second H1 is a section
# heading written a level too high -- it claims to be the page's title, it
# is exempt from the uniqueness and qualification rules every real section
# obeys, and GitBook renders it indistinguishably from the real title.
extra = [lineno for level, _, lineno in page.headings
if level == 1 and lineno != page.h1_line]
findings = [
error(page.rel, lineno, 'EXTRA H1',
'a page has exactly one H1, its title. Demote this to `##` and '
'qualify it by the page\'s subject -- as a `##` it must also be '
'unique across pages, which is the check it has been escaping')
for lineno in extra
]

banner_line = None
for lineno in range(page.h1_line + 1, len(page.lines) + 1):
if page.lines[lineno - 1].strip():
banner_line = lineno
break

if banner_line is None:
return [error(page.rel, page.h1_line, 'BANNER MISSING',
return findings + [error(page.rel, page.h1_line, 'BANNER MISSING',
f'nothing follows the H1; add a banner below it, e.g.\n {BANNER_EXAMPLE}')]

text = page.lines[banner_line - 1].rstrip()
if not text.startswith('>'):
return [error(page.rel, banner_line, 'BANNER MISSING',
return findings + [error(page.rel, banner_line, 'BANNER MISSING',
f'add a banner below the H1, e.g.\n {BANNER_EXAMPLE}')]
if not BANNER_RE.match(text):
return [error(page.rel, banner_line, 'BANNER MALFORMED',
return findings + [error(page.rel, banner_line, 'BANNER MALFORMED',
'banner must read `> **<type>** · Applies to **<product> V10**'
'[ · Prerequisites: <links>]`, where <type> is one of '
f'{", ".join(PAGE_TYPES)} and the separator is " · " (U+00B7).'
f'\n e.g. {BANNER_EXAMPLE}')]
return []
return findings


# --------------------------------------------------------------------------
Expand Down
Loading