Skip to content

Page banners, qualified headings, and a linter that gates the build (spec 011) - #74

Merged
iancooper merged 25 commits into
masterfrom
docs/spec-011-page-banners
Aug 5, 2026
Merged

Page banners, qualified headings, and a linter that gates the build (spec 011)#74
iancooper merged 25 commits into
masterfrom
docs/spec-011-page-banners

Conversation

@iancooper

Copy link
Copy Markdown
Member

Spec 011 of the documentation restructure responding to #67. It establishes a set of
authoring conventions, enforces them with a linter that gates the build, and takes
the corpus from 324 violations to zero.

The argument is that an unenforced convention decays. That is not a hypothesis — it is
what the audit measured. CLAUDE.md has carried authoring rules for years with no CI to
check them, and 14% using-directive compliance is what that looks like after a couple
of years. So every convention here ships with a rule, and every rule ships with a gate.

What lands

Every page now states what it is. All 110 pages carry a banner immediately below the
H1:

> **Reference** · Applies to **Brighter V10** · Prerequisites: [Basic Configuration](/contents/BrighterBasicConfiguration.md)

This serves a reader deciding whether to invest in a page, and a retrieval system that
has pulled one chunk out of it with no surrounding context. The version segment matters
more than it looks: pre-V10 Brighter is well represented in blog posts and Stack Overflow
answers, so a model with no counter-evidence in context will confidently emit
configuration that no longer compiles. The banner is that counter-evidence, and it
survives the front-matter stripping chunkers apply — which is also why it is a visible
blockquote rather than front matter, GitBook rendering front matter literally into the
page body
.

Section headings now name their subject. ## Configuration and ## Best Practices
each appeared on 26 pages, which makes every one of them a worse search result and every
extracted chunk unattributable — and GitBook resolved the collisions into
#configuration-1, #configuration-2, anchors no author would choose. 260 cross-page
headings and 31 within-page headings were qualified, and the 19 anchor links that moved
with them were repointed in the same pass.

"ServiceActivator" is gone from prose, replaced by "Dispatcher". 28 findings across
12 pages. One was worth the rule on its own: three transport pages carried an identical
sentence — "the material on configuring Service Activator in Basic
Configuration
"
— whose own link target says dispatcher.
Terminology drift stated and contradicted in a single line, three times over.

Two demonstrator page splits, the two pages the issue and our own reading
independently flagged as worst:

Original Before After
RabbitMQConfiguration.md 566 330 + RabbitMQDurability.md + RabbitMQMigrateToQuorumQueues.md + RabbitMQConnectionStability.md
BrighterBasicConfiguration.md 1,070 237 + CommandProcessorConfigurationReference.md + DispatcherConfigurationReference.md

Both cores keep their original file name, so no published URL moves and no page-level
redirect is needed. What did break was anchor-level links — GitBook redirects operate on
pages, not fragments — so 28 of those across 20 pages were repointed by hand.

tools/pagelint.py gates the build. Six rules, stdlib only, single file, same exit
contract as linkcheck.py. Repo-wide it reports 0 errors; on a pull request it also
runs --changed, which makes the code rules strict for blocks overlapping your diff
— block granularity, not file. Fixing a typo on a 700-line page therefore obliges nothing
beyond the typo, which is the point: a rule that makes small corrections expensive stops
people making them.

The remaining 836 warnings are the pre-existing using-directive debt (802 blocks across
93 pages) and 34 untagged fences. Both are deliberate, counted, and scheduled.

Things worth a reviewer's attention

  • The banner asserted a version that has never existed. The approved design fixed the
    vocabulary as Brighter V10 | Darker V10 | Brighter and Darker V10, assuming Darker
    tracks Brighter's version line. It does not — Paramore.Darker's latest release is
    4.1.1, and there has never been a Darker V10. The sweep put that claim on 10 pages
    before it was caught. Corrected to ('Brighter V10 and Darker V4', 'Brighter V10', 'Darker V4'), defined once in pagelint.py and imported everywhere else.
  • One specified task described defects that did not exist. It was scoped as merging
    three duplicate-content pairs. Glossary.md defines Dispatcher and CloudEvents
    once each; one cited line number points into an unrelated Timeout entry. What
    actually collided was a section heading with its own first term two lines below.
    Executed as written, it would have destroyed correct material. Fixed as heading renames
    instead.
  • .gitbook.yaml's structure: key contained a zero-width space, so GitBook has
    never read that block — it fell back to defaults that happen to name the same two
    files, which is exactly why nothing ever looked broken. The value carried a matching
    one, so fixing only the key would have converted a silently-ignored block into a
    silently-broken one. Both gone, verified by byte inspection.
  • A new page is 100% added lines, so every code block in it is already strict. The
    design's "move blocks verbatim" instruction rested on the opposite premise, and across
    the two splits that was 42 blocks — 42 hard errors the moment this PR opened. Rule 6 now
    honours the // ... escape its own message had always advertised: it downgrades to a
    warning and never silences
    , so the debt stays counted.
  • No information loss was checked mechanically, not by eye. For each split, every
    substantive line of the original was tested for verbatim presence across the resulting
    pages. Only the deliberate edits came back — 24 lines and 4 lines.

Not in this PR

spec/011-authoring_conventions/worklist.md lists 42 pages with a verdict for the
restructure that follows, and 12 of those rows say keep — a list of only split
candidates would invite the next spec to re-open every page it omitted.
FAQ.md, Glossary.md and V10MigrationGuide.md are recorded as legitimately outside
Diátaxis and explicitly not split candidates.

Still open in 011: the language-tag backfill, --fix for pagelint.py, the acceptance
pass, and a rendered-preview check that needs this branch published to GitBook.

#67 stays open until the whole programme lands.

Verification

python3 tools/linkcheck.py     # No broken internal links (112 files checked)
python3 tools/pagelint.py      # 0 errors, 836 warnings across 110 pages

