Page banners, qualified headings, and a linter that gates the build (spec 011) - #74
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdhas carried authoring rules for years with no CI tocheck them, and 14%
using-directive compliance is what that looks like after a coupleof 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.
## Configurationand## Best Practiceseach 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-pageheadings 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:
RabbitMQConfiguration.mdRabbitMQDurability.md+RabbitMQMigrateToQuorumQueues.md+RabbitMQConnectionStability.mdBrighterBasicConfiguration.mdCommandProcessorConfigurationReference.md+DispatcherConfigurationReference.mdBoth 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.pygates the build. Six rules, stdlib only, single file, same exitcontract as
linkcheck.py. Repo-wide it reports 0 errors; on a pull request it alsoruns
--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 across93 pages) and 34 untagged fences. Both are deliberate, counted, and scheduled.
Things worth a reviewer's attention
vocabulary as
Brighter V10 | Darker V10 | Brighter and Darker V10, assuming Darkertracks Brighter's version line. It does not —
Paramore.Darker's latest release is4.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 inpagelint.pyand imported everywhere else.three duplicate-content pairs.
Glossary.mddefinesDispatcherandCloudEventsonce each; one cited line number points into an unrelated
Timeoutentry. Whatactually 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'sstructure:key contained a zero-width space, so GitBook hasnever 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.
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 awarning and never silences, so the debt stays counted.
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.mdlists 42 pages with a verdict for therestructure that follows, and 12 of those rows say
keep— a list of only splitcandidates would invite the next spec to re-open every page it omitted.
FAQ.md,Glossary.mdandV10MigrationGuide.mdare recorded as legitimately outsideDiátaxis and explicitly not split candidates.
Still open in 011: the language-tag backfill,
--fixforpagelint.py, the acceptancepass, and a rendered-preview check that needs this branch published to GitBook.
#67 stays open until the whole programme lands.
Verification
The gate was proven to fail before it was trusted to pass.
--changedreporting 0errors 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 realpull_requestevent.🤖 Generated with Claude Code
https://claude.ai/code/session_012tcdwxVb8NmKaX2S6fvyFg