From faeac681fda8d414f9e46ecc833216964d24782f Mon Sep 17 00:00:00 2001 From: iancooper Date: Tue, 25 Aug 2026 00:10:32 +0100 Subject: [PATCH] =?UTF-8?q?spec:=20009=20Phase=203=20=E2=80=94=20rung=201,?= =?UTF-8?q?=20and=20the=20code=20on=20the=20page=20is=20the=20code=20that?= =?UTF-8?q?=20ran?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D1. `contents/TutorialFirstCommand.md`, the corpus's first `Tutorial`-typed page: a command dispatched to a handler in-process, no broker, no Docker. Task 3.1 settled `host.Run()` by observation rather than guessing. The sample prints `Hello Ian`, then `Application started. Press Ctrl+C to shut down.`, and sits there — still running at 45 seconds. `AddBrighter().AutoFromAssemblies()` registers no `IHostedService`; the only two in the DI package come from the opt-in `BrighterPipelineValidationExtensions`. So the page drops that one line and says why the sample keeps it. No change was made to Brighter. The near-miss on the way: `git grep "host.Run()"` matched one sibling of three, and the finding drafted from it — "HelloWorld is an outlier" — was false. The other two end `await host.RunAsync()`. One term, two spellings, exactly Phase 2's `Box Provisioning` against `BoxProvisioning`, one phase later. Task 3.3's assertion is the point, because a repo-wide `0 errors` is a silence. `BANNER_RE` was called directly and asserted to match with group(1) == 'Tutorial' — the vocabulary alternative that had never fired on a real page. Then two red-proofs on the page itself, each asserting the mutation produced the input the branch rejects before the tool ran: a type out of vocabulary gives BANNER MALFORMED, a deleted banner gives BANNER MISSING, both exit 1. Restored from a copy taken aside and asserted byte-identical. Task 3.4's first measurement was wrong and looked right. `NUGET_PACKAGES` moves the global packages folder but not the HTTP cache, so a "cold" restore of 4.5s was served locally while the directory really did go 0 → 195 MB. Redone with all four cache locations redirected and each verified empty: restore 4.5s → 6.2s, http-cache 0 → 113 files, total machine time 10.9s. An empty cache is a claim about one directory; cold is a claim about four. The three `csharp` blocks were then extracted from the markdown and compared byte for byte against the `.cs` files that were compiled and run — identical — and the `xml` block against the `` the three `dotnet add package` commands generated. Four reader-facing claims were checked against the Brighter source; two were wrong as drafted, including a promise about rung 2 that rung 2 would have broken. Design's D1 outline links rung 2, which does not exist yet and would have failed linkcheck with MISSING FILE. Link down the ladder, never up — Phases 6, 8 and 11 inherit this. Gates: linkcheck clean at 145 files; pagelint 0 errors, 790 warnings, 144 pages — the using-directive debt unmoved, since all three C# blocks carry their own directives and none uses the `// ...` escape; `--changed origin/master` reached 7 code blocks strict, this spec's first non-vacuous strict run; both urlmap checks 0. Tasks re-derived, not incremented: 11 done, 28 open, 39 total. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 11 + SUMMARY.md | 1 + contents/TutorialFirstCommand.md | 222 +++++++++++++++++++ spec/009-getting_started_tutorials/tasks.md | 181 ++++++++++++++- spec/011-authoring_conventions/pagetypes.tsv | 1 + 5 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 contents/TutorialFirstCommand.md diff --git a/CLAUDE.md b/CLAUDE.md index d2c5850..e250ebe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,6 +74,17 @@ headings now carry their subject. `## Configuration` appears on 26 pages, which makes every one of them a worse search result and every extracted chunk unattributable. See [Heading qualification](#heading-qualification). +**Tutorial pages use `## Step N: …` headings in place of step 4's skeleton**, and that +is a convention rather than an exception to be tidied away later. A tutorial is a +sequence a reader executes once, not a reference they consult; Key Concepts / +Configuration / Best Practices imposes a shape that fights the order the reader needs. +The steps take everything else in the pattern — H1, banner, introduction, then +*Further Reading* — and they satisfy [heading qualification](#heading-qualification) +on their own terms, because a step heading names what that step does +(`## Step 4: Wire Up Brighter`) and is unique across pages. This seeks no exemption +from any rule in the [ledger](#the-ledger); it records why these pages look different. +`contents/TutorialFirstCommand.md` is the first of them. + ### SUMMARY.md Management **Critical:** Always update SUMMARY.md when adding or reorganizing documentation. diff --git a/SUMMARY.md b/SUMMARY.md index 23345c5..625b390 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,5 +1,6 @@ ## Get Started +* [1. Your First Command](/contents/TutorialFirstCommand.md) * [Why Brighter?](/contents/WhyBrighter.md) * [Basic Concepts](/contents/BasicConcepts.md) * [Show me the code!](/contents/ShowMeTheCode.md) diff --git a/contents/TutorialFirstCommand.md b/contents/TutorialFirstCommand.md new file mode 100644 index 0000000..43e7a40 --- /dev/null +++ b/contents/TutorialFirstCommand.md @@ -0,0 +1,222 @@ +--- +description: "Build a .NET console app that sends a command to its handler through Brighter's Command Processor, in-process, with no broker and no Docker." +layout: + description: + visible: false +--- + +# Your First Command + +> **Tutorial** · Applies to **Brighter V10** + +Build a .NET console app that sends a [command](/contents/Glossary.md#command) to its +[handler](/contents/Glossary.md#handler) through Brighter's +[Command Processor](/contents/Glossary.md#command-processor), in-process, with no broker and +no Docker. + +This is the first rung of the ladder. Everything here runs in one process, so you can see +what Brighter does to a request before any messaging is involved. The rungs above add a +broker, then durability, then streaming — each one a small delta on what you build here. + +## What You'll Build: Your First Command + +A single console application with three files: + +| File | What it is | +|---|---| +| `GreetingCommand.cs` | the request — an instruction to do something, carrying a name | +| `GreetingCommandHandler.cs` | the code that runs when that command is sent | +| `Program.cs` | wiring: register Brighter, resolve the Command Processor, send | + +When you run it, the handler prints `Hello Ian` and the program exits. + +## Before You Start Your First Command + +- **The .NET 9 SDK.** Check with `dotnet --version`. +- **No Docker, and no broker.** Rung 1 is deliberately in-process. +- **About ten minutes**, nearly all of it reading and typing. The machine work — create, + restore, build, run — measured **11 seconds** on a clean machine with an empty NuGet + package cache. If your restore takes minutes rather than seconds, the problem is your + package feed, not this tutorial. + +## Step 1: Create the Console Project + +```bash +dotnet new console -n HelloWorld -f net9.0 +cd HelloWorld +dotnet add package Paramore.Brighter --version 10.7.0 +dotnet add package Paramore.Brighter.Extensions.DependencyInjection --version 10.7.0 +dotnet add package Microsoft.Extensions.Hosting --version 9.0.0 +``` + +`Paramore.Brighter` carries `Command`, `RequestHandler` and `IAmACommandProcessor`. +`Paramore.Brighter.Extensions.DependencyInjection` carries `AddBrighter`. +`Microsoft.Extensions.Hosting` gives you the generic host that owns the service container. + +**Expected result:** `HelloWorld.csproj` now contains these three references, and no others. + +```xml + + + + + +``` + +> **This is the one place the page and the sample differ on purpose.** The working sample +> this tutorial is drawn from — +> [`samples/CommandProcessor/HelloWorld`](https://github.com/BrighterCommand/Brighter/tree/master/samples/CommandProcessor/HelloWorld) +> — references Brighter by `ProjectReference` into the source tree, because it is built +> inside that repository. You are installing released packages, so you pin a version. The +> C# below is otherwise the sample's code, unchanged. + +## Step 2: Define the Greeting Command + +A command is an instruction to do one thing, addressed to one handler. Derive it from +`Command`, which supplies the identity Brighter uses to trace the request through the +pipeline. + +```csharp +using Paramore.Brighter; + +namespace HelloWorld +{ + public sealed class GreetingCommand(string name) : Command(Id.Random()) + { + public string Name { get; } = name; + } +} +``` + +Put this in `GreetingCommand.cs`. + +**Expected result:** the project still builds — `dotnet build` reports `0 Error(s)`. + +## Step 3: Write the Greeting Command Handler + +A handler is the target of exactly one request type. Deriving from +`RequestHandler` is what tells Brighter this class handles that command; +you do not register it anywhere by hand. + +```csharp +using System; +using Paramore.Brighter; +using Paramore.Brighter.Logging.Attributes; + +namespace HelloWorld +{ + public sealed class GreetingCommandHandler : RequestHandler + { + [RequestLogging(step: 1, timing: HandlerTiming.Before)] + public override GreetingCommand Handle(GreetingCommand greetingCommand) + { + Console.WriteLine($"Hello {greetingCommand.Name}"); + + return base.Handle(greetingCommand); + } + } +} +``` + +Put this in `GreetingCommandHandler.cs`. + +Two details worth naming now, because every rung above reuses them: + +- **`return base.Handle(greetingCommand)`** passes the request to the next step in the + pipeline. Return anything else and you truncate the pipeline. +- **`[RequestLogging(...)]`** is an attribute that inserts a logging step *before* your + handler runs. That is Brighter's middleware in miniature: attributes on the handler + method build a pipeline around it. See + [Building a Pipeline of Request Handlers](/contents/BuildingAPipeline.md). + +**Expected result:** still `0 Error(s)`. Nothing runs yet — nothing has sent the command. + +## Step 4: Wire Up Brighter + +```csharp +using HelloWorld; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; + +var builder = Host.CreateApplicationBuilder(); +builder.Services.AddBrighter().AutoFromAssemblies(); +var host = builder.Build(); + +var commandProcessor = host.Services.GetRequiredService(); + +commandProcessor.Send(new GreetingCommand("Ian")); +``` + +Replace the contents of `Program.cs` with this. + +`AddBrighter()` registers the Command Processor and its dependencies. +`AutoFromAssemblies()` scans the assemblies loaded in the current application — skipping +`System.*`, `Microsoft.*` and Brighter's own — for classes deriving from `RequestHandler`, +and registers each against the request type in its generic argument. That is how +`GreetingCommandHandler` is found without you naming it anywhere. + +`Send` dispatches to exactly one handler and is synchronous: it returns when the handler +has finished. Use `Publish` when you want an [event](/contents/Glossary.md#event) delivered +to every subscriber instead, and `Post` when you want it sent over a broker — see +[Dispatching Requests](/contents/DispatchingARequest.md). + +> **The sample ends with `host.Run()` and this page does not.** `host.Run()` blocks until +> the process is signalled, and `AddBrighter()` on its own registers no hosted service, so +> here it would leave you at a prompt that never returns. Keep it when your app has +> something to host — a consumer, a web server — which is what the sample's siblings do. + +## Step 5: Run Your First Command + +```bash +dotnet run +``` + +**Expected result** — three log entries, then the greeting, then your shell prompt back: + +```text +info: Paramore.Brighter.CommandProcessor[148105941] + Building send pipeline for command: HelloWorld.GreetingCommand 01a03600-c575-7836-841d-111f5a2e43fa +info: Paramore.Brighter.CommandProcessor[780428052] + Found 1 pipelines for command: HelloWorld.GreetingCommand 01a03600-c575-7836-841d-111f5a2e43fa +info: Paramore.Brighter.Logging.Handlers.RequestLoggingHandler[600284706] + Logging handler pipeline call. Pipeline timing Before target, for HelloWorld.GreetingCommand with values of {"name":"Ian","correlationId":null,"id":"01a03600-c575-7836-841d-111f5a2e43fa"} at: 08/24/2026 23:00:15 +Hello Ian +``` + +The identifier and the timestamp differ on every run; everything else is fixed. The process +exits with code `0`. + +If you see `Found 0 pipelines`, `AutoFromAssemblies()` did not find your handler — check +that `GreetingCommandHandler` derives from `RequestHandler` and is in the +same assembly as `Program.cs`. + +## What Your First Command Showed You + +You sent a request and Brighter delivered it to a handler you never registered. Three things +did that work, and all three scale up unchanged: + +- **The Command Processor is the only thing you call.** Your code never names a handler. + Which method you call decides how the request travels — `Send` here, `Post` once there + is a broker — but the handler is always reached the same way. +- **Handlers are found by their generic argument.** `RequestHandler` is + both the declaration and the registration. +- **The pipeline is built per request, not once per application.** `Building send pipeline` + appears on every `Send`, because `Send` constructs the chain — the logging step, then your + handler — and runs it. Attributes decide what goes into it. + +What you have not got yet is durability or another process. Nothing left this application, +so nothing survived it. That is the next rung, which sends the same shape of request over +RabbitMQ to a consumer running separately. + +## Further Reading + +- [Basic Concepts](/contents/BasicConcepts.md) — commands, events and requests, defined +- [Building a Pipeline of Request Handlers](/contents/BuildingAPipeline.md) — what + `[RequestLogging]` is an instance of +- [Basic Configuration](/contents/BrighterBasicConfiguration.md) — what + `AddBrighter()` accepts beyond the defaults +- [Dispatching Requests](/contents/DispatchingARequest.md) — `Send`, `Publish` and `Post` + compared +- [Glossary](/contents/Glossary.md) — every term this page linked, and the rest diff --git a/spec/009-getting_started_tutorials/tasks.md b/spec/009-getting_started_tutorials/tasks.md index d04f13f..f57ea7c 100644 --- a/spec/009-getting_started_tutorials/tasks.md +++ b/spec/009-getting_started_tutorials/tasks.md @@ -7,8 +7,9 @@ moved. See § *Re-derive the total* and Task 3.5. applied) and `requirements.md` (approved 2026-08-03) **Executes against:** a corpus that **Spec 010 moved after this spec was approved** — see §2. -**Total tasks: 39, across 12 phases. 6 done — Phases 1 and 2 complete, 2026-08-24.** -Re-derived, not incremented: `grep -c '^- \[x\] \*\*Task'` says 6 and `'^- \[ \]'` says 33. +**Total tasks: 39, across 12 phases. 11 done — Phases 1, 2 and 3 complete, 2026-08-24.** +Re-derived, not incremented: `grep -c '^- \[x\] \*\*Task'` says 11 and `'^- \[ \] \*\*Task'` +says 28. The phase table's Tasks column still sums to **39** independently. --- @@ -542,24 +543,105 @@ unchanged — 3 files, 114 lines, verified present today — which is what makes rung nothing upstream can stall. If the sample turns out to need a change, that is a finding to record, not a change to make quietly. -- [ ] **Task 3.1:** Run `HelloWorld` and settle the `host.Run()` question by observation +- [x] **Task 3.1:** Run `HelloWorld` and settle the `host.Run()` question by observation — + **DONE 2026-08-24. Decision: the page drops it.** - Input: `../Brighter/samples/CommandProcessor/HelloWorld/` - Output: a recorded decision — drop `host.Run()` in the page's version and explain why the sample keeps it, or tell the reader to press Ctrl+C - Notes: design says **decide at writing time by running it; do not guess**. A tutorial whose last step leaves the reader at a hung prompt has failed at the last step, which is the failure mode this spec exists to prevent. - -- [ ] **Task 3.2:** Write `contents/TutorialFirstCommand.md` + - **Observed, not inferred.** `dotnet run` in the sample printed the pipeline lines, then + `Hello Ian`, then `Application started. Press Ctrl+C to shut down.` and **sat there** — + still running at 45 seconds, killed by hand. The greeting arrives *before* the host + starts, so `Send` completes and `host.Run()` contributes nothing but the block. Design's + worry was right. + - **Why it contributes nothing, checked in the source rather than assumed:** + `AddBrighter().AutoFromAssemblies()` registers **no `IHostedService`**. The only two in + `Paramore.Brighter.Extensions.DependencyInjection` are `BrighterValidationHostedService` + and `BrighterDiagnosticHostedService`, and both are registered from + `BrighterPipelineValidationExtensions.cs:96` and `:129` — **opt-in methods `AddBrighter` + does not call.** So the host is asked to run with nothing to run. + - **The near-miss, and it is Phase 2's lesson wearing different clothes.** Asking which + siblings share the shape, `git grep "host.Run()"` over + `samples/CommandProcessor/**/Program.cs` matched **one file of three** — and the draft + finding written from that was *"`HelloWorld` is an outlier among its own siblings"*, which + is **false**. `HelloWorldAsync` and `HelloWorldInternalBus` both end + `await host.RunAsync()`. One term, two spellings, and the query was well-formed and + answering a different question — exactly `Box Provisioning` against `BoxProvisioning` one + phase earlier. **Reading the two files settled in ten seconds what the grep had got + backwards.** + - **So the page's sentence is the true one:** all three samples run the host, and + `HelloWorldInternalBus` genuinely needs to, because it registers + `ServiceActivatorHostedService` and consumes. `HelloWorld` inherits the shape from a + sibling that needs it. The page keeps the sample's code to the line and drops that one + line, saying why — **the second documented page/sample divergence, alongside the + `.csproj`** (standing obligation 7). **No change was made to Brighter**, per this phase's + *must not*. + +- [x] **Task 3.2:** Write `contents/TutorialFirstCommand.md` — **DONE 2026-08-24** - Input: design § D1 (outline, five code examples, glossary links); the sample's three `.cs` files - Output: `contents/TutorialFirstCommand.md`, ~180 lines - Notes: blocks start at the first `using`, below the ~25-line MIT `#region Licence` — AC3's documented exception. The `.csproj` divergence gets its one line. Link `#command`, `#handler`, `#command-processor`; all three resolve (§2.2). - -- [ ] **Task 3.3:** Land the page's `SUMMARY.md` entry, its `pagetypes.tsv` row, and assert - the `Tutorial` banner type actually passes + - **As shipped: 222 lines** (`len(text.splitlines())`, the house convention), **7 code + blocks — 3 `csharp`, 1 `xml`, 2 `bash`, 1 `text`.** Design estimated ~180. + Design's outline is followed step for step; the `xml` block is the extra one, and it earns + its place as Step 1's **expected result** (standing obligation 1 requires every step to + state one, and for "add three packages" the result *is* those three `PackageReference` + lines). The three C# blocks reproduce the sample below its licence region, to the line, + with the single `host.Run()` deletion of Task 3.1. + - **All three C# blocks carry real `using` directives and none uses the `// ...` escape**, + so the page adds **nothing** to the using-directive debt: it stayed at 790 across a page + count that went 143 → 144. AC1's baseline is unmoved by construction rather than by luck. + - **The pin was re-derived at writing time, as §2.5 insists, not carried from §2.5.** + `api.nuget.org/v3-flatcontainer/paramore.brighter/index.json`, highest non-prerelease: + **10.7.0**, and `paramore.brighter.extensions.dependencyinjection` agrees at 10.7.0. The + third package, `Microsoft.Extensions.Hosting 9.0.0`, is deliberately **not** a + `Paramore.Brighter` line, so D9 will leave it alone — design § D9 restricts its scan to + lines mentioning `Paramore.Brighter`, and this page is the first real test of that. + - **Design's D1 outline links rung 2 and rung 2 does not exist yet.** Written as outlined, + the page carried two `/contents/TutorialFirstMessage.md` links and would have failed + `linkcheck.py` with **MISSING FILE** — standing obligation 3 states the rule for a page's + *own* `SUMMARY.md` entry, and this is the same rule pointing outward. Both were rewritten + to name the next rung without linking it. **Every rung has this problem with the rung + above it**, so Phases 6, 8 and 11 each inherit it: link *down* the ladder freely, never + *up*. The upward links are Phase 9's to add, when `GetStarted.md` lists what exists. + - **The page's code was proved to be the code that ran, not merely code that looks like + it.** After the last edit, the three `csharp` blocks were extracted from the published + markdown and compared byte for byte against the three `.cs` files in the timed run's + working directory: **all three identical**. The `xml` block was compared against the + `` the three `dotnet add package` commands actually generated: **identical**. + This is the check AC2 and AC3 are really asking for. *"Test all code examples"* is + normally satisfied by having run something similar at some point; here the artefact that + compiled and the artefact that publishes are provably the same bytes, and the comparison + is one script that any later phase can re-run. + - **Four reader-facing claims were checked against the Brighter source rather than + asserted, and two of them were wrong as first drafted:** + - *"`AutoFromAssemblies()` scans the assemblies of your application"* — too vague to be + useful and slightly wrong. It scans `AppDomain.CurrentDomain.GetAssemblies()`, skipping + dynamic assemblies and anything named `System.*`, `Microsoft.*` or `Paramore.Brighter*` + (`ServiceCollectionBrighterBuilder.cs:116`). The page now says that, which is also what + makes the *"`Found 0 pipelines`"* troubleshooting line meaningful. + - *"`Send` is the entire API surface for dispatch … rung 2 swaps in a broker and this + line does not change"* — **false, and it was a promise about a page not yet written.** + Rung 2 uses `Post`. Rewritten to say what is true: your code never names a handler, and + the method you choose decides how the request travels. + - *"The pipeline is built per request, not per application"* — **true**, and worth + keeping because a reader can see it in the output. `Send` constructs a + `PipelineBuilder` inside a `using` and calls `Build` on every invocation + (`CommandProcessor.cs:317-321`). + - *"`Post` sends it over a broker"* — true, and the corpus already says so in those words + at `DispatchingARequest.md:249`, so the page links there rather than re-explaining. + - **Four link texts were retitled to match their targets' H1s** (`Building a Pipeline of + Request Handlers`, `Basic Configuration`, `Dispatching Requests` ×2). `linkcheck.py` + cannot see this — the targets resolved either way — and a link whose text does not match + the page it lands on is the sort of thing only reading catches. + +- [x] **Task 3.3:** Land the page's `SUMMARY.md` entry, its `pagetypes.tsv` row, and assert + the `Tutorial` banner type actually passes — **DONE 2026-08-24** - Input: §2.1's block; §2.3 - Output: one `SUMMARY.md` line under `## Get Started`; one appended `pagetypes.tsv` row; a green `pagelint.py` naming this page @@ -567,16 +649,79 @@ to record, not a change to make quietly. `> **Tutorial** · …`, so confirm rule 1 and rule 2 accept it rather than inferring it from a whole-repo `0 errors`. Run `--changed` with the page **staged**, and read the code-block count in its scope line. - -- [ ] **Task 3.4:** Clean-machine timed run (AC1, AC2) + - **Landed as §2.1 settled it**: `* [1. Your First Command](/contents/TutorialFirstCommand.md)` + is the **first** entry of the existing `## Get Started`, above the three orientation + pages; no section was created. `pagetypes.tsv` went **143 → 144 rows**, appended not + re-sorted, `verdict` = `Tutorial` and `applies` = `Brighter V10`. Section width went + 3 → 4 of 12, so S2 is untroubled, and `--check-shape` reports the tree at 143 pages. + - **The assertion, three ways, because a repo-wide `0 errors` is a silence.** + First, `BANNER_RE` was called **directly** on the literal banner string and asserted to + match with `group(1) == 'Tutorial'` — the vocabulary's never-fired alternative, fired. + Then two red-proofs on the page itself, each asserting the mutation produced *the input + the branch rejects* before the tool ran: `**Tutorial**` → `**Walkthrough**`, asserted + `BANNER_RE.match(...) is None`, gave **BANNER MALFORMED, exit 1**; the banner deleted + outright, asserted the first non-blank line after the H1 was no longer a banner, gave + **BANNER MISSING, exit 1**. Restored from a copy taken aside — never `git checkout --` — + and asserted **byte-identical**. + - **So the page is read, and `Tutorial` is accepted rather than skipped.** *"A rule that + never fires is invisible to everything but an enumeration"* is why this was worth five + minutes at the first page rather than a surprise at Phase 12. + - **An incidental confirmation worth keeping**: the banner-deleted proof also raised + **DESCRIPTION MISMATCH**, reporting the opening line as *"handler through Brighter's + Command Processor…"* — rule 7's extractor skipping the line after the H1, which on this + page is the banner and, with the banner gone, was the first line of a hard-wrapped + sentence. Exactly the documented behaviour, and it proves **rule 7 reads this page too**; + the unmutated page raises nothing, so the `description:` front matter equals the rendered + opening sentence. + - **Gates, with the page staged.** `linkcheck.py` clean at **145 files**; `pagelint.py` + **0 errors, 790 warnings, 144 pages**; both `urlmap.py` checks **0**. And + `--changed origin/master` reported **4 files, 4 hunks, 1 documentation page, 7 code + blocks strict** — **the first non-vacuous strict run this spec has had.** Phases 1 and 2 + reached 0 code blocks and said so; six of Spec 010's phases were vacuous for three + different reasons. All 7 blocks passed rule 6 on their own `using` directives. + - **Predicted URL, from the tool rather than guessed**: `get-started/tutorialfirstcommand` + (`python3 tools/urlmap.py | grep -i tutorialfirstcommand`). **Probe it once after the + sync, not in an until-loop** — a wrong path and an unsynced page are the same silence. + +- [x] **Task 3.4:** Clean-machine timed run (AC1, AC2) — **DONE 2026-08-24** - Input: the page as written - Output: a measured duration recorded in this document and reflected on the page - Notes: **follow the page's own `dotnet add package` lines, not the sample's project references** — that divergence is exactly what this run exists to exercise. Fresh clone, empty NuGet cache. Adjust the page to the measurement, never the reverse; the 10-minute figure is an estimate until this task replaces it. - -- [ ] **Task 3.5:** Write the `## Step N:` deviation into `CLAUDE.md` + - **Measured, following the page's own commands, against released 10.7.0 packages:** + + | Step | Cold | + |---|---:| + | `dotnet new console -n HelloWorld -f net9.0` | 2.8s | + | three `dotnet add package` | 6.2s | + | writing the three `.cs` files | 0.1s | + | `dotnet run` (cold build) | 1.9s | + | **total machine time** | **10.9s** | + + `0 Error(s)`, exit code **0**, and the process **returns to the prompt** — which is Task + 3.1's decision holding up under the reader's own path rather than the sample's. + - **`NUGET_PACKAGES` alone does not give you a cold run, and the first measurement was + wrong because of it.** Redirecting the global packages folder to an empty directory + produced a plausible **9.0s** total with **4.5s** of restore — and that restore was served + from the machine's **HTTP cache**, `~/.local/share/NuGet/http-cache`, which is a + *separate* location `NUGET_PACKAGES` does not touch. The directory really did go from 0 + files to 195 MB, so every cheap check agreed it was cold. Redone with all four locations + redirected (`NUGET_PACKAGES`, `NUGET_HTTP_CACHE_PATH`, `NUGET_SCRATCH`, + `NUGET_PLUGINS_CACHE_PATH`), each verified empty by `dotnet nuget locals all --list` + **before** the run: restore went 4.5s → **6.2s** and the http-cache went 0 → **113 files**, + which is the proof that bytes crossed the network. **An empty cache is a claim about one + directory; "cold" is a claim about four.** + - **What the number means for the page, which is not what design assumed.** 10.9 seconds of + machine time says the tooling is not the reader's cost — reading and typing is. So the + page keeps **"about ten minutes"** as the reader figure and, rather than asserting it, + states the measured 11 seconds beside it and turns it into a **diagnostic**: *if your + restore takes minutes rather than seconds, the problem is your package feed, not this + tutorial.* Design's *"time targets are measured, not asserted"* is honoured by publishing + what was actually measured, and by not passing off a machine timing as a reader timing. + +- [x] **Task 3.5:** Write the `## Step N:` deviation into `CLAUDE.md` — **DONE 2026-08-24** - Input: design § Style Notes; `CLAUDE.md` § *File Organization Pattern* - Output: a sentence in `CLAUDE.md` recording that tutorial pages use `## Step N: …` headings in place of the Key Concepts / Configuration / Best Practices skeleton, and why @@ -587,6 +732,18 @@ to record, not a change to make quietly. exception is at risk from every sweep run in between. The headings still satisfy rule 3 — they are subject-qualified and unique — so this records a convention, it does not seek an exemption. + - **Landed as a paragraph inside `CLAUDE.md` § *File Organization Pattern***, immediately + below the note explaining why headings carry their subject — the nearest place a reader + meets the skeleton it deviates from. It states what the deviation is, why a sequence + resists a reference skeleton, that the step headings satisfy heading qualification on + their own terms, that **no exemption from the ledger is sought**, and it names + `contents/TutorialFirstCommand.md` as the first page taking it. + - **The non-obvious half is what the deviation does *not* touch.** Everything else in the + pattern still applies — H1, banner, introduction, *Further Reading* unqualified from the + navigation allowlist — so the page is a normal page with steps in the middle, which is + why `pagelint.py` needed no change and why the ledger acquires no new row. Had this gone + unwritten, the risk was never that a sweep would break the page: it is that someone would + read `## Step 4: Wire Up Brighter` as an *unqualified* heading and "fix" it. --- diff --git a/spec/011-authoring_conventions/pagetypes.tsv b/spec/011-authoring_conventions/pagetypes.tsv index ef1c342..fd5a06c 100644 --- a/spec/011-authoring_conventions/pagetypes.tsv +++ b/spec/011-authoring_conventions/pagetypes.tsv @@ -141,3 +141,4 @@ contents/MigratingToNullableReferenceTypes.md How-to high "split from NullableRe contents/AgreementDispatcherRouting.md Explanation high "split from AgreementDispatcher.md (spec 010, Task 9.3); single-mode by construction" Explanation Brighter V10 contents/MigratingToPollyV8.md How-to high "split from PolicyRetryAndCircuitBreaker.md (spec 010, Task 9.4); single-mode by construction" How-to Brighter V10 contents/ConfiguringOpenTelemetry.md How-to high "split from Telemetry.md (spec 010, Task 9.5); single-mode by construction" How-to Brighter V10 +contents/TutorialFirstCommand.md Tutorial high "spec 009 D1, rung 1; tutorial by construction — one path, numbered steps, measured run" Tutorial Brighter V10