The gate was proven to fail before it was trusted to pass. --changed reporting 0
errors is indistinguishable from a run that found nothing to be strict about, so it was
forced red on purpose: one character changed inside a using-less block turns that block
alone
into an error while the other fifteen on the page stay warnings, and the repo-wide
step stays green in the same run. #73 was a deliberate one-character probe, closed
unmerged, confirming origin/<base_ref> resolves on a real pull_request event.

🤖 Generated with Claude Code

https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg

iancooper and others added 25 commits August 4, 2026 10:30
105 rows, one per page under contents/, proposing a Diataxis page type
from the four signals in design section 1. The verdict column ships
blank: apply_banners.py reads verdict and never proposed, and hard-stops
without writing a page if any row is empty. Task 3.2 is a human filling
that column, and it cannot be skipped or defaulted.

Rows sort lowest confidence first, so the pages needing thought come
before the ones needing a glance:

  review   29  no signal fired - no proposal offered
  low      18  signals conflict, or the page scores 3+ modes
  medium   44  one signal fired cleanly
  high     15  title shape is decisive

Two refinements to the signal cascade, both learned from the corpus:

- "How X Works" is tested before the verb rule. Four pages open with
  "How", and all four are Explanation, not How-to -
  HowBrighterWorks, HowServiceActivatorWorks and the two
  HowConfiguring* pages. The verb rule alone would have mislabelled
  every one of them.
- SECTION_SKEW deliberately omits sections whose members split across
  modes - "Using an External Bus", "Brighter Configuration", the two
  handler-pipeline sections. Those pages fall through to the queue
  rather than inheriting a wrong default, which is why the queue is 29
  rather than 5. Design section 1 is explicit that a review queue beats
  a default: a wrong page type tells the reader the page is something it
  is not.

A page scoring 3+ modes has its confidence downgraded rather than its
proposal changed, and the signal column records the mode count. Those
rows are where Task 7.1's worklist notes come from, and the difficulty
is not recoverable after the sweep - so it is captured now.

FLAGGED carries the five pages design section 1 names as needing
argument. Four have an argued answer and it is proposed at low
confidence, because "the design suggested it" is not "a human agreed".
ShowMeTheCode has none - the design says Reference fits poorly but it is
not a tutorial - so it goes to the queue with that note attached.

Generator kept beside modemix.py rather than deleted, since it documents
how each proposal was derived. apply_banners.py (Task 3.3) is the one
that gets deleted after use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
29 of 105 verdicts filled - the rows proposetypes.py left blank because
no signal fired. classification-notes.md captures the reasoning, which
design section 1 says has to happen during the review because it is not
recoverable afterwards.

Five verdicts overrode the proposal:

  AgreementDispatcher       Explanation -> How-to
  CloudEventsSupport        Explanation -> How-to (split, Reference overspill)
  NullableReferenceTypes    How-to      -> Reference
  ShowMeTheCode             Explanation -> How-to
  MessageMappers            How-to      -> Explanation

ShowMeTheCode is the interesting one: design section 1 had ruled out
Tutorial and called Reference a poor fit, leaving Explanation by
elimination. How-to was the reading nobody proposed, and it is the
page's stated intent.

The load-bearing change is not a page type. Spec 010's README carried
"merge BasicConcepts.md into Glossary.md" as a scoped deliverable in
three places, and that is withdrawn. The two pages do different jobs:
BasicConcepts.md is a curated 24-term orientation set a newcomer can
read without working through the 100-term Glossary.md. The audit
measured the overlap and inferred duplication without asking why the
smaller set existed. Specs 002 and 006 both feed the page deliberately -
002's Task 3.1 added DLQ, Nack and Poison Message to it - which is the
corroboration that this is a live convention rather than an accident.
Per-term links into the matching Glossary.md anchor replace the merge.
010's README and PROMPT.md's decisions list are both amended, so the
merge cannot be executed from a stale note.

Also recorded, for Spec 013 rather than 011: four Explanation pages are
missing their how-to (ClaimCheck, DynamicMessageDeserialization,
QueriesAndQueryObjects, MessageMappers), and MessageMappers itself wants
breaking into three - a default-mapper how-to, the explanation, and a
transforms explanation that states plainly that transforms require a
custom mapper. That last one is a correctness gap, not a filing one: a
reader can currently come away thinking transforms work with the default
mapper.

HandlerFailure.md and ErrorHandlingOptions.md are confirmed as
Explanation and Reference, with the merge-or-pair question flagged for
010 rather than closed.

Two items still need a ruling: Logging.md is a 3-line TODO stub that is
linked from SUMMARY.md, so the orphan check cannot see it; and rule 5
misses the two-word "Service Activator", 19 instances across 11 pages.
76 verdicts remain blank.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
apply_banners.py can now run: 105 of 105 verdicts filled, which is the
hard stop it enforces. Reference 48, How-to 30, Explanation 27.

No page took Tutorial. That is correct today and is precisely the gap
spec 009 exists to fill - the corpus has no tutorial, which is the
substance of the criticism in Docs#67.

Maintainer rulings applied:

- Family overview pages are Explanation, not Reference. Section skew
  cannot tell an overview from an implementation, so it proposed
  Reference for both. BrighterSchedulerSupport, DistributedLock,
  BrighterOutboxSupport and BrighterInboxSupport move; BoxProvisioning
  was already ruled this way and is the pattern's first instance rather
  than an exception to it.
- Logging.md stays Reference and the content gap is raised against spec
  013. It is a three-line TODO that is listed in SUMMARY.md, so it is
  published, navigable, and invisible to the orphan check precisely
  because it is linked.
- Rule 5 widened to Service\s*Activator. Findings go from 11 to 28,
  covering 30 occurrences across 12 pages - two lines carry both
  spellings and the rule reports once per line. CLAUDE.md's ledger,
  rule-5 note and pitfall list all updated to match, since the ledger is
  what AC5 checks.
