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
82 changes: 61 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,33 +343,58 @@ supported was right anyway.

### llms.txt

> **Superseded 2026-08-12, and not yet rewritten.** The format below describes a file
> this repository does not generate. **GitBook owns `/llms.txt`**, builds it from the
> published tree, and cannot be overridden; our space contributes 143 of its entries
> automatically. The platform's line format is `- [Title](url): description`, with **no
> type field** — the type reaches a reader through the banner, which the `.md` variant
> prints one line below the H1. Spec 010 Phase 11 rewrites this section; it is left
> standing meanwhile so the change is reviewed rather than assumed.

`llms.txt` at the repository root indexes the documentation for retrieval systems.
The format is defined here; Spec 010 owns the generator, which builds it from
`SUMMARY.md` and reads each page's banner for the type — which is why the banner has
to land first.
**`/llms.txt` is GitBook's, not ours.** The platform builds it from the published tree,
serves it at the space root, and there is no way to override it — a file written in this
repository would live on GitHub only and compete with the canonical one. So this section
describes a file to *feed*, not one to generate. Measured live 2026-08-22:

```
# Brighter and Darker Documentation
```text
# Paramore Brighter Documentation

> CQRS and Messaging frameworks for .NET. Brighter handles Commands and Events;
> Darker handles Queries.
## Paramore Brighter Documentation
- [Basic Concepts](https://brightercommand.gitbook.io/paramore-brighter-documentation/get-started/basicconcepts.md): A command is an instruction to carry out work.

## Get Started
- [Your First Command](/contents/TutorialFirstCommand.md): Tutorial — send a command in-process, no broker required.
## V9 Paramore Brighter Documentation
- [Basic Concepts](https://brightercommand.gitbook.io/paramore-brighter-documentation/v9-paramore-brighter-documentation/overview/basicconcepts.md): Brighter V9 (superseded). A command is an instruction to carry out work.

## Transports
- [RabbitMQ Configuration](/contents/RabbitMQConfiguration.md): Reference — connection, publication and subscription parameters.
# Agent Instructions
## Querying This Documentation
```

Sections mirror `SUMMARY.md`. Each line is `- [Title](path): Type — one sentence.`
**56,527 bytes, 201 entries, and all 201 carry a description** — 143 from this space and
58 from `v9`. The trailing *Agent Instructions* section is the platform's own, and it
documents a query endpoint worth knowing about: `GET <page>.md?ask=<question>` answers a
natural-language question against the page.

**One line per page: `- [Title](url): description`.** There is **no type field**, and none
should be added: the type reaches a reader through the banner, which the `.md` variant
prints one line below the H1, so a prefix would only spend the snippet repeating what the
page is about to say. AC9 was narrowed to match this, deliberately.

**Three parts, three different sources, and they are easy to misremember:**

| Part | Comes from | Evidence |
|---|---|---|
| The **URL slug** | the **filename** — falling back to the `SUMMARY.md` title only when no file exists | Phase 10: repointing an entry at a real file *moved* the URL. The seven fileless entries carried title-shaped slugs; the fifty-one real pages filename-shaped ones |
| The **title** | the **`SUMMARY.md` entry text**, not the page's H1 | 32 pages spell the two differently — `AWS SNS and SQS Configuration` against an H1 of `AWS SQS Configuration` — and the index uses the `SUMMARY.md` text on all 32 |
| The **description** | the page's `description:` front matter, which is [its opening sentence](#the-opening-sentence) with the markdown stripped | 201 of 201 |

So the way to change a page's line in the index is to change its `SUMMARY.md` entry or its
opening sentence. **Nothing here is generated by a tool in this repository**, and the one
that was designed — `tools/llmstxt.py` — was cancelled once the platform was measured
rather than assumed.

**The `##` groups are GitBook *spaces*, not our information architecture.** The twelve
sections of `SUMMARY.md` do not reach this file; a reader of the index sees two groups,
one per space. That is the platform's model and it is not configurable, so the tree this
repository builds is for readers and for navigation, not for the index.

**A V9 description is the one place a marker belongs.** Those pages carry no banner, so
their version is recoverable from nothing — which is why all 58 `v9` descriptions open
with the literal prefix **`Brighter V9 (superseded).`**, and why
`layout.description.visible: false` is deliberately **omitted** on that branch: rendering
the description is the only way the marker reaches a reader who arrives from a search
engine rather than the index.

### Enforcement

Expand All @@ -384,6 +409,21 @@ Exit code is 1 when anything is an error, 0 when clean or warnings only, 2 on ba
arguments — the same contract as `linkcheck.py`, which it runs beside in
`.github/workflows/docs.yml`.

**Scope is `contents/` plus `README.md`, which is 143 pages.** The README is not a
documentation page — it is the site root — but GitBook publishes it, and its
`description:` reaches a reader through `/llms.txt` exactly like any other page's. It
therefore takes **rule 7 and nothing else**: a root page carries no banner, so rules 1
and 2 would be asking it to declare a page type it does not have, and it sits outside
heading qualification, whose purpose — attributing a chunk to the page it came from — is
circular for the site root. It is excluded from the cross-page uniqueness *corpus* too,
so no page can be reported as colliding with a page that is exempt.

That boundary is load-bearing rather than tidy. The reason rule 7 could not simply be
pointed at `README.md` is that the opening-sentence extractor **skips the line after the
H1**, which on every other page is the banner. On the README that line *is* the opening
sentence, so the tool would have summarised the site root with its second paragraph and
then reported the front matter — correct front matter — as a mismatch.

Two strictness levels. Repo-wide, missing `using` directives are a **warning with a
count**, so the existing debt stays visible without blocking unrelated work. On a pull
request, `--changed` makes them an **error for code blocks that overlap your diff** —
Expand Down
88 changes: 86 additions & 2 deletions spec/010-information_architecture/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2956,7 +2956,7 @@ numbered here so it is a box rather than a paragraph. **None is optional.** They
property worth naming: *every one of them is invisible to a green build*, which is why they
are in a spec about enforcement at all.

- [ ] **Task 11.6:** Rewrite `CLAUDE.md`'s § llms.txt to the format the platform ships
- [x] **Task 11.6:** Rewrite `CLAUDE.md`'s § llms.txt to the format the platform ships
- Input: `CLAUDE.md` § *llms.txt*, carrying a SUPERSEDED note since `991fd03`; §3 above
- Output: the section describes `/llms.txt` as GitBook builds it, and the SUPERSEDED note
is gone because there is nothing left for it to warn about
Expand Down Expand Up @@ -3118,7 +3118,7 @@ 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
- [x] **Task 11.9:** `README.md`'s front matter is enforced by nothing
- Input: `README.md`; `pagelint.py`'s `load_pages()`
- Output: either the rule reaches it, or the boundary is recorded where someone will read it
- Notes: `pagelint.py` reads `contents/` only — a boundary that exists so the banner rules
Expand All @@ -3128,6 +3128,90 @@ pages; `--check-shape` 0; `--check-redirects` 0 at 77 entries.
that it takes the *description* rules and not the *banner* ones — the root of a site does
not carry a page type — so this is a scope question about rule 7, not about rule 1.


### Tasks 11.6 and 11.9 as executed — 2026-08-22

**Both are the same task from opposite ends: a convention nothing was checking.** 11.6 is a
section describing a file that does not exist; 11.9 is a file no section's rule reaches.

**Task 11.6 — the `llms.txt` section, rewritten from a measurement.** It described a
generator this spec cancelled, under a SUPERSEDED note that had stood since `991fd03`. It now
describes the file GitBook actually serves, measured live at the moment of writing: **56,527
bytes, 201 entries, 201 with a description** — 143 from this space, 58 from `v9` — plus a
trailing *Agent Instructions* section the platform appends, which documents a `?ask=` query
endpoint nothing in this programme had recorded.

**Writing it turned up a third source nobody had pinned down.** A line is
`- [Title](url): description`, and its three parts come from three different places:

| Part | Source | How it was established |
|---|---|---|
| URL slug | the **filename** | Phase 10, the hard way: repointing an entry at a real file moved the URL |
| **Title** | the **`SUMMARY.md` entry text**, not the H1 | **Measured here.** 32 pages spell the two differently — `AWS SNS and SQS Configuration` against an H1 of `AWS SQS Configuration` — and the index uses the `SUMMARY.md` text on **all 32** |
| description | the page's front matter | 201 of 201 |

Nobody had asked where the title came from, because on 110 of the 142 pages the two agree
and either answer looks right. **The discriminating cases were free to find and are the only
ones that answer the question** — the same shape as the slug correction, which was settled by
the seven entries where filename and title disagreed.

> **One claim in the first draft of that section was wrong, and it was wrong in the direction
> this programme keeps failing in.** It said every `v9` description opens with *"Superseded"*.
> The prefix is **`Brighter V9 (superseded).`**, and the example URL was written for a space
> root that does not exist — `v9`'s pages publish *under* this space's path. Both were
> plausible reconstructions of work done a session earlier. **Checked against the bytes and
> corrected before the commit**, which is the only reason this reads as a note rather than a
> finding.

**And checking it found three pages that are not there.** All 58 `v9` entries carry the
prefix; **three carry nothing else** — `DarkerBasicConfiguration.md`, `ImplementAQueryHandler.md`
and `Logging.md`, whose bodies are an H1 and, in two cases, the word `TODO`. Phase 10's sweep
behaved correctly: there was no opening sentence to append, so it appended nothing. **The
empty description is the symptom; the empty page is the defect**, and it is the same defect as
master's `Logging.md`, which the maintainer ruled *write it*. Left for a ruling rather than
decided here — see the note at the end of this section.

**Task 11.9 — `README.md` now takes rule 7, and the reason it could not before is the
interesting part.** The boundary was never a policy: `opening_sentence()` **skips the line
after the H1**, because on all 142 pages that line is the banner. On the README that line *is*
the opening sentence. Point the existing rule at the file unchanged and it summarises the site
root with its **second paragraph**, then reports the front matter — which is correct — as a
`DESCRIPTION MISMATCH`. A rule extended without reading it would have failed loudly on a
correct page, which is the better of the two outcomes and still wrong.

Root pages take **rule 7 and nothing else**, and are excluded from the cross-page uniqueness
*corpus* as well as from its reporting, so no page can be reported as colliding with a page
that is exempt. `--fix` gets the same narrowing: all three repairs are no-ops on `README.md`
today, but by accident rather than by intent, and an accident is not a boundary.

**Red-proofed on four branches, with the extractor called directly first.** That first step is
the one the corpus made necessary: rule 7 was green on 142 pages while reading the wrong input
on twenty of them, so a probe that only watches the tool fire proves the wrong thing.

| Probe | Result |
|---|---|
| the extractor, called directly | line **10**, `"Documentation for the Brighter and Darker projects."` — the real sentence, not the second paragraph |
| baseline | clean, exit 0 |
| description drifted from the sentence | `DESCRIPTION MISMATCH`, exit 1 |
| description unquoted | `DESCRIPTION UNREADABLE`, exit 1 |
| opening sentence deleted | `DESCRIPTION MISMATCH`, exit 1 |

The last row is worth keeping: deleting the sentence does **not** give `SUMMARY MISSING`,
because the next paragraph becomes the summary and mismatches the front matter instead. On a
page with more than one paragraph, *"no opening sentence"* surfaces as a mismatch. The page was
copied aside and restored byte-identically, asserted both ways.

**The corpus is 143 pages to `pagelint.py` from this commit**, and every figure in this
document quoting 142 refers to `contents/` alone. Task 11.4 counts both.

> **Outstanding, and it is a maintainer's call:** the three empty `v9` pages above. The
> precedent is Phase 10, which **removed** six empty scheduler pages from `v9`'s `SUMMARY.md`
> because GitBook published them as empty pages competing in search with the real V10 docs.
> These three differ in one way that matters: those six documented a feature V9 does not have,
> while these document features it does — they were simply never written. Removing them takes
> away a navigation entry a reader may expect to find; leaving them publishes two pages whose
> body is `TODO`.

---

## Appendix A — Where each of the 32 new pages nests
Expand Down
39 changes: 34 additions & 5 deletions tools/pagelint.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@
# not pages and carry no banner.
PAGES_DIR = 'contents'

# Repo-root files that GitBook nevertheless publishes as pages. README.md is the
# site root and the 143rd entry in /llms.txt, so its `description:` reaches a
# reader exactly like any other page's -- and until spec 010 Task 11.9 nothing
# checked it, because this tool read contents/ and stopped.
#
# They take rule 7 and nothing else. A root page carries no banner (rules 1 and
# 2 would be asking it to declare a page type it does not have) and stands
# outside the heading-qualification convention, whose whole purpose is to
# attribute a chunk to the page it came from -- which for the site root is
# circular. Everything about the opening sentence applies unchanged.
ROOT_PAGES = frozenset({'README.md'})

# Canonical. CLAUDE.md quotes this list verbatim; the two must not drift.
# Their repetition is a feature: it makes the end of every page predictable.
NAV_ALLOWLIST = frozenset({
Expand Down Expand Up @@ -232,6 +244,7 @@ def __init__(self, path, rel, lines):
self.path = path
self.rel = rel
self.lines = lines
self.is_root = rel in ROOT_PAGES
self.headings = [] # (level, text, lineno)
self.blocks = [] # dicts: info, start, end, body
self.prose = [] # (lineno, text) outside fences
Expand Down Expand Up @@ -300,7 +313,7 @@ def load_pages():
pages = {}
for path in md_files():
rel = os.path.relpath(path, ROOT)
if os.path.dirname(rel) != PAGES_DIR:
if os.path.dirname(rel) != PAGES_DIR and rel not in ROOT_PAGES:
continue
with open(path, encoding='utf-8') as fh:
lines = fh.read().splitlines()
Expand Down Expand Up @@ -393,6 +406,11 @@ def check_headings(pages, reported):
# 3a: which pages carry each normalised H2 text.
pages_by_slug = defaultdict(set)
for rel, page in pages.items():
# Root pages are outside this convention, so they must not be inside its
# corpus either: a page colliding with README.md would be reported
# against a rule README.md is exempt from.
if page.is_root:
continue
for level, text, _ in page.headings:
if level == 2 and not is_nav(text):
pages_by_slug[slug(text)].add(rel)
Expand Down Expand Up @@ -890,8 +908,14 @@ def opening_sentence(page):
if banner is None:
return None, 'nothing follows the H1'

# Past the banner, or from the H1 on a root page, which has none. Getting
# this wrong is silent rather than loud: the extractor would take the
# opening sentence *for* the banner, skip it, and summarise the page with
# its second paragraph -- which then mismatches the front matter derived
# from the first.
scan_from = page.h1_line if page.is_root else banner
in_fence = False
for lineno in range(banner + 1, len(page.lines) + 1):
for lineno in range(scan_from + 1, len(page.lines) + 1):
line = page.lines[lineno - 1]
match = FENCE_RE.match(line)
if match:
Expand Down Expand Up @@ -1122,7 +1146,7 @@ def describe_scope(merge_base, ranges, pages, reported):
for start, end in ranges[rel])
)
line = (f'--changed {merge_base}: {files} file(s), {hunks} hunk(s) in the '
f'diff; {len(strict_pages)} page(s) under {PAGES_DIR}/, '
f'diff; {len(strict_pages)} documentation page(s), '
f'{blocks} code block(s) strict.')
if not blocks:
line += ('\n No code block overlaps the diff, so the strict rules are '
Expand Down Expand Up @@ -1196,7 +1220,9 @@ def main(argv):
if fix:
changes, refusals = [], []
for rel in reported:
for fixer in (fix_banner_version, fix_language_tags, fix_description):
fixers = ((fix_description,) if pages[rel].is_root
else (fix_banner_version, fix_language_tags, fix_description))
for fixer in fixers:
made, held = fixer(pages[rel])
changes += made
refusals += held
Expand All @@ -1221,10 +1247,13 @@ def main(argv):
findings = []
for rel in reported:
page = pages[rel]
# A root page takes rule 7 only -- see ROOT_PAGES.
if page.is_root:
continue
findings += check_banner(page)
findings += check_code_blocks(page, strict.get(rel, []))
findings += check_terminology(page)
findings += check_headings(pages, reported)
findings += check_headings(pages, [r for r in reported if not pages[r].is_root])
findings += check_summaries(pages, reported)

findings.sort(key=lambda f: (f.path, f.line, f.rule))
Expand Down
Loading