diff --git a/CLAUDE.md b/CLAUDE.md index 771a2a2..d2c5850 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 .md?ask=` 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 @@ -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** — diff --git a/spec/010-information_architecture/tasks.md b/spec/010-information_architecture/tasks.md index 9dfd8f5..a79d14e 100644 --- a/spec/010-information_architecture/tasks.md +++ b/spec/010-information_architecture/tasks.md @@ -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 @@ -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 @@ -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 diff --git a/tools/pagelint.py b/tools/pagelint.py index 9f190f5..22a747a 100644 --- a/tools/pagelint.py +++ b/tools/pagelint.py @@ -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({ @@ -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 @@ -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() @@ -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) @@ -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: @@ -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 ' @@ -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 @@ -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))