- The remaining 59 rows bulk-confirmed as proposed.

Two verdicts are the assistant's calls rather than maintainer rulings,
and are flagged as such in classification-notes.md section 7:
CustomScheduler -> How-to (its content is Implementation Steps, twice),
and V10MigrationGuide -> How-to, which contradicts design section 1.
The design argued Reference because the page is "consulted rather than
read through", but the page is structurally Before You Start, Step 1
through Step 6, Rollback Plan. That does not conflict with the
NullableReferenceTypes ruling: there the migration steps are a section
of a page about a language feature; here they are the whole page.

Also recorded for later specs: the seven scheduler-family pages share an
identical template, so one split decision covers all seven rather than
seven worklist rows (Task 7.1); and four Explanation pages have no
how-to route through them, one of which - transforms requiring a custom
mapper - is a correctness gap rather than a filing one, now written into
spec 013's README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
… 3.3)

The one-shot sweep script. It reads the verdict column and never the
proposal, and a blank verdict is a hard stop: it prints the offending
rows and exits 1 without writing to a single page. Proved rather than
asserted - blanking one verdict, blanking one product and setting one
verdict to "Guide" produced three refusals and left contents/ untouched.

pagetypes.tsv gains an "applies" column, which Task 3.1 did not produce
and the banner grammar requires. Derived from SUMMARY.md section plus
Darker mentions in prose: 96 Brighter V10, 5 Darker V10 (the pages in
the two Darker sections, all Darker-dominant in prose), 4 Brighter and
Darker V10 (CQRSWithBrighterAndDarker, Glossary, BasicConcepts,
ShowMeTheCode). Only 14 pages mention Darker at all, so the split is
unambiguous everywhere except WhyBrighter, which mentions Darker five
times but is titled "Why Brighter?" and is filed as Brighter V10.

Prerequisites are omitted from every banner. The segment is optional by
design and choosing prerequisites is a per-page judgement, so including
it would stop the sweep being a sweep. They get added as pages are
edited for other reasons.

Two deviations from the design worth stating:

- The script is re-runnable. A first line already matching BANNER_RE is
  replaced rather than duplicated, so a corrected verdict costs one TSV
  edit and a re-run. Any other blockquote in that position is left alone
  and reported, because it is content this script did not write.
- It is therefore NOT deleted at Task 3.4 as the design says. The
  maintainer has explicitly reserved the right to raise page-type
  exceptions later, and deleting the tool that applies corrections
  before those are settled is backwards. Delete it once the verdicts
  are final.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Mechanical. 105 files, 212 insertions, zero deletions, and every
inserted line is either a banner or a blank. Generated by
apply_banners.py from the 105 human-reviewed verdicts in pagetypes.tsv.

  Reference    48
  How-to       30
  Explanation  27
  Tutorial      0

No page is a Tutorial. That is correct today and is the gap spec 009
exists to fill.

Verified: pagelint rules 1 and 2 report zero, as does NO H1; linkcheck
clean at 107 files; all 105 banners sit below the H1 with a blank line
either side.

Two things nearly rode along and were backed out, because a 105-file
diff is only reviewable if it is genuinely nothing but the one change:

- The first run left the banner with no blank line after it, so it ran
  straight into the following heading. The linter passed anyway - rule 1
  only checks the first non-blank line after the H1 - which is a
  reminder that a green linter is not the same as a correct page.
- The second run silently added a trailing newline to the 18 files that
  lacked one, showing up as 18 deleted lines. Harmless in itself, but it
  is not what this commit claims to be doing. apply_banners.py now
  preserves whether a file ended with a newline; tidying those 18 is a
  separate change if it is worth making at all.

Rule 5's 28 terminology findings are deliberately not in this commit.
They are hand edits and this one is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Phase 3 is four of five tasks done; only 3.5, the rendered-preview
sanity check, remains and it needs a published branch.

classification-notes.md gains section 9, recording two defects the
sweep nearly shipped. The first is the more interesting: the initial run
left the banner with no blank line after it, so it ran into the
following heading - and pagelint passed, because rule 1 only asks what
the first non-blank line after the H1 is. By that test the page was
perfect. Worth remembering before trusting a green linter on the next
mechanical sweep.

Also noted: prerequisites were omitted from every banner, so that half
of the grammar is currently unexercised across all 105 pages. Task 3.5's
rendered preview is the place to look at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Darker is Brighter's sister project - the query side to Brighter's
command side - so its documentation sits alongside and often parallels
Brighter's without being the same. Checking the classification against
that turned up two wrong calls and one structural finding.

WhyBrighter.md now reads "Applies to Brighter and Darker V10". Its
opening sentence is "So why would you choose Brighter & Darker?", its
Command Query Separation section exists to explain that Brighter
modifies state and Darker reads it, and Type over Convention opens
"Brighter & Darker recognize...". I had filed it as Brighter V10 on the
strength of its title while its content said otherwise.

QueryPipeline.md moves from Explanation to How-to. Its Brighter
counterpart BuildingAPipeline.md is a How-to, and the divergence was my
misreading rather than a real difference: I took the lead from
Introduction and How the Query Pipeline Works, when the payload is
Available Decorators, Decorator Patterns and Configuring Polly Policies.
A reader wanting retry on a query handler comes here to do a task. Four
of the five parallel pairs already lined up; this was the fifth.

The structural finding is the reverse of what "sister project" might
suggest. QueryPipeline.md is 928 lines against BuildingAPipeline.md's
177, because the Darker page absorbs material Brighter splits across
BuildingAPipeline, PolicyRetryAndCircuitBreaker and PolicyFallback. The
asymmetry is architectural, not editorial - the same subject decomposed
one way on one side and not at all on the other. Recorded for Task 7.1
and spec 010 rather than fixed by moving a banner.

Also raised, not resolved: "Applies to Brighter V10" now sits on 96
pages, and on a cross-cutting page that is an exclusion claim telling a
Darker reader the page is not for them. RequestValidation.md says in its
own text that it excludes Darker, so some are right. The middleware and
resilience pages are the unverified ones, and settling them needs the
Darker source rather than the docs. The banner has at least made an
implicit claim explicit on every page at once, which is the first time
it has been checkable.

apply_banners.py replaced 2 banners and left 103 unchanged, which is why
it was kept rather than deleted at Task 3.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Design section 1 fixed the vocabulary as Brighter V10 | Darker V10 |
Brighter and Darker V10, assuming Darker tracks Brighter's version line.
It does not. Paramore.Darker's latest release is 4.1.1, with no
published prerelease, and its tags - 4.1.1, 4.1.0, 4.0.1, 4.0.0, 3.0.0,
2.0.79 - are an independent line.

So "Darker V10" is a version that has never existed, and Task 3.4's
sweep put that claim on 10 pages, Glossary.md and ShowMeTheCode.md among
them. The banner exists to stop a reader or a model acting on the wrong
version; a false version marker is the worst thing it could carry.

APPLIES_TO is now ('Brighter V10 and Darker V4', 'Brighter V10',
'Darker V4'), defined once in tools/pagelint.py, imported by
apply_banners.py, documented in CLAUDE.md. Five pages read Darker V4 and
five Brighter V10 and Darker V4. The vocabulary stays closed rather than
becoming a version pattern, so that at V11 every unbumped page fails the
build instead of quietly asserting last year's version.

Retargeting the ten failed on the first attempt, instructively. The
script decided whether a blockquote was its own by matching BANNER_RE,
but those banners were written under the old vocabulary and no longer
matched, so it correctly refused to touch what it took for page content
and wrote nothing. The distinction it lacked is between "is this banner
valid" and "is this banner ours". Those differ exactly when a vocabulary
changes, which is not an edge case - it is every version bump, and V11
will hit it on all 105 pages. BANNER_SHAPE_RE now answers the second
question structurally while BANNER_RE keeps answering the first: the
applier replaces on shape, the linter validates on grammar, and a stale
banner reports BANNER MALFORMED rather than being silently skipped.

Darker's next release is in flight and ../Darker is 7 commits ahead of
its tag, so on the maintainer's instruction the content of those pages
is deferred rather than updated: the working tree documents unreleased
behaviour and the docs site publishes the deployed version. The pages
are positioned instead - the ten are identifiable from the applies
column, and the bump is one edit to APPLIES_TO, one to that column, and
a re-run.

Also fixed while in here: a NameError on apply_banners.py's validation
error path, left by renaming APPLIES to APPLIES_TO.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
`pagelint.py` reported 28 SERVICEACTIVATOR errors across 12 pages, 30
occurrences. All must reach zero before Task 5.1 puts the linter in CI.
They fall into three kinds, and each gets a different fix:

- **Prose meaning the Dispatcher** — renamed. The three transport pages
  carried an identical sentence pointing at
  `#configuring-the-dispatcher` while calling it "Service Activator",
  which is the collision in miniature.
- **Identifiers in bold** — moved to backticks, as CLAUDE.md already
  asks. Package names, `ServiceActivatorOptions`,
  `ServiceActivatorHostedService`.
- **The term as its own subject** — per-line opt-out rather than the
  page-level one, so the rest of those pages stay under the rule.
  `BasicConcepts.md` and `ImplementingExternalBus.md` cite the
  Enterprise Integration Patterns pattern; `HowServiceActivatorWorks.md`
  documents when to use the name at all, and one of its lines is a
  deliberate example of the wrong form.

Two renames assert API facts, both checked against Brighter source
rather than inferred: the Control API's "node" is a Dispatcher
(`GetNodeStatus(this IDispatcher)` sets `NodeName` from
`dispatcher.HostName`), and the health check reads
`Dispatcher.Subscriptions`
(`BrighterServiceActivatorHealthCheck.cs`).

28 insertions, 28 deletions — every changed line is one of the 28
findings. pagelint 324 -> 296 errors, warnings unchanged at 840, so no
code block was disturbed. linkcheck clean at 107 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Two deviations from the planned remediation, both argued rather than
silently taken:

- `HowServiceActivatorWorks.md` takes per-line opt-outs, not the
  page-level one. The page-level comment would disable rule 5 across all
  460 lines to excuse three.
- `BrighterBasicConfiguration.md` was fixed now rather than deferred to
  Task 6.9's page split. Phase 5 precedes Phase 6, so deferring would
  have gated CI on the largest remaining piece of work.

And a finding: rule 5 cannot see headings. `Page._parse()` routes
heading lines to `page.headings` and everything else to `page.prose`,
and `check_terminology` reads `page.prose`. Three headings carry the
term and all three are legitimate, so nothing is wrong today — but
nothing stops a future `## Configuring Service Activator` either. Not
fixed unilaterally: the trailing-comment opt-out cannot be reused on a
heading, because `slug()` would fold the comment into the anchor and
break every inbound link. Left as a maintainer ruling on rule 5's scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Task 4.5 was specified as merging three duplicate-content pairs. **Two
of the three are not duplicates, and the third is not content.** Each
was checked against the file before being touched:

- `Glossary.md` **defines `Dispatcher` once, not twice.** The audit
  recorded `:95` and `:393`; `:95` is the `### Dispatcher` term entry
  and `:393` has nothing to do with Dispatcher — it is inside the
  Timeout entry. What actually collides is the *section* heading
  `## Dispatcher` with its own first term `### Dispatcher`, two lines
  below it. One definition, two headings.
- `CloudEvents` is the same pattern, `## CloudEvents` above
  `### CloudEvents`, and equally not a second definition.
- `FAQ.md` does have two identical `###` headings, but the second held
  no answer — it was a one-line signpost back to the first.

So there is nothing to merge. The real defect is a section heading that
steals its own term's anchor: `#dispatcher` resolved to the section, and
the term entry got `#dispatcher-1`. Fixed by naming the sections after
what they contain — `## Dispatcher and Consumers` (Dispatcher, Consumer,
ServiceActivator) and `## CloudEvents and Encodings` (CloudEvents,
binary-mode, structured-mode). Every term stays an H3, and `#dispatcher`
now unambiguously means the term.

**Spec 009's D12 is therefore not blocked.** It was recorded as waiting
on a `Dispatcher` merge that does not exist; it can link
`Glossary.md#dispatcher` as soon as this lands.

The FAQ signpost is reworded to the distinct, performance-framed
question its section is actually about, keeping the pointer and the
navigation while removing the duplicate.

Rule 3b: 34 -> 31. linkcheck clean at 107 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Rule 3b at zero, from 31. pagelint 293 -> 262 errors; rule 3a unchanged
at 262 and warnings unchanged at 840, so nothing but headings moved.

Within one page a duplicate heading is always a defect — it produces
#configuration-1, #configuration-2 on sections that sit next to each
other. The qualifier comes from the enclosing H2, so the heading says
which of the five components, four strategies or two patterns it belongs
to:

- `InMemoryOptions.md` — 16, the worst page: five InMemory components
  each with When to Use / Configuration / Limitations / Example Usage
- `HandlerFailure.md` — 8: four error-handling strategies each with
  What It Does / When to Use It
- `ReactorAndProactor.md` — 6: Handlers, Message Mappers and
  Middleware/Attributes under both the Reactor and Proactor requirements

All occurrences are qualified, including the first. Leaving the first
bare and qualifying the rest satisfies the rule but leaves the arbitrary
"whichever came first" anchor the rule exists to remove.

**The de-duplication exposed two links that resolve to the wrong
section**, which is the argument for the rule in miniature:

- `KafkaConfiguration.md:227` says "See Configuration Callback *below*"
  but `#configuration-callback` resolved to the publication hook *above*
  it, the subscription one being `-1`. Now points at
  `#subscription-configuration-callback`.
- `AWSSQSConfiguration.md:418` promised migration guidance and linked
  `#aws-sdk-v4-support` — the section the line is already inside. The
  guidance is the next H2, `## V10 Migration Path`.

`AWSSQSConfiguration.md`'s repeated H3 also restated its own H2's title.
What it actually adds is the v3/v4 package list, so it is named for
that.

linkcheck clean at 107 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
…4.1-4.4)

**pagelint.py is at 0 errors**, from 262. Rules 1-5 are all clean; what
remains is the 804-block using-directive debt, which is warnings and
deliberate. linkcheck clean at 107 files.

74 files, 279 insertions, 279 deletions — 260 headings and 19 links,
nothing else.

The qualifier is the page's subject, from its H1 with filler removed, so
`## Provisioning` on seven distributed-lock pages becomes
`## MS SQL Distributed Lock Provisioning` and the rest in kind. Where the
prefix read badly it was written by hand: `## Provisioning the MSSQL
Outbox Table`, not `## MSSQL Outbox Provisioning the Outbox Table`; and
`## Hangfire Best Practices`, not `## Hangfire Scheduler Best
Practices`, as CLAUDE.md asks.

**Six collisions are fixed on the other page**, because the anchor here
carries inbound links worth keeping:

- `BasicConcepts.md` keeps `## Command` and `## Command Processor`
  (8 inbound links); `Command Patterns` takes `## The Command Pattern`
  and `## The Command Processor Pattern`, which is what that page is
  about anyway.
- `BrighterBasicConfiguration.md` keeps `#configuring-the-dispatcher`
  (6 inbound links, 4 cross-page); `HowConfiguringTheDispatcherWorks.md`
  takes `## Configuring a Dispatcher for an External Bus`.
- `ShowMeTheCode.md`, `EventDrivenCollaboration.md` and
  `DispatchingARequest.md` likewise keep theirs.

The four bold outbox H2s lose their emphasis as they are qualified, per
Task 4.2's note — `slug()` ignores emphasis, so the anchor is unchanged
by that part.

**Two headings were navigation under another name**, and take the
allowlisted spelling rather than a qualifier: `## Additional Resources`
on five pages becomes `## Further Reading` (none of the five had any
allowlisted heading), and `## Next` on three becomes `## Next Steps`
(each is a single "See X for guidance" line).

**FAQ.md renames all eight category headings, not just the four that
collide.** Its own table of contents lists all eight, so qualifying half
would leave that list reading two ways at once.

19 links were repointed in the same pass, so there is no window in which
the tree is inconsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Ticks Tasks 4.1-4.6 and adds a "Phase 4 as executed" section carrying
what the phase found that the design did not predict:

- Task 4.5's premise was wrong. The Glossary defines Dispatcher and
  CloudEvents once each; the audit's second line number for Dispatcher
  points into an unrelated entry. Spec 009's D12 is therefore not
  blocked on a merge that never existed.
- Task 4.4 predicted 8 same-page anchor links. There are 18 anchors
  carrying 28 links; choosing which side of each collision moves cut the
  rewrites to 19.
- De-duplication exposed two links that had always resolved to the wrong
  section while passing linkcheck, because the anchor existed - just not
  on the section the author meant.
- `## Additional Resources` and `## Next` were navigation under another
  name, so they took allowlisted spellings instead of qualifiers. No
  allowlist change was needed.

`qualify.py` and `dedupe_within.py` are kept beside `apply_banners.py`.
Their SUBJECT/OVERRIDE/KEEP tables *are* Task 4.1's reviewed proposal
list, so keeping them keeps the reasoning rather than only the result -
and both are re-runnable if a verdict changes.

pagelint.py: 0 errors. linkcheck.py: clean, 107 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Rule 5 cannot see headings, and stays that way. The risk is low enough
to be a future problem, solved then.

Recorded in both places the gap was written up, and marked "do not
re-raise" so a later session reads it as accepted rather than
overlooked. `CLAUDE.md`'s ledger needs no change - it already describes
rule 5 as applying to prose, which is now the intended scope rather than
an accident of the parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Two steps, per design §4. A repo-wide `pagelint.py`, which is the four
rules that are errors everywhere — banner presence and grammar, heading
uniqueness across and within pages, terminology — and a pull-request-only
`--changed origin/${{ github.base_ref }}`, which additionally makes the
code rules errors for blocks overlapping the diff.

**This lands only now because only now does it pass.** The tree is at 0
errors and 840 warnings; the warnings are the 804-block using-directive
debt plus the 36 untagged fences, both deliberate and both P1. Wiring the
linter in before the sweeps would have meant a red build for four phases
and a habit of ignoring it — which is the failure the audit measured in
the first place, every unenforced CLAUDE.md rule having quietly decayed.

**The `--changed` step is checked to fire rather than assumed to.** Run
locally against origin/master this branch reports 0 errors, which has the
same shape as a vacuous pass, so it was taken apart: `changed_ranges`
returns 118 files and 465 hunks, so the ranges are real. The branch's
edits are banner lines and heading lines and genuinely overlap no C#
block. Editing one line inside the using-less block at
`AWSSQSConfiguration.md:29` then turns exactly that block into an error
— exit 1 — while the other ten blocks on the same page stay warnings.
That is the block granularity the rule promises, demonstrated in both
directions. Task 5.2 still owes the same check on a real
`pull_request` event, where the checked-out ref is a merge commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
011 owns this workflow and 009's design § D9 says plainly that the version
gate **adds a step to it** rather than creating a second workflow. Building
the shape now costs a guarded no-op; retrofitting it later costs a
restructure while another spec is mid-flight.

Two requirements come from 009 and both are met here:

- **A daily schedule.** The event that invalidates a pinned version is a
  release in *another* repository, so push and pull_request alone would
  leave a stale pin undetected until someone happened to touch the docs.
  The `check` job runs on the schedule too, which is harmless — the tree
  either still lints or it does not.
- **Exit 2 is not a pass.** An unreachable NuGet is an unchecked pin. The
  guard is a plain `if [ -f ]` around a bare invocation, so the step's
  exit code is versioncheck.py's own. Verified under `bash -e {0}`, which
  is what Actions uses: absent → 0, and 0/1/2 propagate unchanged. A
  `continue-on-error` or a `|| true` would have quietly broken that, which
  is why neither is here.

The guard carries a REMOVE THIS comment naming 009 D9. A guard that is
inherited rather than removed un-gates the check silently, which is the
same class of failure as the vacuous `--changed` pass Task 5.2 exists to
rule out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
`--changed origin/${{ github.base_ref }}` expanded the ref directly into
a `run:` command line. Git ref names may contain `;`, `&` and `$` —
check-ref-format rejects spaces and `~^:?*[\` but not those — so this is
a shell-injection shape, even though reaching it needs write access to
create the branch a pull request is then based on. Low risk, but the fix
costs three lines and the pattern is worth not having in the repo's only
workflow.

Behaviour is unchanged: quoted, `origin/$BASE_REF` reaches pagelint.py as
the single argument it did before. Verified locally against master.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
Tasks 5.1, 5.2 and 5.3 ticked; 26 of 43. **AC6 is closed**: both tools run
in CI and either one failing fails the build.

The write-up is § *Phase 5 as executed*, and the part worth keeping is
Task 5.2. Run locally, `--changed` reported 0 errors — the exact shape of
the vacuous pass the task exists to rule out. Taken apart rather than
accepted: `changed_ranges` returns 118 files and 465 hunks, so the ranges
were real and this branch simply touches no C# block. The positive
direction was then forced, first locally and then on PR #73, a
one-character probe closed unmerged as soon as its run was read.

What that run settled, and what no local check could:

- on a `pull_request` event, where the checked-out ref is a merge commit,
  `origin/<base_ref>` **does** resolve. **The contingency `git fetch
  origin ${{ github.base_ref }}` is not needed and was not added.**
- the run went red on `AWSSQSConfiguration.md:29` alone — the other
  fifteen blocks on that page stayed warnings — while the repo-wide step
  stayed green at 0 errors in the same run. The two strictness levels are
  independent in practice, not only in design.

Also recorded: the `versions` guard was checked to propagate exit codes
under `bash -e {0}`, including **2**, which 009 requires not be a pass;
and why `base_ref` was moved off the command line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
… (spec 011)

Rule 6's message has always offered two remedies -- "Add them, or mark the
omission with `// ...`" -- and CLAUDE.md's *Complete code blocks* lists `// ...`
among the three parts a tool can check. `check_code_blocks` implemented only the
first. The escape it advertised did nothing.

That gap blocks Phase 6. Design §10 says the demonstrator splits move C# blocks
verbatim, `using` directives or not, reasoning that rewriting them would oblige
backfill under `--changed`. The premise is wrong in the other direction: a new
page is 100% added lines, so every moved block already overlaps the diff and is
already strict. Proven with a throwaway page and `git add -N` -- one
moved-verbatim block, one error, exit 1. Across the two split candidates that is
42 using-less blocks, i.e. 42 CI errors the moment a PR opens.

So `// ...` now downgrades the finding to a warning when strict, and never
silences it: the block still counts towards the debt and its message says the
omission is declared, not fixed. Ruled by the maintainer over backfilling the 42
blocks (contradicts §10, and risks naming wrong namespaces) and over exempting
new files wholesale (a permanent hole -- any new page could ship using-less
blocks forever, which is the decay this spec exists to stop).

Verified in all three directions:

- repo-wide baseline unchanged, 0 errors / 840 warnings / 804 blocks across 89
  pages -- strict_ranges is empty there, so severity was already warning
- an elided block under `--changed` is a warning and still counted
- a bare block under `--changed` is still an error, still exit 1

CLAUDE.md's ledger row and *Complete code blocks* both updated, so the
linter/CLAUDE.md parity AC5 asks for holds in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
The first demonstrator split. 566 lines interleaving all four Diataxis modes
become a 331-line Reference core that keeps the original file name -- so the
most-linked URL does not move -- plus three single-mode pages:

  RabbitMQDurability.md              Explanation   why quorum queues and
                                                   persistence exist, what each
                                                   costs
  RabbitMQMigrateToQuorumQueues.md   How-to        moving an existing
                                                   subscription, and enabling
                                                   persistence alongside it
  RabbitMQConnectionStability.md     How-to        retry, heartbeats, blocked
                                                   connections, monitoring

Moved verbatim, per design §10. The reduced core was built by slicing the
original by line number rather than retyping it, so every kept and lifted line is
byte-identical. The Reference core also absorbs the configuration tables the
design asks it to -- quorum requirements and validation, the persistence
producer/consumer settings and complete example, the connection reliability
knobs, and the ack/nack semantics -- so every knob is in one place.

Task 6.5, no information loss, checked mechanically rather than by eye. Of the
original's substantive lines, exactly 24 are not present verbatim across the four
pages: one anchor deliberately repointed (`#persistent-messages` ->
`#rabbitmq-persistence-options`, renamed in this same pass) and the 23 items of
the four `Best Practices` sections, which design §6a folds rather than moves
because "guidance" is not a page type. Where each of the 23 went:

- Quorum Queue Best Practices (5) -> Durability § When to Use Quorum Queues;
  items 4 and 5 merge, both being cluster capacity
- Persistent Message Best Practices (6) -> Durability § When to Use Persistent
  Messages (1, 2, 5) and § Persistence Performance Considerations (3); the two
  actionable ones, TTL and rehearsing recovery, become steps in the migration
  how-to § Enabling Persistent Messages (4, 6)
- Best Practices for Connection Stability (6) -> Connection Stability, each as
  the step that implements it rather than a list at the end
- Best Practices for Blocked Connections (6) -> Connection Stability § Handling
  Blocked RabbitMQ Connections and § Monitoring RabbitMQ Connection Events

None survives as a standalone `## Best Practices`, which removes 4 of that
heading's 26 instances.

One deliberate departure from Task 6.3, which listed "Ack and Nack behaviour"
under the migration how-to. The section is a general description of Brighter's
consumer semantics with no migration content, so putting it on a how-to page
re-mixes the modes this split exists to separate. It stays in the Reference core
as `## RabbitMQ Ack and Nack Behaviour`; the how-to's new drain step links to it,
which is where the migration-specific point actually lives.

Two bare fences carrying log output became ```text, so rule 4's untagged count
falls 36 -> 34 and neither becomes a strict error on the new page.

Verified: linkcheck clean at 110 files including the orphan check; pagelint 0
errors repo-wide and 0 under `--changed origin/master`. The strict run is not
vacuous -- changed_ranges reports the three new pages at 126, 82 and 144 lines
and RabbitMQConfiguration.md at 19 hunks -- and the `// ...` markers are
load-bearing: removing one turns its block into an error and the build red.
Debt unchanged at 804 blocks, so no code block was gained or lost in the move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
…7-6.11)

The second demonstrator split, and the largest and worst-mixed page in the repo.
1,070 lines become a 237-line How-to core that keeps the original file name --
so its 6 inbound `#configuring-the-dispatcher` links do not move -- plus two
single-mode reference pages:

  CommandProcessorConfigurationReference.md   Reference   673 lines
  DispatcherConfigurationReference.md         Reference   234 lines

Moved verbatim, per design §10, by slicing the original by line number. The only
transforms applied to moved text are the three the tasks call for: headings
promote one level and shed their bold (Task 6.7 -- slug() strips emphasis, so
every anchor survives both changes untouched), C# blocks with no `using`
directives gain a `// ...` marker, and links whose target changed file are
repointed.

CommandProcessorConfigurationReference.md at 673 lines exceeds CLAUDE.md's
500-line guidance, correctly and per Task 6.8: it is single-mode reference
material, and splitting a parameter list by size is the error requirements §
Mode mixing warns against.

The How-to core gains the one thing it did not have -- the path that works.
`## Configuring The Command Processor` had been a bare heading over 630 lines of
reference dump; it now carries a minimal `AddBrighter()` / `AutoFromAssemblies()`
example with real `using` directives, and links out for every option. Checked
against `ServiceCollectionBrighterBuilder.cs:116` rather than assumed, which
caught a wrong claim in the first draft: `AutoFromAssemblies()` does not scan
"the calling assembly if you pass none". It always scans every non-framework
assembly in the AppDomain, and the argument is additive.

Task 6.10, the fold into V10MigrationGuide.md, is "no information loss, but no
duplication either" applied in both directions. `### 2. Simplified Configuration`
already carried richer before/after code for the same two renames, so the Quick
Migration Guide's snippets were dropped rather than duplicated; the V9->V10
method table with its Purpose column and the Dispatcher terminology note were
new to the guide and were absorbed.

25 inbound links across 20 pages repointed, plus 3 stranded on the new Dispatcher
page whose "as discussed above" no longer had an above. Where a link's text named
the old page it was renamed with its target, so no link now says "Basic
Configuration" while pointing somewhere else. `#configuring-the-dispatcher` is
deliberately absent from the repointing map: its H2 stays behind when its
children move, which is the whole reason the children could move at all.

No information loss, checked mechanically: of the original's substantive lines,
exactly 4 are not present verbatim across the four pages -- the 3 rewritten
stranded links, and the fold's lead-in sentence, which the migration guide
already states as "Builder methods renamed for clarity".

Verified: linkcheck clean at 112 files including the orphan check; pagelint 0
errors repo-wide and 0 under `--changed origin/master`. Debt falls 804 -> 802,
which is exactly the two dropped Quick Migration Guide blocks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
…11, Task 6.12)

Task 6.12 asked whether the splits move any page URL. They do not: both keep the
core under its original file name, so the two most-linked URLs are unchanged, and
the five new pages are new URLs with nothing to redirect from. What broke was
anchor-level inbound links, which GitBook redirects cannot address -- they
operate on pages, not fragments -- and which were fixed by repointing the links
themselves. The new pages' URLs derive from SUMMARY.md placement, which Spec 010
changes immediately afterwards, so their redirects belong to 010. So the honest
output is a recorded finding, not a no-op stanza.

The zero-width spaces were worse than recorded. The key was literally
`​structure`, not `structure`, so GitBook has never read that block and has
been falling back to its defaults -- which happen to name README.md and
SUMMARY.md, which is why nothing ever looked broken. The value was
`SUMMARY.md​` to match, so fixing the key alone would have turned a
silently-ignored block into a silently-broken one. Both characters and the
trailing double-spaces are gone, verified by byte inspection.

Phase 6 recorded in tasks.md: all 12 tasks ticked, 38/43 overall. The write-up
carries the rule 6 finding, the two mechanical no-information-loss checks, the
one deliberate departure from Task 6.3, and the two count movements (debt 804 ->
802, untagged fences 36 -> 34).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
… (spec 011)

Found by adding the five Phase 6 split pages to pagetypes.tsv, which they were
missing from -- so a future `APPLIES_TO` bump would have re-run the sweep across
105 pages and silently skipped the 5 newest. Adding them exposed a worse bug
immediately: the very next run stripped the Prerequisites segment from all five.

The script builds a banner from `verdict` and `applies` only, then overwrites
whatever banner it finds. Its docstring says prerequisites are "omitted", which
was accurate while no page in the corpus exercised the segment -- and PROMPT.md
recorded the sweep as "safe and idempotent" on exactly that basis. The two splits
made five pages that do exercise it, and the claim silently stopped being true.

So an existing ` · Prerequisites: ...` segment is now carried across a rewrite.
The script still never authors one -- choosing prerequisites is a per-page
judgement that cannot be made mechanically, which is the original reasoning and
still holds -- but not authoring is different from deleting.

Proven both ways, because "110 unchanged" on its own only shows the run did
nothing:

- a forced verdict change (Explanation -> Reference on RabbitMQDurability.md)
  rewrites the banner and the prerequisite survives it
- with the verdict restored, all 110 pages report unchanged and `git diff` on
  contents/ is empty

The five rows are appended rather than merged into the existing order, which is
not sorted by any single rule (runs of 28/13/1/63 by proposed-ness), so appending
touches no reviewed row: 5 insertions, 0 deletions.

pagelint 0 errors / 836 warnings across 110 pages; linkcheck clean at 112 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
D8, the artifact Spec 010 executes against. Written to stand alone: 010
needs the conventions (Task 1.3) and this file, and both now exist, so
nothing in 011 blocks another spec.

Seeded from the >=3-mode cohort and extended where the score was the
wrong signal. 12 of the 42 rows say `keep` -- a list of only split
candidates would invite 010 to re-open every page it omitted.
FAQ.md, Glossary.md and V10MigrationGuide.md are recorded as outside
Diataxis and explicitly not split candidates.

The cohort reconciles exactly, and one prediction in it was wrong.
modemix.py now reports 30 pages at >=3 modes and 13 at four, against
the approved 31 and 14. Re-scoring every page at 335f078 and diffing
against HEAD shows only the seven split-affected pages changed at all:
Phase 4 qualified 260 headings across 74 files and moved no page's mode
score. BrighterBasicConfiguration.md scored 2, not 3, so it was never in
the 31 -- it was split on being 1,070 lines of two different jobs, which
the score never showed. The sixth of the design's figures to be wrong.

The page-type tally was stale in two places, and pagetypes.tsv was right
throughout. classification-notes.md section 11 recorded 48/30/27 and
PROMPT.md carried it to 50/32/28; the truth is 50/33/27. The missing
move is section 10's own QueryPipeline.md Explanation -> How-to
correction: the tally was written before it was applied and never
re-derived. Nothing ever disagreed with anything, which is why it
survived four sessions. Corrected in both places, with the awk one-liner
that re-derives it.

Three findings that changed rows:

- The scheduler family is six pages, not seven. Section 6 counts
  PostgreSQLMessageBroker.md among them; it is a transport
  (SUMMARY.md:72) sharing the template by coincidence.
- Its six migration sections are near-copies -- all "swap the factory"
  -- and its six comparison sections duplicate BrighterSchedulerSupport.md's
  existing "Choosing a Scheduler". So the family resolves to 5 references
  + 1 shared how-to + 1 enriched overview, not 18 pages.
- Phase 6's own split created an overlap nothing checks for:
  HowServiceActivatorWorks.md:147 (76 lines) now duplicates
  DispatcherConfigurationReference.md, created three commits earlier.

Three content defects recorded rather than fixed, being out of scope
here: SweeperCircuitBreaking.md:16 reads "How It Work";  the Dispatcher
overlap above; and QueriesAndQueryObjects.md:746 "Query Patterns" sits
alongside a whole QueryPatterns.md page.

linkcheck.py clean at 112 files; pagelint.py 0 errors, 836 warnings
across 110 pages. No page under contents/ was touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg
@iancooper
iancooper merged commit b5500bb into master Aug 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant