From f5962ffce935e930972bdf34994ed6e66566e889 Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 5 Oct 2026 08:14:09 +0100 Subject: [PATCH 1/4] =?UTF-8?q?spec:=20018=20compile=20residual=20?= =?UTF-8?q?=E2=80=94=20requirements=20approved,=20design=20drafted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Requirements re-derive 017's residual at 3a79b20 (990 / 299 / 674 / 17; 630 FAILED on the 64 pages no 017 tranche reached) and add 017's D3, the AWS V4 pin. Approved 2026-10-04. The design's probe (probe/run.sh, probe/v4pin.sh) shows only 10 of the 64 pages have at most one hard block, so repairs phase by section group; that a by-name second compile misreads 39 import rows and 29 DEFECTs; that the V4 pin works as a second project selected per page; and that AddServiceActivator is dead and shown as current on two pages. D1-D5 ruled by the maintainer 2026-10-04 and written into both documents. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjnH2pJ98aMabu1MyTpRxy --- spec/.current-spec | 2 +- .../.requirements-approved | 0 spec/018-compile_residual/README.md | 119 +++++ spec/018-compile_residual/design.md | 480 +++++++++++++++++ spec/018-compile_residual/probe/hardcat.py | 63 +++ spec/018-compile_residual/probe/offtable.py | 23 + spec/018-compile_residual/probe/run.sh | 18 + .../probe/tranche_pages.py | 6 + spec/018-compile_residual/probe/v4pin.sh | 23 + spec/018-compile_residual/requirements.md | 490 ++++++++++++++++++ 10 files changed, 1223 insertions(+), 1 deletion(-) create mode 100644 spec/018-compile_residual/.requirements-approved create mode 100644 spec/018-compile_residual/README.md create mode 100644 spec/018-compile_residual/design.md create mode 100644 spec/018-compile_residual/probe/hardcat.py create mode 100644 spec/018-compile_residual/probe/offtable.py create mode 100644 spec/018-compile_residual/probe/run.sh create mode 100644 spec/018-compile_residual/probe/tranche_pages.py create mode 100644 spec/018-compile_residual/probe/v4pin.sh create mode 100644 spec/018-compile_residual/requirements.md diff --git a/spec/.current-spec b/spec/.current-spec index 76b1cea..7b90b24 100644 --- a/spec/.current-spec +++ b/spec/.current-spec @@ -1 +1 @@ -017-compile_repairs +018-compile_residual diff --git a/spec/018-compile_residual/.requirements-approved b/spec/018-compile_residual/.requirements-approved new file mode 100644 index 0000000..e69de29 diff --git a/spec/018-compile_residual/README.md b/spec/018-compile_residual/README.md new file mode 100644 index 0000000..9583ac3 --- /dev/null +++ b/spec/018-compile_residual/README.md @@ -0,0 +1,119 @@ +# Spec 018: Compile Residual + +**Created:** 2026-10-04 +**Status:** Design Phase — `design.md` drafted 2026-10-04, awaiting `/spec:review` + +> **Re-derive this README before executing it.** It was written before anyone looked — check every +> count and every named gap against the tree, with the command beside the figure. + +## Topic Overview + +Spec 017 ended with **674 of 990** C# blocks that still do not compile against the released +packages. **630** of them are on the **64** pages that 017's tranches never reached. 018 picks up +017's residual sentence (`spec/017-compile_repairs/tasks.md` § *What 017 shipped*) in the order +that sentence sets out: + +1. **`pagelint` rule 8**, the D4 ruling of 2026-09-29 (017 task 6.7). It turns + `probe/attr_mismatch.py` into a standing gate. It needs a per-block opt-out with a reason, + `PAIRED` beside `APPLIES_TO`, a `CLAUDE.md` ledger row, the red-proof carried over, and the + probe retired. +2. **`--classify`'s second compile** (017 friction #71). It supplies the `using`s, compiles + again, and classifies what remains, so the tranches are decided by one committed, red-proofed + instrument rather than by a probe in `spec/`. +3. **Tranche the 64 unreached pages** by what that instrument finds, and repair them. + +## Subject + +**process** — a gate is built, an instrument is extended, and blocks are repaired page by page +against existing rules. No new reader-facing topic is added. + +## Current State + +Every figure below is **inherited from 017 at `e0385b4`**. All of them are stale until re-derived. + +| Figure | Inherited value | Command | +|---|---:|---| +| C# blocks: BUILT / FAILED / SKIPPED | 990: 299 / 674 / 17 | `python3 tools/blockcheck.py --report $TMPDIR/r.tsv` (after building `refs.csproj` and `blockcheck.csproj`) | +| FAILED on the 64 off-tranche pages | 630 | `--classify` joined to 017 § *The tranches* | +| …by class: import / parse / page-type / values / other / same-page | 314 / 145 / 113 / 34 / 18 / 6 | `python3 tools/blockcheck.py --classify` | +| Off-tranche pages with nothing BUILT | 36 | the same join | +| `pagelint` `using` debt | 524 blocks, 66 pages | `python3 tools/pagelint.py` (warning count) | +| `attr_mismatch` hits | 1, the deliberate `PipelineValidation.md` #7 | `python3 spec/017-compile_repairs/probe/attr_mismatch.py` | + +**Known lower bound:** *import* undercounts the work. At 017 task 1.10, **162** blocks that +`--classify` called *import* had a defect behind the missing `using` (friction #71). That is the +reason for item 2. + +**Friction carried in from 017** that binds this spec: #69 (commit every helper used by more than +one task, and every behaviour run's `Program.cs`, under `spec/018-*/probe/`), #71 (above) and #79 +(name a hit by page and block, never by line). + +## Acceptance criteria + +Provisional. `/spec:requirements` settles them. + +1. **Rule 8 exists and is red-proofed.** `python3 tools/pagelint.py` reports `ATTRIBUTE KIND`. A + plant hits on both mismatch directions and stays silent on a matched pair, and CI runs it. + Decided by the plant command, run as a two-way control. +2. **Rule 8 is on the ledger.** `CLAUDE.md` § *The ledger* has the row, and § *Page Conventions* + has the section. `pagelint` and `CLAUDE.md` read the same in both directions. **No instrument — + checked by reading**, by the reviewer. +3. **The deliberate hit is marked, not counted.** `PipelineValidation.md` #7 carries + ``, and repo-wide rule 8 hits are **0**. + Decided by `python3 tools/pagelint.py`. +4. **The probe is retired.** `spec/017-compile_repairs/probe/attr_mismatch.py` is no longer run by + any task or workflow, and its history stays in 017. Decided by `git grep attr_mismatch -- .github tools`. +5. **`--classify` compiles twice.** A block whose only fault is a missing `using` classifies as + fixable, and a block with a defect behind the `using` classifies by that defect. Decided by a + red-proof with one case of each, recorded in `tasks.md`. +6. **The 64 pages are tranched by the committed instrument**, with no probe column in any tranche + table. **No instrument — checked by reading**, by the reviewer. +7. **The residual falls, and the baseline holds what built.** BUILT rises from the re-derived + start, and `--report` exits 0. Decided by `--report`, against the re-derived figure. +8. **Behavioural claims in repaired blocks are run with a control** (`CLAUDE.md` § *Compiling an + example*). Each run's `Program.cs` is committed beside its row (friction #69). **No instrument — + checked by reading**, by the reviewer. + +## Open questions + +1. **Does rule 8's plant live in a `pagelint --plant-attr` flag or a plant file?** + *Recommendation:* a plant file under `tools/`, run by CI. `pagelint` keeps one CLI shape, and + the plant can be read as a fixture. Depends on how CI invokes `pagelint` today. +2. **What does `--classify` output for a block that BUILDs once `using`s are supplied?** + *Recommendation:* a new class, e.g. `import-only`, kept distinct from `import`. Then the + lower bound and the true figure both stay visible. Depends on whether 017's class names are + consumed by anything committed. +3. **Can 018 tranche all 64 pages, or must it close on a residual again?** *Recommendation:* set + the tranche count after the second compile, from what it finds, and do not promise all 64 + up front. Depends on item 2's output. +4. **Does a tool-only phase 1 (rule 8 + second compile) ship as its own PR before any page + changes?** *Recommendation:* yes, following 017's phase 1. It changes no page beyond the one + marker, so it needs no site sign-off. The marker on `PipelineValidation.md` does change the + published source, so ask anyway. + +## Status Checklist + +- [x] Requirements gathered — `requirements.md`, 2026-10-04 +- [x] Requirements reviewed and approved — 2026-10-04 +- [x] Documentation outline created — `design.md`, 2026-10-04 +- [ ] Outline reviewed and approved +- [ ] Writing tasks identified +- [ ] Writing complete +- [ ] Documentation reviewed +- [ ] Spec closed + +## Next Steps + +1. Re-derive the counts above +2. Read `SUMMARY.md` for where this sits, and `contents/` for what already covers it +3. Identify source material: 017's `tasks.md` (§ *What 017 shipped*, § *For the maintainer: D4*, + the friction ledger 67–80), `tools/README.md`, `tools/pagelint.py`, `tools/blockcheck.py` and + `CLAUDE.md` +4. Run `/spec:requirements`, and get the requirements approved before going further + +## Notes + +- `CLAUDE.md` is the authority on documentation standards; cite it rather than restating it +- `tools/README.md` is the authority on the gates and their expected numbers +- Source code lives in `../Brighter` and `../Darker`, and is **read-only** +- `SUMMARY.md` gets updated whenever a page is added, or the page is an orphan diff --git a/spec/018-compile_residual/design.md b/spec/018-compile_residual/design.md new file mode 100644 index 0000000..c9fe03e --- /dev/null +++ b/spec/018-compile_residual/design.md @@ -0,0 +1,480 @@ +# Spec 018: Compile Residual — Design + +**Created:** 2026-10-04 +**Status:** Draft, for `/spec:review` +**Requirements:** approved 2026-10-04 (`.requirements-approved`), seven open questions approved open + +> Every number here was measured 2026-10-04 at Docs `master` `3a79b20`, Brighter `10.7.0` and +> `origin/master` `a7b3898aa`, Darker `4.1.1`. The probe that produced the experiments is committed +> under `spec/018-compile_residual/probe/` (friction #69) and reproduces them in about 95 seconds: +> +> ```bash +> dotnet build tools/blockcheck/refs/refs.csproj -c Release +> dotnet build tools/blockcheck/blockcheck.csproj -c Release +> bash spec/018-compile_residual/probe/run.sh $W; echo $? # 0 — E1, E2 +> bash spec/018-compile_residual/probe/v4pin.sh $W; echo $? # 0 — E3 +> ``` + +## Design Subject + +**Process**, as the requirements declare. No page is created, so no outline is written file by file. + +| Section | Status | +|---|---| +| **SUMMARY.md changes** | **N/A.** No page is added, moved or retitled. On a process spec this is a category error, not an empty section | +| **Page outlines** (type, banner, opening sentence, headings) | **N/A.** Every repaired page keeps the banner, type and headings it has. A repair that changes an opening sentence re-runs `pagelint --fix ` (017 `design.md` § *Page Repair Rules*, item 6) | +| **The one new prose section** | `CLAUDE.md` § *Handler attributes match their handler*, outlined under § *Rule 8* below | + +## Requirements Re-Verified + +The experiments below changed four deliverables and added two. + +| Requirement | Delta | +|---|---| +| **P0-1, rule 8** | **Unchanged in shape. Two details fixed.** The marker binds to the **next C# block below it**, the way `blockcheck`'s skip marker does (`tools/blockcheck.py:280–300`). So the two markers can stack above one fence, and "the line before the fence" no longer has to be literal. `PAIRED` is re-derived as **13** names at both refs (§ *API Resolved*) | +| **P0-2, the second compile** | **Changed shape.** E4 shows that a second compile which supplies `using`s by name, as 017's probe does, misreads **at least 39** of the 64 pages' FAILED blocks. Those are 17 *import* rows for a handler's `Context` property resolved to `Polly.Context`, and 22 for a page's own `Order` resolved to `StackExchange.Redis.Order`. At the second compile, **29 of 119** DEFECT verdicts are such artefacts. P1-1, the receiver-aware read, cannot stay P1 if P0-2 is to decide the tranches. **D1, ruled 2026-10-04: promoted into P0-2** | +| **P0-3, the tranches** | **Changed shape.** 017 tranched by hard-block count. E1 shows that rule reaches **10** of the 64 pages: only 2 have no hard block, and 8 have one. The hard blocks are spread across every section. So 018 phases by **section group**, with each group's page tables drawn from the committed instrument at the end of phase 2 (AC8) | +| **P0-5, the V4 pin** | **Answered.** Open question 4 asked whether V4 and V3 can share one restore. `refs.csproj:191–192` already says they cannot (`AWSSDK.SQS` 4.x against a cap below 4.0). E3 shows that a **second project with the seven packages swapped** removes every `CS0234` from the six blocks, and breaks **9** BUILT V3 blocks. So the pin is chosen **per page**, by the namespace the page's blocks import | +| **Q3, how many pages** | **Answered: all 64**, in five section groups of 107–137 FAILED blocks each (§ *Target And Phases*). The targets are set from E1 | +| **Added: P0-10, `AddServiceActivator` on two pages** | **New, found by E2.** `AwsScheduler.md:290` and `AzureScheduler.md:231` call `builder.Services.AddServiceActivator(…)` as current code. At `10.7.0` and `master` the name appears only in two doc comments, with no definition. `tools/symbolwatch.tsv` has no row for it, so `symbolcheck` is green over it. **D5, ruled 2026-10-04: repaired in phase 3, with a watchlist row** | +| **Added: P0-7, P0-8, P0-9 — E2's repair categories with no 017 rule** | Forthcoming APIs (4 blocks), single options shown alone (most of 64 *fragment* blocks), handler methods shown without their class (32 blocks), and Shouldly assertions (6 blocks). **D2, D3 and D4, ruled 2026-10-04**: a handler wrapper, two new skip reasons, and Shouldly pinned | + +**Figures carried forward, re-derived:** BUILT / FAILED / SKIPPED **299 / 674 / 17** (`--report` +rows, `cut -f1 | uniq -c`); off-tranche **630 on 64** (`--classify` joined to +`probe/tranche_pages.py`'s 75 pages); `pagelint` **524 / 66** (its summary line). These match +`requirements.md` § *Current state* and `tools/README.md` rows 2 and 9. + +## The Experiments + +### E1 — the second compile over the 64 pages + +**Method.** `probe/run.sh` runs 017's E1 (`run.sh`, `pages.py`) and E3 (`stubs.py`) at the current pin, +with `Order` excluded as 017 § *The tranches* rules, then joins the result to the 64 pages +(`probe/offtable.py`). Its verdicts: **PARSE**; **DEFECT** (a binder error once `using`s are +supplied); **BUILT** by a `using`; and STUB, split by mechanical stubs into **BUILT** (empty stub), +**MEMBERS** (the stub needs members), **HIDDEN** (a defect masked by a `dynamic` stub) and +**SAME-PAGE**. *Hard* = PARSE + DEFECT. *Reachable* = the two BUILTs + MEMBERS + HIDDEN, 017's +definition. + +**Control.** The join over the **75** tranche pages gives **44** FAILED blocks, the figure 017 closed +on. The 64-page table is byte-identical across two runs from fresh directories (`diff` of the sorted +`offtable.tsv`, silent). + +**Output, 630 blocks:** + +| Verdict | Blocks | +|---|---:| +| BUILT by a `using` alone | **34** | +| BUILT by an empty stub | **69** | +| MEMBERS | **161** | +| HIDDEN | **71** | +| SAME-PAGE | **31** | +| PARSE | **145** | +| DEFECT | **119** | +| *Reachable* | **335** | +| *Hard* | **264** | + +**Pages by hard blocks:** 0 → **2**; 1 → **8**; 2 → **17**; 3 → **8**; 4 → **4**; 5 or more → +**25**. Of the **36** off-tranche pages with nothing BUILT, **28** have a reachable block, and **11** +reach one by a `using` or an empty stub alone. The eight with nothing reachable are +`DispatcherConfigurationReference.md`, `ConfiguringOpenTelemetry.md`, `BrighterInboxSupport.md`, +`BrighterOutboxSupport.md`, `CausationTrackingStores.md`, `SwitchingSchedulers.md`, +`AzureServiceBusConfiguration.md` and `RabbitMQConfiguration.md`. + +**What it changes:** 017's tranche rule (hard ≤ 1) holds **10** pages and 53 reachable blocks. The +other 54 pages hold 282 reachable blocks behind their hard ones. Tranching by hardness would leave +most of the reachable work for a 019. + +### E2 — the 264 hard blocks, by the repair each needs + +**Method.** `probe/hardcat.py`. DEFECT blocks are re-explained in their *after-`using`* state +(`stageP`), so a missing `using` cannot mask the error. Each is sorted by the first matching tag, in +this order: forthcoming → pin-gap → two-snippets → collision → wrapper → fragment → api. PARSE blocks +are sorted by their text. **A heuristic for sizing, not an instrument.** P0-2's `--classify` decides, +and AC7 reconciles the two. + +| DEFECT (119) | Blocks | Repair rule | +|---|---:|---| +| **api**: a member, named argument or conversion the pinned release does not have | **50** | 017: verify at 10.7.0, repair at every recurrence, one ledger row | +| **wrapper**: `object` / `Context` has no `Handle`, `HandleAsync`, `Bag`, i.e. a handler member staged without its base class | **22** | built by the handler wrapper (D2) | +| **two-snippets**: `CS0128`, `CS0111`, a name declared twice in one fence | **15** | split the fence, as 016 and 017 did | +| **fragment**: `CS0161`, `CS0825`, `CS0116` and kin | **14** | 017: make whole, or skip with an accepted reason | +| **collision?**: `Task`, `Tag`, `User`, `Activity` and kin, a page type named like a pinned one | **7** | a page type, read by the receiver-aware resolution (D1) | +| **pin-gap**: Shouldly's `ShouldBe…`, not pinned | **6** | all on `TestDoubleOptions.md`; `Shouldly` is pinned (D4) | +| **forthcoming**: `OnceOnlyAction.Replay`, on `master` and not in 10.7.0 | **4** | SKIPPED, *forthcoming* (D3) | +| unexplained | **1** | read at the phase | + +| PARSE (145) | Blocks | Repair rule | +|---|---:|---| +| **fragment**: a single option (`OnConflict = OnSchedulerConflict.Overwrite`), a bare expression, an attribute list | **64** | SKIPPED, *a single option shown alone*, where its type resolves in the pin (D3); otherwise made whole under 017's rule | +| **literal `...`** in code: `AddBrighter(...)` | **59** | 017: rewrite as `// ...`, and complete the statement | +| **leading `.` chain**: `.AddProducers(…)` | **12** | 017: a fragment | +| **types and statements** in one fence | **7** | split the fence, or reorder to top-level form | +| **not C#**: JSON IAM policies in a `csharp` fence (`AwsScheduler.md` #13, #14; `AzureScheduler.md` #19) | **3** | 017: retag. The corpus falls by 3 | + +**Verified by reading, three per bucket** (`blockcheck --show`): `AwsScheduler.md` #13 is a JSON IAM +policy; `CommandProcessorConfigurationReference.md` #13 is `services.AddBrighter(...)`; +`KafkaConfiguration.md` #10 is `configHook: config => {…}`, a named argument shown alone; +`QueryPipeline.md` #12 is three attributes with nothing to decorate. + +### E3 — the V4 pin, case, control and reverse + +**Method.** `probe/v4pin.sh` copies `refs.csproj` with seven `PackageReference`s swapped for their +`.V4` twins, builds it (0 errors; 546 reference assemblies; `awssdk.sqs/4.0.100.7` against the main +pin's `3.7.500.5`), and compiles three sets against both pins. + +| Set | Against the V4 pin | Against the main pin | +|---|---|---| +| **Case**: the six `CS0234 … V4` blocks | **0** `CS0234`. Four are left with **1** diagnostic each (`CS0103`, a value): `DistributedLock.md` #2, `DynamoDbDistributedLock.md` #1, #2, `S3LuggageStore.md` #1. `AwsScheduler.md` #2, #3 keep 6 and 14 missing names | **11** `CS0234` across the six | +| **Reverse**: the 15 BUILT blocks on AWS-family pages | **9 break**: `AWSSQSConfiguration.md` #1, #3–#6, `DynamoInbox.md` #1, `DynamoOutbox.md` #1–#3. All V3 namespaces | all build | + +So the V4 pin cannot replace the main one, and a page cannot be selected by **the packages its prose +names**: `DynamoInbox.md` names the V4 package at `:21`, and its one block imports V3 and breaks +under V4. It is selected by **the namespace its C# blocks import**. On the four pages with a V4 +block, every BUILT block (`AwsScheduler.md` #19, `DistributedLock.md` #1, `S3LuggageStore.md` #2) +still builds under V4. So **per-page selection is enough today**, and per-block selection (P2-3) is +not needed. + +### E4 — what a by-name `using` gets wrong + +**Method.** Over the off-tranche *import* rows of `--classify`, the names it took as import evidence +whose only pinned namespaces are third-party. + +| Name | *import* rows | Pages | Resolved to | What the page means | +|---|---:|---:|---|---| +| `Order` | **22** | 10 | `StackExchange.Redis` | the page's own domain type (017 § *The tranches*) | +| `Context` | **17** | 5 | `Polly`, `Google.Api` | `RequestHandler.Context`, an `IRequestContext?` property (`RequestHandler.cs:62`) | +| `Policy` | 3 | 2 | AWS, GCP model types | Polly's, or the page's own | +| `Tag`, `User` | 2 each | 1, 2 | AWS model types | page types | + +Then **at the second compile**, 22 DEFECT blocks are `wrapper` and 7 are `collision?` (E2): **29 of +119**. 017 met the same blind spot and ruled it *not repaired* (2026-09-27). Each phase then carried +an `awk` exclusion and a reading rule (friction #70). If P0-2 builds the second compile on by-name +resolution, the tranche tables inherit 39 wrong *import* rows and 29 wrong DEFECT verdicts, and AC8 +reads an instrument that misleads. + +### E5 — handler methods shown without their class + +**Method.** Members-shaped C# blocks containing an `override` of `Handle`, `HandleAsync`, `Execute` +or `ExecuteAsync`, over the whole corpus, by verdict. + +**Output:** **32** FAILED, all off-tranche; 1 SKIPPED on a tranche page. They fail on `Context`, +`base.HandleAsync` and the like, because the `members` wrapper stages them in a `Holder` with no +base class (`tools/blockcheck.py:355–368`). This is the `wrapper` bucket of E2 from the other side. + +## API Resolved For This Design + +```bash +# PAIRED: the same 13 at both refs (52 attribute class names in total) +git -C ../Brighter grep -hoE 'class [A-Za-z]+Attribute' -- src | sed -E 's/class ([A-Za-z]+)Attribute/\1/' | sort -u > a +grep -E 'Async$' a | sed 's/Async$//' | sort -u | comm -12 - a +# BulkDepositCallSite DeferMessageOnError DepositCallSite DontAckOnError FallbackPolicy FeatureSwitch +# Monitor RejectMessageOnError RequestLogging UseInbox UsePolicy UseResiliencePipeline ValidateRequest +git -C ../Brighter grep -n 'class ConfigurationException' 10.7.0 -- src # live, ConfigurationException.cs:32 +git -C ../Brighter grep -n 'ValidatePipelines' 10.7.0 -- src # live, BrighterPipelineValidationExtensions.cs:58 +git -C ../Brighter grep -nE 'class RequestHandler(Async)?<' 10.7.0 -- src/Paramore.Brighter # live, :52 and :54 +git -C ../Brighter grep -n ' Context' 10.7.0 -- src/Paramore.Brighter/RequestHandler.cs # IRequestContext? Context, :62 +git -C ../Darker grep -nE 'abstract class QueryHandler(Async)?<' 4.1.1 -- src # live, QueryHandler.cs:9, QueryHandlerAsync.cs:9 +git -C ../Brighter grep -n 'AddServiceActivator' 10.7.0 -- src # DEAD: doc comments only, no definition +git -C ../Brighter grep -n -A14 'enum OnceOnlyAction' -- src # Replay: absent at 10.7.0; FORTHCOMING on master (#4067) +ls ../Brighter/src | grep -i v4 # 8 dirs: 7 packages the pages name + a misspelt Tranformers.AWS.V4 +``` + +`pagelint` functions this design extends are resolved by line in `tools/pagelint.py`: `APPLIES_TO` +`:164`, `OPT_OUT` `:215`, `check_code_blocks` `:475`, `check_terminology` `:806`, `main` `:1188`. +`blockcheck`'s are resolved in `tools/blockcheck.py`: `classify` `:254`, `SKIP_RE` `:169`, the +marker binding `:280–300`, `WRAPPERS` `:355`, `CLASS_ORDER` / `classify_failure` `:1198–1204`. + +## Rule 8 — `ATTRIBUTE KIND` + +**What it reads.** Every fenced block whose info string is `csharp`, `c#` or `cs`, through +`pagelint.Page`, so FAILED, SKIPPED and BUILT blocks alike, on every page `pagelint` walks. It does +**not** consult the banner's product. `PAIRED` names Brighter attributes only, and Darker 4.1.1 has no +async twins, so a Darker page cannot hit. That keeps rule 8 independent of rule 2. + +**What it reports.** For an attribute line `[Name(` or `[NameAsync(` with `Name` in `PAIRED`, it skips +following attribute and blank lines to the decorated line. It reports when a sync attribute +decorates a line naming `HandleAsync`, or an `…Async` attribute decorates one naming `Handle(`. This +is the probe's `scan()`, moved and unchanged, so its 25 runs in 017 stand as its history. Message: + +```text +contents/PipelineValidation.md:250: ATTRIBUTE KIND: [RejectMessageOnError] is the sync attribute, on HandleAsync. Use [RejectMessageOnErrorAsync], or mark the block if the mismatch is the point +``` + +**Level.** Error, repo-wide and under `--changed`, per the ledger row below. + +**`PAIRED`**, beside `APPLIES_TO`: + +```python +# Brighter handler attributes that exist with and without the Async suffix. Re-derive with the +# version bump that edits APPLIES_TO, in ../Brighter at the new tag: +# git grep -hoE 'class [A-Za-z]+Attribute' -- src | sed -E 's/class ([A-Za-z]+)Attribute/\1/' \ +# | sort -u > a; grep -E 'Async$' a | sed 's/Async$//' | sort -u | comm -12 - a +PAIRED = ('BulkDepositCallSite', 'DeferMessageOnError', 'DepositCallSite', 'DontAckOnError', + 'FallbackPolicy', 'FeatureSwitch', 'Monitor', 'RejectMessageOnError', 'RequestLogging', + 'UseInbox', 'UsePolicy', 'UseResiliencePipeline', 'ValidateRequest') +``` + +**The opt-out.** `` binds to the next C# block +below it, as `blockcheck`'s skip does. It is reported in the same three ways that one is: a marker +with **no reason** is an error, a marker with **no C# block after it** is an error, and a **second** +marker on the same block is an error. Every honoured marker prints with its reason and a count, +`symbolcheck`'s convention: *"1 block marked attr-mismatch-intended"*. **The marker silences rule 8 +only**, for that one block. + +**`--plant`.** A mode, not a file: `pagelint` refuses paths outside `contents/` (exit 2), and a plant +file would loosen that. It runs in-memory cases and exits **0** only if every one behaves: + +| Plant | Must | +|---|---| +| sync attribute on `HandleAsync` | hit | +| `…Async` attribute on `Handle` | hit | +| matched pair, sync on `Handle` | not hit | +| a mismatched block under a marker with a reason | not hit | +| a marker with no reason | report the marker | + +The last two are new, beyond the probe's three, because the opt-out is new. The red-proof (AC1) +removes one plant's expectation and shows exit 1. + +**CI.** `.github/workflows/docs.yml`, `check` job, after `python3 tools/pagelint.py`: +`- run: python3 tools/pagelint.py --plant`. It is bare, with no `|| true`. + +**The page.** `contents/PipelineValidation.md`, the line above block 7's fence (`:247`): +``. +The rule and the marker are in **one PR** (`tools/README.md`, rule 3). + +**`CLAUDE.md`.** A ledger row, after rule 6's: + +| Convention | Rule | Repo-wide | `--changed` | +|---|---|---|---| +| A handler attribute's kind matches its method's: sync on `Handle`, `…Async` on `HandleAsync` | 8 (`ATTRIBUTE KIND`) | error | error, unless the block is marked `attr-mismatch-intended` with a reason | + +And **§ *Handler attributes match their handler***, under *Page Conventions* after *Complete code +blocks*. It is about 15 lines and has three parts: +1. **The rule and why the compiler cannot catch it.** Brighter throws `ConfigurationException` when + it builds the pipeline, and `ValidatePipelines()` reports it at startup. A block can therefore + compile, enter `blockcheck`'s baseline, and still be wrong +2. **`PAIRED` and where it lives.** Cite the tuple; do not list it +3. **The opt-out**, with `PipelineValidation.md`'s marker as the worked example, and why it is + per-block: a page-wide marker would have hidden `#9` and `#10` beside the deliberate `#7` + +**`tools/README.md`.** Row 2's ref and description; *What each gate actually checks*, `pagelint`'s +bullet, gains rule 8; *The other modes* gains `python3 tools/pagelint.py --plant`. + +**Retiring the probe.** `spec/017-compile_repairs/probe/attr_mismatch.py` stays in place as 017's +history. No 018 task names it as an instrument (AC5). + +## The Second Compile — `--classify` + +**What changes.** `classify` (`tools/blockcheck.py:254`) gains the probe's second stage. For every +FAILED block that is not *parse*, it supplies the `using` directives the pinned type table resolves, +recompiles, and classifies what remains. The first-compile class is kept. + +**Output.** Still one row per FAILED block, sorted, and still exit 2 on nothing to classify. It now has +five columns: + +```text +pageordinalclassfirstnames +``` + +`class` is the second-compile class and `first` is today's class. That keeps 017's lower bound beside +the true figure, as open question 2 recommended. Nothing parses the four columns today +(`git grep -n -- '--classify' tools .github` → only `blockcheck` and `tools/README.md`). + +**Classes**, in `CLASS_ORDER`: + +| Class | Means | Repair rule (017 § *Page Repair Rules*) | +|---|---|---| +| `parse` | does not parse as staged | as 017 | +| `built-by-using` | builds once given its `using`s | the `using`s, in the block | +| `defect` | after the `using`s, a binder error that is not a missing name | verify and repair | +| `same-page` | after the `using`s, only names another block on the page declares | FAILED, listed | +| `values` | after the `using`s, only lower-case missing names | a value stub | +| `page-type` | after the `using`s, a capitalised name no pin ships and the page never shows | a type stub | + +**Resolution is receiver-aware (D1).** A name is resolved to a pinned type only when it +is not something the block or its page already supplies: +- it is **not declared on the page** (`TYPE_DECL_RE` over the page's blocks, as `same-page` already + does), which removes `Order`, `Task`, `Tag` and `User` +- it is **not a member of the staged class's base**, which removes `Context`. The handler wrapper + (D2) makes the base known. A members block it does not wrap, because its signature names no request + type, has `Context`, `Bag` and `base.Handle…` read as wrapper artefacts: classed `defect` with a + note, never `built-by-using` + +The pinned type table is read by `Program.cs --types`, which exists (017 phase 1). No new C# mode is +needed: the recompile is the existing `--explain` over a rewritten stage. + +**Red-proof (AC6).** One recorded run over five named blocks, each with a known answer. A +`built-by-using` block (one of the 34); a `defect` behind a `using` (one of the 50 *api*); a +`page-type` block; a `parse` block; and the **control**: a block that page-declares a type also +pinned elsewhere (an `Order` block), which must **not** come out `built-by-using`. A second run +with the page-declared check disabled must turn the control `built-by-using`, which shows the +check is what decides it. + +**Reconciled (AC7).** A block-by-block join of the new `--classify` against `probe/run.sh`'s +`verdicts.tsv` at the same ref. Every disagreement is listed with its reason. The expected +disagreements are E4's 39 and 29, each read by the probe by name and by `--classify` by receiver. + +## The V4 Pin + +```text +tools/blockcheck/ +├── refs/refs.csproj unchanged: 100 packages, V3 AWS +├── refs-v4/refs-v4.csproj new: refs.csproj with the 7 AWS packages swapped for .V4 +└── scaffold/pages.tsv gains a third column: `v4` on the pages whose blocks import a .V4 namespace +``` + +- **Selection is per page**, by a `v4` cell in `pages.tsv`, the file that already maps pages to their + scaffold. The rule, enforced by `--report` beside the unit rule: a page marked `v4` has a C# block + importing a `.V4` namespace, and a page with such a block is marked `v4`. Today that is **4 pages**: + `AwsScheduler.md`, `DistributedLock.md`, `DynamoDbDistributedLock.md`, `S3LuggageStore.md` +- **`--report` prints a fourth scope line**: `N reference assemblies (v4 pin)`, beside the main + pin's, because a pin that did not restore must not shrink silently (`tools/README.md` § *Reading a + number before you trust it*) +- **CI's `blocks` job** gains one build step: `dotnet build tools/blockcheck/refs-v4/refs-v4.csproj -c Release`. + Without it the gate exits 2, the same contract as the main pin +- **Its comment** says why it exists, citing `refs.csproj:191–192`, and that its versions move with + `refs.csproj`'s Brighter version in the same edit +- **The baseline key is unchanged.** A block is still (page, ordinal); the pin is a property of the + page, as the scaffold unit is + +**Expected verdict movement:** none at phase 2. Four of the six need a value stub, and two need more +(E3). Each is repaired in the phase that holds its page's section: `AwsScheduler.md` #2, #3 in S1; +`S3LuggageStore.md` #1 in S4; `DistributedLock.md` #2 and `DynamoDbDistributedLock.md` #1, #2 in S5. +The last four are on 017 tranche pages, listed in 017 § *Blocks that stay FAILED* as waiting for +"D3, 018", so S4's and S5's tables carry them beside the off-tranche pages. + +## Page Repair Rules + +**017 `design.md` § *Page Repair Rules* and § *Scaffold Stub Rules* apply unchanged**, and are cited +rather than restated. Every FAILED block on a reached page leaves in one of four states: BUILT, +SKIPPED with an accepted reason, FAILED and listed, or retagged. Rule 8 of that section, *"`probe/attr_mismatch.py` +runs in every tranche PR"*, is replaced by rule 8 the gate, which now runs on every push. + +**What 018 adds**, ruled 2026-10-04: + +| Case | Blocks (E2/E5) | Rule | Ruling | +|---|---:|---|---| +| A forthcoming API, which the page already says ships after the pinned release | 4 | SKIPPED, reason *"forthcoming: ships after Brighter 10.7.0, as the page says at line N"* | **D3** | +| A single option, argument or expression shown alone, with no enclosing call on the page | ≤ 64 | SKIPPED, reason *"a single option shown alone; its type is named in the sentence before it"*, where that type resolves in the pin | **D3** | +| A handler method shown without its class | 32 | built by the handler wrapper | **D2** | +| Shouldly assertions | 6 | `Shouldly` pinned in `refs.csproj` | **D4** | +| `AddServiceActivator` as current code | 2 sites | rewrite to `AddConsumers`, add a `symbolwatch.tsv` row, and opt out the two V9 discussion pages | **D5** | + +**And from 017's friction, as standing obligations in `tasks.md` § 1** (P0-6): #68 (both reading +criteria at every phase close), #69 (probes committed here), #72 (the off-tranche term predicted), +#73 (repair at every recurrence; only scope goes to the maintainer; a *second pass* task per +phase), #74 (a ledger row only after its grep has run), #79 (hits named by page and block). + +## Target And Phases + +### The section groups + +From E1, grouped by `SUMMARY.md` section so that a page family is repaired together. The +schedulers share a structure, with five *parse* blocks each on four of them: + +| Group | Sections | Pages | FAILED | Reachable | Hard (parse + defect) | Nothing BUILT | +|---|---|---:|---:|---:|---:|---:| +| **S1** | Scheduler | 8 | 129 | 81 | 29 + 16 | 3 | +| **S2** | Commands, Handlers and Pipelines; Get Started | 10 | 107 | 66 | 15 + 23 | 7 | +| **S3** | Darker; Understanding Brighter | 10 | 137 | 75 | 31 + 20 | 6 | +| **S4** | Using an External Bus; Transports | 16 | 124 | 59 | 39 + 19 | 10 | +| **S5** | Brighter Configuration; V10 Migration; Outbox and Inbox; Reference; Health Checks and Observability | 20 | 133 | 54 | 31 + 41 | 10 | +| **Total** | | **64** | **630** | **335** | **145 + 119** | **36** | + +**These are working groups, not the tranche tables.** AC8 requires the tables to be the committed +instrument's, so each group's page table is drawn in `tasks.md` from `--classify` at phase 2's close, +with the command beside it. + +### The targets + +- **BUILT ≥ 560 at the close.** That is 299 + 264 (the reachable blocks less the 71 HIDDEN, whose + stubs mask the real error), with nothing assumed from the 264 hard blocks. 017 set its target the + same way and beat it, because hard repairs and recurrences added blocks a reachable count cannot + see +- **Pages with nothing BUILT: 39 → ≤ 14.** Three of the 39 are tranche pages: + `ReturningResultsFromAHandler.md`, `ReplayOnSeenReference.md` (forthcoming, D3) and + `DynamoDbDistributedLock.md`, which the V4 pin can reach (below). None is counted on. Of the 36 + off-tranche pages, 28 have a reachable block. 39 − 25 = 14, so **25 of those 28** must gain a BUILT + block. The eight with nothing reachable are not counted on +- **Every FAILED block on the 64 pages** leaves in one of the four states, and is named in 018 + § *Blocks that stay FAILED* if it stays (AC13) + +### Phases + +Each phase is **one PR** (`tools/README.md` § *One phase is one pull request*). + +```text +Phase 1 rule 8 one page: the marker on PipelineValidation.md #7 — sign-off asked + ├─ tools/pagelint.py: PAIRED, ATTRIBUTE KIND, the opt-out and its three faults, --plant + ├─ .github/workflows/docs.yml: pagelint --plant + ├─ CLAUDE.md: the ledger row; § Handler attributes match their handler + └─ tools/README.md: row 2, the pagelint bullet, the other modes +Phase 2 instruments no page changes + ├─ --classify: the second compile, five columns, the receiver-aware read (D1) + ├─ refs-v4.csproj, pages.tsv's v4 column and its rule, the scope line, the CI build step + ├─ the handler wrapper (D2); Shouldly in refs.csproj (D4) + ├─ P1: the `statements` Program (#75), a non-compiling unit as a finding (#77), pagelint's per-page count (#80) + ├─ P1-3: the net10.0 measurement, recorded before any table is drawn + └─ tasks.md: the S1–S5 page tables, drawn from --classify (AC8) +Phase 3 S1 — Scheduler + D5's AddServiceActivator repair and watchlist row +Phase 4 S2 — Commands, Handlers and Pipelines; Get Started +Phase 5 S3 — Darker; Understanding Brighter +Phase 6 S4 — Using an External Bus; Transports + S3LuggageStore.md #1 (V4) +Phase 7 S5 — the remaining five sections + DistributedLock.md #2, DynamoDbDistributedLock.md #1, #2 (V4) +Phase 8 close acceptance walk, ledgers, § What 018 shipped, the residual for 019 +``` + +**Phase 2 changes no page, but it may move verdicts.** D2's wrapper and #75's `Program` change how +blocks are staged, so blocks may build with no page edit. Any that do enter `baseline.tsv` in phase +2's PR, because the gate requires equality. Phase 2's first task measures that set before writing a +row (§ *Gate Movement Predicted*). + +## Gate Movement Predicted + +Each gate's corpus is as `tools/README.md` § *What each gate actually checks* states. Figures are +cited from its rows, not restated. + +| Gate | Phase 1 | Phase 2 | Phases 3–7 | Why | +|---|---|---|---|---| +| `linkcheck` | **unmoved** | **unmoved** | **unmoved**, unless a repair adds a link | no new `.md` under its walk: `spec/` is excluded, and `--plant` is a mode, not a file | +| `pagelint` errors | **0, unmoved** | 0 | 0 | rule 8 lands with the marker that satisfies it | +| `pagelint` warnings | **unmoved**: the marker is an HTML comment, and no block's text changes | **unmoved** | **fall**, by the blocks each phase gives their `using`s; predicted per phase from its table | the debt is per block | +| `pagelint --plant` | **new, exit 0** | 0 | 0 | | +| shape / redirects / `--verify` | **unmoved** | **unmoved** | **unmoved** | no `SUMMARY.md` change | +| `versioncheck` | **unmoved** | **unmoved** | **unmoved** | no tutorial pin touched. A repair that adds one is predicted in its phase | +| `optioncheck` | **unmoved** | **unmoved** | **unmoved** | no option table touched | +| `symbolcheck` | **unmoved** | **unmoved** | phase 3: **entries 22 → 23**, and **silenced** rises by the opt-out sites on `V10MigrationGuide.md` and `FAQ.md` (D5); otherwise unmoved | the `AddServiceActivator` row and its opt-outs | +| `blockcheck` corpus | **unmoved, 990** | **unmoved** | **moves both ways**: retags (−3 for E2's JSON) and fence splits (+ for E2's 15 two-snippet and 7 mixed blocks); predicted per phase | a split adds a block | +| `blockcheck` BUILT | **unmoved, 299** | **rises by the blocks the handler wrapper (D2), the `Program` fix (#75) and the Shouldly pin (D4) make build**, measured by phase 2's first task before any row is written. Up to 32 + 6, less those that fail for another reason | **rises**, to ≥ 560 by the close | | +| `blockcheck` scope lines | unmoved | **+1 line**, the V4 pin's assemblies; the main pin's assemblies **rise** by Shouldly's (D4); the scaffold rule also checks the `v4` column | units rise with each phase's stubs | | + +## Design Decisions + +D1–D5 were ruled by the maintainer on 2026-10-04, each as recommended. D6 and D7 needed no ruling. + +| # | Question | Decision | Ruled | +|---|---|---|---| +| **D1** | **Promote P1-1, the receiver-aware `--classify`, into P0-2?** It changes approved scope | **Yes.** E4: 39 off-tranche *import* rows and 29 of 119 second-compile DEFECTs are misreads by name. AC8 asks the tranches to be the instrument's, and a by-name instrument is wrong on about one block in nine. 017's *not repaired* ruling was taken before the second compile existed, when the cost was a reading rule; now it is a wrong table | **Yes**, the maintainer, 2026-10-04 | +| **D2** | **A handler wrapper for members blocks that override `Handle`, `HandleAsync`, `Execute` or `ExecuteAsync`?** The `Holder` derives from `RequestHandler`, `RequestHandlerAsync`, `QueryHandler` or `QueryHandlerAsync`, with the type arguments read from the signature | **Yes.** 32 blocks, all off-tranche. The alternative is 32 page edits that wrap a method in a class the reader did not need to see. It is the same kind of help the `members` wrapper already gives, and it is enforced the same way: a block whose signature names no request type is not wrapped | **Yes**, the maintainer, 2026-10-04 | +| **D3** | **Two new accepted skip reasons:** *forthcoming, as the page says*; and *a single option shown alone, its type named in the sentence before it* | **Yes to the first.** 4 blocks; the page already tells the reader, and the gate should not argue. **Yes to the second, narrowly**: the type must resolve in the pin, so the skip cannot hide a dead name. The rest of the 64 fragments are made whole under 017's rule | **Agreed**, the maintainer, 2026-10-04 | +| **D4** | **Pin Shouldly?** | **Yes**: one package, 6 blocks, all on `TestDoubleOptions.md`, a package the page's reader installs | **Yes**, the maintainer, 2026-10-04 | +| **D5** | **`AddServiceActivator`:** rewrite the two sites to `AddConsumers`, and add a `symbolwatch.tsv` row, with per-symbol opt-outs on `V10MigrationGuide.md` and `FAQ.md`, which discuss the V9 name | **Yes, in phase 3** (S1 holds both sites). 015's triage is the precedent for a watchlist row with opt-outs | **Yes**, the maintainer, 2026-10-04 | +| **D6** | **Is the V4 pin's selection per page enough?** | **Yes, today.** E3: every BUILT block on the four V4 pages builds under V4. Per-block selection stays P2-3, triggered only by a page that mixes V3 and V4 blocks | — | +| **D7** | **Raise P1-3, the `net10.0` measurement, to P0?** | **No, but run it in phase 2 before the tables are drawn**, as friction #76 asks. Raise it if it moves a verdict | — | + +## Design Quality Checklist + +- [x] Readable with no prior context: it opens with the subject, the deltas and the experiments, and + every verdict word is defined in E1 +- [x] No page is created, so page types, banners and headings are **N/A**, marked so. The one new + prose section (`CLAUDE.md`) is outlined +- [x] Every API named is resolved, with the command: `PAIRED`, `ConfigurationException`, + `ValidatePipelines`, the handler base classes, `Context`, `AddServiceActivator` (dead), + `OnceOnlyAction.Replay` (forthcoming) +- [x] Every number has its command, or the committed probe that produced it. The one unmeasured + figure, the BUILT movement from D2 and #75, is named, and phase 2 measures it first +- [x] Experiments have two-way controls: E1 against 017's 44; E3's case, control and reverse; AC6's + `Order` control +- [x] Gate movement predicted for all nine gates plus `--plant`, including "unmoved" +- [x] Structure shown as trees and tables diff --git a/spec/018-compile_residual/probe/hardcat.py b/spec/018-compile_residual/probe/hardcat.py new file mode 100644 index 0000000..7017e42 --- /dev/null +++ b/spec/018-compile_residual/probe/hardcat.py @@ -0,0 +1,63 @@ +"""The off-tranche hard blocks, sorted by the 017 repair rule each would need. A heuristic for +sizing the design, not an instrument: P0-2's --classify is what decides. Run after offtable.py. +DEFECT diagnostics are re-read at the second compile (stageP), so a missing using cannot mask them.""" +import collections, re, subprocess, sys +W = sys.argv[1]; sys.path.insert(0, 'tools'); import pagelint +DLL = 'tools/blockcheck/bin/Release/net9.0/blockcheck.dll'; REFS = 'tools/blockcheck/refs/bin/Release/net9.0/refs.txt' +tr = set(open(f'{W}/tranche.txt').read().split()) +V = [l.rstrip('\n').split('\t') for l in open(f'{W}/verdicts.tsv')] +off = lambda page: page.replace('contents/', '') not in tr +defids = [b for b, p, v, _ in V if v == 'DEFECT' and off(p)] +parids = [b for b, p, v, _ in V if v == 'PARSE' and off(p)] +idx = {l.split('\t')[0]: l.rstrip('\n').split('\t')[1] for l in open(f'{W}/stage/index.tsv')} +decl = re.compile(r'\b(?:class|record|interface|struct|enum)\s+([A-Za-z_]\w*)') +pagedecl = collections.defaultdict(set) +for bid, page in idx.items(): pagedecl[page] |= set(decl.findall(open(f'{W}/stage/{bid}.cs').read())) +out = subprocess.run(['dotnet', DLL, '--explain', f'{W}/stageP', REFS] + defids, capture_output=True, text=True).stdout +diag = collections.defaultdict(list) +for l in out.splitlines(): + p = l.split('\t') + if len(p) >= 4: diag[p[0]].append((p[1], p[3])) +rx = re.compile(r"^'([^']+)' does not contain a definition for '([^']+)'") +DUP = {'CS0128', 'CS0111', 'CS0101', 'CS0102'} +FRAG = {'CS0161', 'CS0825', 'CS0116', 'CS0106', 'CS1106', 'CS0841', 'CS1520'} +ORDER = ['forthcoming', 'pin-gap', 'two-snippets', 'collision', 'collision?', 'wrapper', 'fragment', 'api'] +dc = collections.Counter() +with open(f'{W}/defcat.tsv', 'w') as f: + for bid in defids: + page, tags = idx[bid], set() + for c, m in diag[bid]: + if c in ('CS0246', 'CS0103', 'CS0234'): continue + mm = rx.match(m) + if mm: + short, mem = mm.group(1).split('.')[-1].split('<')[0], mm.group(2) + if mem == 'Replay': tags.add('forthcoming') # OnceOnlyAction.Replay, after 10.7.0 + elif re.match(r'Should(Be|Not)', mem): tags.add('pin-gap') # Shouldly is not pinned + elif short in pagedecl[page]: tags.add('collision') + elif short in ('object', 'Context') or mem in ('Handle', 'HandleAsync', 'Bag'): tags.add('wrapper') + elif short in ('Task', 'Tag', 'User', 'Activity', 'Span', 'Key', 'Log'): tags.add('collision?') + else: tags.add('api') + elif c in DUP: tags.add('two-snippets') + elif c in FRAG: tags.add('fragment') + else: tags.add('api') + k = next((o for o in ORDER if o in tags), 'none'); dc[k] += 1 + print(bid, page, k, ','.join(sorted(tags)), sep='\t', file=f) +pages = pagelint.load_pages() +rid = {l.split('\t')[3]: (l.split('\t')[1], int(l.split('\t')[2])) for l in open(f'{W}/r.tsv')} +pc = collections.Counter() +with open(f'{W}/parcat.tsv', 'w') as f: + for bid in parids: + rel, n = rid[bid] + b = [b for b in pages[rel].blocks if (b['info'] or '').strip().lower() in ('csharp', 'c#', 'cs')][n - 1] + txt = '\n'.join(re.sub(r'//.*', '', t) for _, t in b['body']).strip() + if re.match(r'^\{\s*"', txt) or re.match(r'^\s*<\w', txt) or (re.search(r'^\s*"\w+"\s*:', txt, re.M) and 'var ' not in txt): + k = 'not-csharp' + elif '...' in txt: k = 'literal-ellipsis' + elif re.search(r'^\s*(public |private |internal |protected )?(sealed |static |abstract )*(class|record|interface) \w', txt, re.M) \ + and re.search(r'^(?!\s*(public|private|protected|internal|\[|\}|\{|//))\s*[a-z_]\w*(\.\w+)*\s*(\(|=)', txt, re.M): + k = 'types-and-statements' + elif re.match(r'^\s*\.', txt): k = 'leading-dot-chain' + else: k = 'fragment' + pc[k] += 1; print(bid, rel, k, sep='\t', file=f) +print(f'{len(defids)} DEFECT:', ', '.join(f'{n} {k}' for k, n in dc.most_common())) +print(f'{len(parids)} PARSE:', ', '.join(f'{n} {k}' for k, n in pc.most_common())) diff --git a/spec/018-compile_residual/probe/offtable.py b/spec/018-compile_residual/probe/offtable.py new file mode 100644 index 0000000..bb9bb7d --- /dev/null +++ b/spec/018-compile_residual/probe/offtable.py @@ -0,0 +1,23 @@ +"""Per off-tranche page: FAILED, and what the second compile and the stubs make of them. +Hard = PARSE + DEFECT; reachable = BUILT by a using, BUILT by an empty stub, MEMBERS, HIDDEN.""" +import collections, re, sys +W = sys.argv[1] +tr = set(open(f'{W}/tranche.txt').read().split()) +st = {l.split('\t')[0]: l.split('\t')[2].strip() for l in open(f'{W}/stubs.tsv')} +pg = collections.defaultdict(collections.Counter) +for l in open(f'{W}/verdicts.tsv'): + bid, page, v, _ = l.rstrip('\n').split('\t'); p = page.replace('contents/', '') + if p not in tr: pg[p][v if v != 'STUB' else 'STUB:' + st[bid]] += 1 +built = collections.Counter(l.split('\t')[1].replace('contents/', '') for l in open(f'{W}/r.tsv') if l.startswith('BUILT\t')) +sec, secof = None, {} +for l in open('SUMMARY.md'): + m = re.match(r'^## (.+)', l) + if m: sec = m.group(1).strip() + m = re.search(r'\(/contents/([^)#]+\.md)', l) + if m: secof.setdefault(m.group(1), sec) +print('section\tpage\tfailed\tusing\tempty_stub\tmembers\thidden\tsame_page\tparse\tdefect\thard\treachable\tbuilt_now') +for p, c in sorted(pg.items(), key=lambda x: (secof.get(x[0], '?'), x[0])): + hard = c['PARSE'] + c['DEFECT'] + reach = c['BUILT'] + c['STUB:BUILT'] + c['STUB:MEMBERS'] + c['STUB:HIDDEN'] + print('\t'.join(map(str, [secof.get(p, '?'), p, sum(c.values()), c['BUILT'], c['STUB:BUILT'], c['STUB:MEMBERS'], + c['STUB:HIDDEN'], c['STUB:SAME-PAGE'], c['PARSE'], c['DEFECT'], hard, reach, built[p]]))) diff --git a/spec/018-compile_residual/probe/run.sh b/spec/018-compile_residual/probe/run.sh new file mode 100644 index 0000000..523a33e --- /dev/null +++ b/spec/018-compile_residual/probe/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Spec 018 design probe. Re-runs 017's second-compile probe at the current pin, then joins it +# to the 64 pages no 017 tranche reached and sorts their hard blocks by repair rule. +# Scratch only: writes under $1, changes nothing in the tree. Run from the Docs root, after +# building refs.csproj and blockcheck.csproj. +# bash spec/018-compile_residual/probe/run.sh +set -euo pipefail +W=${1:?usage: run.sh }; P=spec/018-compile_residual/probe; P17=spec/017-compile_repairs/probe +bash "$P17/run.sh" "$W" # E1: report, stage, classes, types +cp "$W/types.tsv" "$W/types.full.tsv" # 017 § The tranches: Order is a page type +awk -F'\t' '!($1=="type" && $2=="Order" && $3=="StackExchange.Redis")' "$W/types.full.tsv" > "$W/types.tsv" +python3 "$P17/pages.py" "$W" "$W/r.tsv" # verdicts.tsv: BUILT STUB PARSE DEFECT +python3 "$P17/stubs.py" "$W" # stubs.tsv: BUILT MEMBERS HIDDEN SAME-PAGE +python3 tools/blockcheck.py --classify > "$W/cls.tsv" +python3 tools/blockcheck.py --list > "$W/list.tsv" +python3 "$P/tranche_pages.py" spec/017-compile_repairs/tasks.md > "$W/tranche.txt" +python3 "$P/offtable.py" "$W" > "$W/offtable.tsv" # one row per off-tranche page +python3 "$P/hardcat.py" "$W" # defcat.tsv, parcat.tsv, and the counts diff --git a/spec/018-compile_residual/probe/tranche_pages.py b/spec/018-compile_residual/probe/tranche_pages.py new file mode 100644 index 0000000..3820e01 --- /dev/null +++ b/spec/018-compile_residual/probe/tranche_pages.py @@ -0,0 +1,6 @@ +"""The 75 pages 017's tranches held: every `*.md` cell in its phase 2-5 tables.""" +import re, sys +text = open(sys.argv[1]).read() +start = text.index('### Phase 2 — tranche 1a'); end = text.index('## Blocks that stay FAILED') +pages = sorted(set(re.findall(r'^\| [^|]+ \| `([A-Za-z0-9]+\.md)`', text[start:end], re.M))) +print('\n'.join(pages)); print(f'{len(pages)} tranche pages', file=sys.stderr) diff --git a/spec/018-compile_residual/probe/v4pin.sh b/spec/018-compile_residual/probe/v4pin.sh new file mode 100644 index 0000000..23cf055 --- /dev/null +++ b/spec/018-compile_residual/probe/v4pin.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Spec 018 design probe E3: the pin with the seven AWS packages swapped for their V4 twins, +# and the six CS0234 blocks compiled against both pins -- the case, and the control. Also every +# BUILT block on an AWS-family page against the V4 pin, the other direction. +# bash spec/018-compile_residual/probe/v4pin.sh (after run.sh, which stages) +set -euo pipefail +W=${1:?usage: v4pin.sh }; D=tools/blockcheck/bin/Release/net9.0/blockcheck.dll +MAIN=tools/blockcheck/refs/bin/Release/net9.0/refs.txt +mkdir -p "$W/refsv4" +for p in DynamoDb Inbox.DynamoDB Locking.DynamoDB MessageScheduler.AWS MessagingGateway.AWSSQS Outbox.DynamoDB Transformers.AWS; do + printf 's#"Paramore.Brighter.%s" Version#"Paramore.Brighter.%s.V4" Version#\n' "$p" "$p" +done > "$W/refsv4/swap.sed" +sed -f "$W/refsv4/swap.sed" tools/blockcheck/refs/refs.csproj > "$W/refsv4/refsv4.csproj" +echo "V4 packages swapped in: $(grep -c '\.V4" Version' "$W/refsv4/refsv4.csproj")" +dotnet build "$W/refsv4/refsv4.csproj" -c Release > "$W/refsv4.log"; tail -3 "$W/refsv4.log" +V4="$W/refsv4/bin/Release/net9.0/refs.txt" +SIX=(AwsScheduler_2 AwsScheduler_3 DistributedLock_2 DynamoDbDistributedLock_1 DynamoDbDistributedLock_2 S3LuggageStore_1) +count() { awk -F'\t' 'NF>=4{c[$1]++; if($2=="CS0234")v[$1]++} END{for(k in c) print " "k": "c[k]" diagnostics, "(v[k]+0)" CS0234"}' | sort; } +echo "case: the six against the V4 pin"; dotnet "$D" --explain "$W/stage" "$V4" "${SIX[@]}" 2>/dev/null | count +echo "control: the six against the main pin"; dotnet "$D" --explain "$W/stage" "$MAIN" "${SIX[@]}" 2>/dev/null | count +AWS=($(awk -F'\t' '$1=="BUILT" && $2 ~ /AwsScheduler|\/DistributedLock\.md|DynamoDb|S3LuggageStore|AWSSQS|DynamoOutbox|DynamoInbox/{print $4}' "$W/r.tsv")) +echo "reverse: ${#AWS[@]} BUILT AWS-family blocks against the V4 pin -- each line is one that breaks" +dotnet "$D" --explain "$W/stage" "$V4" "${AWS[@]}" 2>/dev/null | awk -F'\t' 'NF>=4{print " "$1}' | sort | uniq -c diff --git a/spec/018-compile_residual/requirements.md b/spec/018-compile_residual/requirements.md new file mode 100644 index 0000000..edee78b --- /dev/null +++ b/spec/018-compile_residual/requirements.md @@ -0,0 +1,490 @@ +# Spec 018: Compile Residual — Requirements + +**Created:** 2026-10-04 +**Status:** **APPROVED 2026-10-04** — `.requirements-approved`. The seven open questions were approved open, so each recommendation is the design's working assumption. + +> Every number here carries the command that produced it, measured 2026-10-04 against Docs `master` +> `3a79b20`, Brighter `10.7.0` and `origin/master` `a7b3898aa`. Gate figures are **cited from +> `tools/README.md`**, rows 2 and 9, not restated as this document's own. + +> **The README re-derives, but it is not complete.** All six of its figures hold at `3a79b20` +> (§ *Current state*). It missed one input: **017's D3, a second pin for the AWS V4 packages, was ruled +> *"018"*** (`spec/017-compile_repairs/design.md` § decisions, D3). That pin holds **6** FAILED blocks +> on 4 pages, and it is P0-5 below. The README also carried only three of 017's fourteen friction +> repairs (#69, #71, #79). Each of the other eleven is placed below as P0, P1, P2 or out of scope, so +> that none is decided by omission. + +## Subject + +**Process.** The deliverable is a new `pagelint` rule, an extended `blockcheck` instrument, a second +pin, and repaired C# on existing pages. **It creates no page.** + +| Section | Status | +|---|---| +| **SUMMARY.md changes** | **N/A.** No page is added, moved or retitled. On a process spec this section is a category error, not an empty heading | +| **Mode mix** | **N/A.** No page changes what it is for | +| **Target audience** | **N/A as a design input**, per the subject table. Every repair still lands on a published page, so it is held to `CLAUDE.md`'s reader standards through § *Constraints*, as 017's were | + +## Topic overview + +016 built `tools/blockcheck.py`, which compiles every C# block on its own against the packages pinned +in `tools/blockcheck/refs/refs.csproj`. CI's `blocks` job enforces its baseline in both directions. +017 raised the baseline from 101 to 299 blocks across 75 tranche pages and closed on this sentence +(`spec/017-compile_repairs/tasks.md` § *What 017 shipped*): + +> **674 of 990 C# blocks still do not compile against the released packages, and 630 of them sit on +> the 64 pages 017's tranches never reached** … so 018 first builds `pagelint` rule 8 and gives +> `--classify` its second compile, and then tranches those 64 pages by what that instrument finds. + +That fixes the order: **instruments first, then pages.** There are two instruments, and both come +from 017: + +- **Rule 8** turns the attribute-mismatch probe into a standing gate. The compiler accepts a sync + handler attribute on `HandleAsync`, and Brighter throws `ConfigurationException` when it builds + the pipeline. So `blockcheck` would certify such a block as BUILT. The maintainer ruled the form on + 2026-09-29 (017 task 6.7, § *For the maintainer: D4*). +- **The second compile** lets one committed instrument draw the tranches. In 017 the tranche lists + came from an uncommitted probe in `spec/`, with `--classify` only as a lower bound. **162** blocks + that `--classify` called *import* had a defect behind the missing `using` (friction #71). + +A block that does not build is one a reader cannot paste and trust, and one the gate cannot hold. + +## Current state + +### The residual, re-derived + +```bash +dotnet build tools/blockcheck/refs/refs.csproj -c Release # 0 errors +dotnet build tools/blockcheck/blockcheck.csproj -c Release # 0 errors +python3 tools/blockcheck.py --report $S/r.tsv; echo $? # 0 +# baseline: 299 blocks required to build · scaffold rule: 45 units checked, 0 violations · 0 findings, 17 skipped +cut -f1 $S/r.tsv | sort | uniq -c # 299 BUILT, 674 FAILED, 17 SKIPPED; 990 rows +python3 tools/blockcheck.py --classify > $S/cls.tsv; echo $? # 0; 674 rows +python3 tools/pagelint.py | tail -1 # 0 errors, 524 warnings (… 66 pages) across 162 pages +``` + +These match `tools/README.md` row 9 (`bd95ee0`) and row 2. **Unmoved since then**: +`git diff --stat bd95ee0 3a79b20 -- contents/ tools/` → 3 files. In `contents/` that is two prose +lines in `Telemetry.md` and `ConfiguringOpenTelemetry.md` linking #4510, and no fence is touched. + +| Figure | Inherited (017 at `e0385b4`) | Measured at `3a79b20` | Method | +|---|---:|---:|---| +| BUILT / FAILED / SKIPPED | 299 / 674 / 17 | **299 / 674 / 17** | `--report`, then `cut -f1 \| uniq -c` on its rows | +| FAILED on the 75 tranche pages | 44 on 24 pages | **44 on 24** | `--classify` joined to the 75 page names in 017 § *The tranches* (phases 2–5 tables; `grep -oE` the `` `*.md` `` cells → 75) | +| FAILED on the other pages | 630 on 64 | **630 on 64** | the same join, complement | +| …by class | 314 import, 145 parse, 113 page-type, 34 values, 18 other, 6 same-page | **the same** | `Counter` over column 3 of the off-tranche rows | +| Off-tranche pages with nothing BUILT | 36 | **36** | off-tranche pages absent from `--report`'s BUILT rows | +| All pages with a FAILED block / nothing BUILT | 88 / 39 | **88 / 39** | `--report` rows alone | +| `pagelint` `using` debt | 524 / 66 pages | **524 / 66** | `pagelint`'s summary line | + +**`--classify`'s own stderr counts the whole corpus**, not the off-tranche share: *"323 import, 146 +parse, 130 page-type, 35 values, 21 other, 19 same-page"* (674). The off-tranche figures above are +those less the 44 tranche blocks. Both sum to 674, and that is the second method. + +### Where the 630 are + +By the `SUMMARY.md` `##` section each page is listed under (the join above, grouped): + +| Section | Pages | FAILED blocks | +|---|---:|---:| +| Scheduler | 8 | 129 | +| Commands, Handlers and Pipelines | 9 | 101 | +| Darker | 6 | 87 | +| Using an External Bus | 10 | 83 | +| Understanding Brighter | 4 | 50 | +| Brighter Configuration | 5 | 44 | +| Transports | 6 | 41 | +| V10 Migration | 3 | 37 | +| Outbox and Inbox | 9 | 26 | +| Reference | 1 | 17 | +| Health Checks and Observability | 2 | 9 | +| Get Started | 1 | 6 | +| **Total** | **64** | **630** | + +The heaviest pages are `HangfireScheduler.md` and `QuartzScheduler.md` with 27 each, then +`QueryPipeline.md` 25, `AwsScheduler.md` 23, and `AzureScheduler.md`, +`CommandProcessorConfigurationReference.md` and `ImplementAQueryHandler.md` with 21 each. +`QueryPipeline.md` is **12 *parse*** of its 25, and `KafkaConfiguration.md` is **14 of 20**. Neither +a `using` nor a stub reaches those blocks. + +### Rule 8's input, re-derived + +```bash +python3 spec/017-compile_repairs/probe/attr_mismatch.py; echo $? # 1 +# contents/PipelineValidation.md:250 RejectMessageOnError on a async handler +python3 spec/017-compile_repairs/probe/attr_mismatch.py --plant; echo $? # 0, "…: OK" +``` + +The one hit is **`PipelineValidation.md` #7**, the deliberate *Async Handler with Sync Attributes* +example. `blockcheck --list` puts block 7 at the fence opening at `:247`, and `:250` falls inside it. + +**The pair list, two refs.** The derivation, run in `../Brighter` at both refs: + +```bash +git grep -hoE 'class [A-Za-z]+Attribute' -- src | sed -E 's/class ([A-Za-z]+)Attribute/\1/' | sort -u > a +grep -E 'Async$' a | sed 's/Async$//' | sort -u | comm -12 - a +``` + +At `10.7.0` and at `origin/master` `a7b3898aa`, this gives the same **13** names: `BulkDepositCallSite DeferMessageOnError +DepositCallSite DontAckOnError FallbackPolicy FeatureSwitch Monitor RejectMessageOnError +RequestLogging UseInbox UsePolicy UseResiliencePipeline ValidateRequest`. These are the probe's +`PAIRED`, name for name. + +**Said:** *"`master` at `2461094a6` has the same 43 attribute classes"* (017 § *For the maintainer: +D4*). **Measured:** this command gives **52** class names at both refs. The 13 pairs agree, so the +rule's input is unchanged. The total depends on the command, and that is why `PAIRED`'s comment must +hold one (P0-1). + +**Nothing outside `spec/` runs the probe:** `git grep -l attr_mismatch -- .github tools` → no output. +**`pagelint` has no plant mechanism today:** `grep -n plant tools/pagelint.py` → no output. + +### The V4 pin, measured + +```bash +grep -rhoE 'Paramore\.Brighter[A-Za-z.]*\.V4[A-Za-z.]*' contents/ | sort | uniq -c # 8 spellings, 48 mentions +grep -rhoiE 'Paramore\.Brighter[A-Za-z.]*\.V4' contents/ | tr A-Z a-z | sort -u | wc -l # 7 packages +grep -rlE 'Paramore\.Brighter[A-Za-z.]*\.V4' contents/ | wc -l # 9 pages +ls ../Brighter/src | grep -i v4 # 8 directories +``` + +Eight spellings name seven packages, because `Inbox.DynamoDB.V4` also appears as `Inbox.DynamoDb.V4` +(below). The eighth source directory is `Paramore.Brighter.Tranformers.AWS.V4`, a misspelt sibling +of `Transformers.AWS.V4` that no page names. + +The blocks that D3 holds FAILED. Two methods agree on the same six: + +| Method | Blocks | +|---|---| +| Staged block text names `.V4` (`--stage`, `grep -l '\.V4'`), joined to FAILED rows | `AwsScheduler.md` #2, #3; `DistributedLock.md` #2; `DynamoDbDistributedLock.md` #1, #2; `S3LuggageStore.md` #1 | +| `--explain` over all 674 FAILED, `CS0234` naming `V4` | the same six | + +(A seventh staged block, `AWSSQSMigrateToV10.md` #7, names `.V4` and is BUILT. The match is outside +a `using`.) Three of the six are on 017 tranche pages and are listed in § *Blocks that stay FAILED* +as waiting for "D3, 018". `AwsScheduler.md` is off-tranche. + +**One page spells a package two ways.** `contents/DynamoInbox.md:21` writes +`Paramore.Brighter.Inbox.DynamoDb.V4`, while the project is `Paramore.Brighter.Inbox.DynamoDB.V4` +(`ls ../Brighter/src`). NuGet IDs are case-insensitive, so the name resolves. It is recorded here as +a fact for the defect ledger, not as a blocker. + +### The pin's framework + +`refs.csproj` and `blockcheck.csproj` both target **`net9.0`** (`grep -n TargetFramework +tools/blockcheck/*.csproj tools/blockcheck/refs/*.csproj`). 017's behaviour runs were `net10.0`, where a +reader of `TickerQScheduler.md` gets TickerQ 10.4.0, not the pin's 9.0.2 (friction #76). + +### 017's friction ledger, as it lands here + +All fourteen of 017's entries (67–80, `spec/017-compile_repairs/tasks.md` § *Friction ledger*), each +placed: + +| # | The repair 017 proposed | Here | +|---:|---|---| +| 67 | `blockcheck` aligns fences across refs by content | **P2-1** | +| 68 | Run the two reading criteria in every phase's close task | **P0-6**, obligation | +| 69 | Commit every reused helper and every behaviour run under `spec/018-*/probe/` | **P0-6**, obligation | +| 70 | `--classify` reads the receiver; a pin change lists name collisions | **P0-2** (D1) | +| 71 | `--classify` gets the probe's second stage | **P0-2** | +| 72 | Predict the off-tranche term from the recurrence greps | **P0-6**, obligation | +| 73 | A verified defect is repaired at every recurrence; only scope changes go to the maintainer; a *second pass* task per phase | **P0-6**, obligation | +| 74 | A ledger row only after its grep has run as written | **P0-6**, obligation | +| 75 | The `statements` wrapper lets a block name `Program` | **P1-2** | +| 76 | *Compiles against the released packages, not in the pin* as a verdict; measure `net9.0` vs `net10.0` | **P1-3** (the measurement); the verdict is **P2-2** | +| 77 | A scaffold unit that does not compile is a finding | **P1-4** | +| 78 | `pages.tsv` maps a unit per block | **P2-3** | +| 79 | Exempt the deliberate hit with a marker; name hits by page and block | **P0-1** and AC3 | +| 80 | `pagelint` per-page count; `optioncheck` primary-constructor options | per-page count **P1-5**; `optioncheck` **out of scope** | + +## Target state + +- `python3 tools/pagelint.py` checks **rule 8, `ATTRIBUTE KIND`**, on every page, as an error both + repo-wide and under `--changed`. CI proves that it can fail. `CLAUDE.md`'s ledger and conventions + describe it, and the 017 probe is no longer the instrument. +- `python3 tools/blockcheck.py --classify` answers the question the 017 probe answered: **what + remains once the `using`s are supplied.** Its classes say which blocks a `using` alone completes and + which hold a defect behind the `using`. +- A second pin compiles the AWS V4 blocks, so they are judged rather than parked. +- The 64 pages are tranched from that instrument's output, and repaired under 017's page-repair + rules. Each FAILED block on a reached page leaves in one of the four states 017's design defined, + and the baseline holds every block that builds. + +## Target audience + +**N/A as a design input** (§ *Subject*). Every page repair is still read by the ordinary reader +`CLAUDE.md` writes for. § *Constraints* binds that. + +## Source material + +- **017, the spec this continues.** `spec/017-compile_repairs/tasks.md`: § *What 017 shipped* (the + residual), § *For the maintainer: D4* (rule 8's proposal and the ruling), § *Friction ledger* + 67–80, § *Blocks that stay FAILED*, § *The tranches* (the recipe and the 75 pages), § 1 *Standing + obligations*. `spec/017-compile_repairs/design.md`: § *Page Repair Rules*, § *Scaffold Stub + Rules*, decisions D1–D4 +- **The probes being promoted.** `spec/017-compile_repairs/probe/attr_mismatch.py` (rule 8) and + `probe/run.sh`, `classify.py`, `usings.py`, `pages.py`, `typedump/` (the second compile) +- **The tools.** `tools/pagelint.py` (`APPLIES_TO` at `:164`, the `allow-serviceactivator` opt-out + at `:215`, `check_code_blocks` at `:475`, `main` at `:1188`), `tools/blockcheck.py` (`classify` + at `:254`, `CLASS_ORDER` and `classify_failure` at `:1198`–`:1204`), + `tools/blockcheck/refs/refs.csproj`, `tools/blockcheck/scaffold/pages.tsv`, + `tools/blockcheck/baseline.tsv`, `.github/workflows/docs.yml` (the `check` and `blocks` jobs) +- **The authorities.** `CLAUDE.md` § *Page Conventions*, § *Enforcement* and its ledger, § *Compiling + an example, and against what*; `tools/README.md` (gates, exit codes, *One phase is one pull + request*) +- **The product.** `../Brighter` at `10.7.0` (the pin) and `origin/master`, read-only: the attribute + classes under `src/`, and the `*.V4` projects under `src/` +- **The command files.** `.claude/commands/spec/*.md` + +## Scope + +### P0 — the spec is not done without these + +- **P0-1: `pagelint` rule 8, `ATTRIBUTE KIND`, as ruled 2026-09-29.** A handler attribute in + `PAIRED` whose sync or async form does not match the method it decorates is an error, repo-wide and + under `--changed`. It reads C# blocks through `pagelint.Page`, including blocks that do not + compile. Its parts: + - **`PAIRED`** is a tuple beside `APPLIES_TO` in `tools/pagelint.py`. Its comment holds the + derivation command above, so the version bump that edits `APPLIES_TO` re-derives `PAIRED` in + the same edit + - **The opt-out is per block, with a mandatory reason:** + `` on the line before the fence. A marker + with no reason is itself an error. A page-wide marker is not offered, because it would have + hidden `PipelineValidation.md` #9 and #10 beside the deliberate #7 + - **`PipelineValidation.md` #7** carries the marker, so repo-wide rule 8 hits read **0** + - **The red-proof is carried over** from the probe's `--plant`: two cases that must hit and one + that must not, plus a marker with no reason, run in CI as `pagelint --plant` (open question 1) + - **`CLAUDE.md`:** a ledger row, and a short § *Handler attributes match their handler* under + *Page Conventions*. **`tools/README.md`:** row 2's figures, and `pagelint`'s description, gain + rule 8 + - **The probe is retired.** It stays in 017 as history, and no 018 task runs it +- **P0-2: `--classify` gets its second compile (friction #71).** For every FAILED block, supply the + `using` directives that the pinned type table resolves, compile again, and classify what remains. + It stays committed, deterministic, read-only, and exit 2 on nothing to report, like the rest of + `blockcheck`'s modes. It must distinguish at least: + - a block that **builds** once given its `using`s + - a block that then fails **only on names no pinned package ships**: stub territory, 017's STUB + - a block that then fails on a **binder error that is not a missing name**: 017's DEFECT + - a block that **does not parse**, unchanged + + **Resolution reads the receiver (friction #70).** A name is resolved to a pinned type only when the + page does not declare it and the staged class's base does not supply it. Evidence: `Order` → + `StackExchange.Redis`'s enum (22 blocks on 10 pages), a handler's `Context` → `Polly.Context` + (17 rows on 5 pages), `AddOpenTelemetry` pinned only on `ILoggingBuilder`, and `Build` on + OpenTelemetry's builders. 017 ruled it *not repaired* (2026-09-27), when the cost was a reading + rule. Here, a by-name second compile would draw the tranche tables wrong. + + It is red-proofed with a two-way control: one block of each class, in a plant or a recorded run. + It is reconciled against 017's probe over the same 674 blocks, block by block, with every + disagreement explained. The output format is open question 2 +- **P0-3: tranche the 64 pages from P0-2's output, and repair them.** Tranches are drawn by the + committed instrument alone, so no tranche table carries a probe column. Repairs follow 017 + `design.md` § *Page Repair Rules* and § *Scaffold Stub Rules*, which 018's design cites and does + not restate. Every FAILED block on a reached page leaves in one of 017's four states. Each reached + page's remaining FAILED blocks are named in an 018 § *Blocks that stay FAILED*, with the reason. + How many pages 018 commits to is open question 3 +- **P0-4: the baseline holds what builds.** Each repair phase's new BUILT blocks enter + `tools/blockcheck/baseline.tsv` in the same PR, and `--report` exits 0 at every merge +- **P0-5: the AWS V4 pin (017 D3, ruled "018").** The seven `*.V4` packages the pages name are + pinned where `blockcheck` can compile against them, without breaking `refs.csproj`'s single-version + restore (017 found the V4 family cannot share it). The six blocks above are judged against it. The + form is a second project or a per-block pin selection: open question 4 +- **P0-6: 017's process repairs become 018's standing obligations.** Write them into 018's + `tasks.md` § 1, beside the programme's seven and 016's five, which 018 inherits by citation: + - **#69:** every helper used in more than one task, and each behaviour run's `Program.cs` with + its case and control, is committed under `spec/018-compile_residual/probe/`, and a run table's + row names its file + - **#68:** each repair phase's close task runs the two reading criteria (AC9, AC10 below) over + the blocks that phase touched, so the acceptance walk re-reads criteria already met + - **#72:** a phase's gate prediction includes the off-tranche term, predicted from the recurrence + greps + - **#73:** a defect verified at the pinned release is repaired at every recurrence in the phase + that finds it. Only a change of scope (a page rewritten, a feature removed, an upstream issue) is + put to the maintainer. Every repair phase has a *second pass* task + - **#74:** a defect-ledger row is written only after its grep has run as written. Its *Page* + column names pages. A carried defect is closed by editing its row + - **#79:** a criterion names a hit by page and block, never by line +- **P0-7: a handler wrapper.** A members-shaped block that overrides `Handle`, `HandleAsync`, + `Execute` or `ExecuteAsync` is staged in a class deriving from `RequestHandler`, + `RequestHandlerAsync`, `QueryHandler` or `QueryHandlerAsync`, with the type + arguments read from its signature. A block whose signature names no request type is not wrapped. + It reaches 32 FAILED blocks (design E5) +- **P0-8: `Shouldly` in `refs.csproj`.** Six blocks on `TestDoubleOptions.md` use its assertions + (design E2) +- **P0-9: two more accepted skip reasons**, beside 017's three: *"forthcoming: ships after Brighter + 10.7.0, as the page says at line N"*, and *"a single option shown alone; its type is named in the + sentence before it"*, allowed only where that type resolves in the pin. Every other fragment is + made whole under 017's rule +- **P0-10: `AddServiceActivator`, dead and shown as current.** `AwsScheduler.md:290` and + `AzureScheduler.md:231` are rewritten to `AddConsumers`. `tools/symbolwatch.tsv` gains a row for + the name, with per-symbol opt-outs on `V10MigrationGuide.md` and `FAQ.md`, which discuss it as the + V9 name. The row and the repairs merge together (`tools/README.md`, rule 3) + +### P1 — should, if the instruments leave room + +- **P1-2: the `statements` wrapper lets a block name `Program` (friction #75)**, by staging such a + block as a top-level file, as P1-3 of 017 answered `args` +- **P1-3: measure what the `net9.0` pin misses against `net10.0` (friction #76)**, before 018's + tranches are drawn: which pinned packages resolve differently, and which verdicts would move +- **P1-4: a scaffold unit that does not compile is a finding, exit 1 (friction #77)**, as a + unit-rule violation is +- **P1-5: a `pagelint` per-page warning count (friction #80)**, so a phase's debt delta is a diff + of two outputs + +### P2 — later, recorded + +- **P2-1:** `blockcheck` aligns fences across two refs by content, and reports *inserted*, + *removed* and *renumbered* (friction #67) +- **P2-2:** *compiles against the released packages, not in the pin* as a verdict the gate reports + and re-checks (friction #76's second half) +- **P2-3:** `pages.tsv` maps a scaffold unit per block (friction #78) + +## Out of scope + +- **New pages, moved pages, retitled pages.** A block that cannot be repaired without rewriting its + page is named in § *Blocks that stay FAILED*, and the rewrite is put to the maintainer (#73) +- **Changes to Brighter or Darker source**, which is read-only (`CLAUDE.md` § *Key Constraints*). A + defect found in the product is filed upstream on the maintainer's word, as 017's four and #4510 + were. **Samples** fall under the narrow exception, and each sample needs its own per-PR ask +- **`optioncheck` construction of primary-constructor options** (friction #80's second half). It is + a different gate, and nothing in this spec's residual depends on it +- **The `v9` branch.** It has no banner and no gate, and it is published as superseded +- **Running every behavioural claim on the 64 pages.** Only blocks a repair *changes* owe a run with + a control (AC10). Claims on untouched blocks are 019's or nobody's, and are not silently implied + here + +## Deliverables + +No page is created, so no page type is chosen. The repaired pages keep the types their banners +already declare (`pagelint` rule 2). + +| Deliverable | File(s) | +|---|---| +| Rule 8 | `tools/pagelint.py` (`PAIRED`, the rule, the opt-out, the `--plant` mode) | +| Rule 8 in CI | `.github/workflows/docs.yml`, `check` job: `python3 tools/pagelint.py --plant` | +| Rule 8 documented | `CLAUDE.md` § *Page Conventions* (new subsection) and § *The ledger* (new row); `tools/README.md` rows 2 and 9 and *What each gate actually checks* | +| The marker | `contents/PipelineValidation.md`, above block 7 | +| The second compile | `tools/blockcheck.py` (`--classify`), and the C# half under `tools/blockcheck/` if a mode is needed there | +| The V4 pin | `tools/blockcheck/refs-v4/refs-v4.csproj`, or as open question 4 decides; `.github/workflows/docs.yml` `blocks` job | +| Repaired pages | the 64 pages, or the subset open question 3 settles, under `contents/` | +| The baseline | `tools/blockcheck/baseline.tsv`; scaffold units under `tools/blockcheck/scaffold/` and `pages.tsv` | +| Instruments that outlive a task | `spec/018-compile_residual/probe/` (#69) | +| The record | `spec/018-compile_residual/design.md`, `tasks.md` (ledgers, run tables, § *What 018 shipped*) | + +## SUMMARY.md changes + +**N/A.** No page is added, moved or retitled (§ *Subject*). `python3 tools/linkcheck.py`'s orphan +check stays at 0 and confirms it. + +## Constraints + +- **`CLAUDE.md` governs every repaired page**, and is cited rather than restated: banner, heading + qualification, `using` directives, version markers, and compiling against the released packages +- **Brighter and Darker are read-only.** Sample additions happen only by PR, and each needs a + fresh per-PR ask with the reuse/extend survey done +- **One phase is one pull request** (`tools/README.md`). The tool-only phase changes one page (the + marker), so it is put to the maintainer for sign-off rather than assumed exempt +- **A gate and the corpus that satisfies it merge together** (`tools/README.md`, rule 3). Rule 8 and + `PipelineValidation.md`'s marker are in the same PR, or `master` goes red +- **No `--baseline `, and no mode that writes `baseline.tsv`** (`tools/README.md`). P0-2 and + P0-5 must not add either +- **Exit 2 is never swallowed**, so a new mode or CI step carries no `|| true` +- **`refs.csproj` keeps its single-version restore.** The V4 pin must not downgrade or break it + +## Acceptance criteria + +| # | Criterion | Instrument | +|---|---|---| +| **AC1** | Rule 8 exists, and fails when it should | `python3 tools/pagelint.py --plant` exits **0** only if both mismatch plants hit and the matched pair is silent, and a red-proof run with a plant **removed** exits non-zero, recorded in `tasks.md`. Two-way. **Built by P0-1; until it exists, this criterion has no instrument** (`--plant` is not a `pagelint` mode at `3a79b20`) | +| **AC2** | Rule 8 is clean on the corpus, with the deliberate example marked | `python3 tools/pagelint.py` → `0 errors`, and `grep -c 'attr-mismatch-intended' contents/PipelineValidation.md` → **1**. A red-proof run with the marker removed reports `ATTRIBUTE KIND` on `PipelineValidation.md` block 7, recorded | +| **AC3** | The opt-out cannot be silent | A marker with no reason is one of `--plant`'s cases and must be reported as an error, so AC1's command decides it. **Built by P0-1; no instrument until then** | +| **AC4** | `CLAUDE.md` and `pagelint` agree on rule 8, in both directions | **No instrument — checked by reading**, by the reviewer: the ledger row, the convention section and the rule's message are read against each other. `pagelint` has no self-check of its ledger, and that is the reason this criterion is a reading | +| **AC5** | The probe is retired | `git grep -l attr_mismatch -- .github tools contents` → no output (exit 1; it reads that today, because nothing outside `spec/` has ever run the probe). That **no 018 task runs it**: `grep -n 'attr_mismatch' spec/018-compile_residual/tasks.md`, each hit **read** by the reviewer, because no grep can tell a task's *Input* from a quotation of 017 | +| **AC6** | `--classify` compiles twice, and can be wrong in both directions | A red-proof with at least one block of each class (built-by-`using`, stub, defect, parse) classifies each correctly. A control block known to be defect-behind-`using` is **not** classified as built. Recorded in `tasks.md`. **Built by P0-2; no instrument until then** | +| **AC7** | `--classify` agrees with 017's probe, or says why not | A block-by-block join of the new `--classify` against 017's `probe/run.sh` + `pages.py` at the same ref, over the FAILED set. Every disagreeing block is listed with its reason. Two methods, recorded. **The probe half runs today; the join needs P0-2** | +| **AC8** | The tranches are the instrument's | **No instrument — checked by reading**, by the reviewer: every tranche table's columns derive from `--classify`'s output by a command shown beside the table, and no column comes from a probe | +| **AC9** | Every repaired block's `// ...` is justified | **No instrument — checked by reading**, by the reviewer, **at every phase's close** (#68): each `// ...` the phase adds, against `--explain`, as 017 task 6.1 did. 017's AC8 second half was unmet at acceptance because this ran only once | +| **AC10** | Behavioural claims in changed blocks are run with a control | **No instrument — checked by reading**, by the reviewer, **at every phase's close** (#68): the phase's changed blocks (`git diff -U0` hunks against fence ranges), each claim mapped to a run whose `Program.cs` is committed under `probe/` (#69) with its case and control. 017's AC11 was unmet at acceptance on 30 blocks | +| **AC11** | The V4 blocks are judged | Each of the six blocks in § *The V4 pin* is BUILT, or FAILED with a diagnostic that is not `CS0234 … V4`: `--explain` on the six, then `grep -c 'CS0234.*V4'` → **0** | +| **AC12** | The residual falls, and the gate holds it | `--report` exits **0** with BUILT above **299**, and every new BUILT block is in `baseline.tsv` (enforced by `--report` itself). The target figure is set at design (open question 3) | +| **AC13** | Every FAILED block on a reached page is accounted for | For each reached page, `--report`'s FAILED rows equal 018 § *Blocks that stay FAILED*'s rows for that page: a join, zero rows either side. **No instrument until that section exists in `tasks.md`**; the join command is written beside it, as 017's AC4 was | +| **AC14** | `tools/README.md` owns every new figure | The corrected-form count from `tools/README.md` (`grep -rn '
' … \| grep -vcE '^(\./)?spec/'`) → **1** for each new gate figure 018 introduces. The form runs today (`'299 BUILT'` → **1**); its inputs are the figures the phases produce | + +## Open questions + +1. **Rule 8's plant: a `--plant` mode or a plant file?** *Recommendation:* a **`--plant` mode** + holding its cases in memory, as the probe's `--plant` does, so that the next rule's plants join + it. A plant file is the costlier form: `pagelint` refuses any path outside `contents/` + (`python3 tools/pagelint.py ` → *"not a page under contents/"*, exit 2), + so a file would mean loosening that refusal. A mode does not touch it. *Depends on:* nothing + further, since the dependency has been measured. +2. **What does `--classify` print for each new class?** *Recommendation:* keep the four-column + `page·ordinal·class·names` row, and add classes rather than columns: `built-by-using`, `stub`, + `defect`, with `parse` unchanged. The first-compile class goes in a fifth column, so the 017-style + lower bound and the true figure both stay visible. *Depends on:* nothing committed parsing the + four columns, and nothing does. `git grep -n -- '--classify' tools .github` finds only + `blockcheck` itself and `tools/README.md`'s description, which the change rewrites. +3. **How many of the 64 pages does 018 commit to?** *Recommendation:* draw the tranches after P0-2 + runs. Commit to every page P0-2 shows as reachable by a `using` or a stub, and set the + hard-block pages' share by count at design, as 017's ≤ 60 target was. Do not promise all 64 up + front: **145** *parse* blocks are an unknown quantity, and **26** of them are on two pages + (`QueryPipeline.md` 12 of its 25, `KafkaConfiguration.md` 14 of its 20). *Depends on:* P0-2's + output. +4. **The V4 pin: a second project, or a per-block pin selection?** *Recommendation:* **a second + project**, `refs-v4.csproj`, with the AWS V4 packages and Brighter 10.7.0, selected per page in + `pages.tsv` by a column. It keeps `refs.csproj`'s restore untouched and costs one more build step + in CI. *Depends on:* whether the V4 packages and the V3 packages that the same pages also name + can share one restore. If not, the six blocks need a per-block selection, which is P2-3's shape. +5. **Is P1-1, the receiver-aware `--classify`, re-put to the maintainer before P0-2 or after?** + *Recommendation:* **before.** P0-2 supplies `using`s from the type table, so without the receiver + it supplies `using StackExchange.Redis;` for every page-own `Order` (22 blocks, 10 pages at 017 + task 6.2). The second compile then reports a defect that is the instrument's, not the page's. + *Depends on:* the maintainer, who ruled it *not repaired* on 2026-09-27. +6. **Does phase 1 (rule 8, the second compile, the V4 pin) ship as one PR or three?** + *Recommendation:* **two.** Rule 8 and its marker go first, because they are small and gate-shaped + and change one page. The second compile and the V4 pin go second, because the tranches depend on + both and neither changes a page. *Depends on:* question 4's answer. A V4 pin that changes + verdicts changes the baseline, and so belongs with the first page repairs. +7. **Is P1-3, the `net10.0` measurement, really P1?** *Recommendation:* **keep it P1, but run it + before the tranches are drawn**, as friction #76 asks. It is a measurement, not a change, and it + can only move tranche boundaries. If it shows verdicts moving, raise it to P0 at the design + review. *Depends on:* nothing but a build. + +## Maintainer rulings — 2026-10-04, at the design review + +| Ruling | Effect on this document | +|---|---| +| **D1:** the receiver-aware `--classify` is P0 | P1-1 is merged into P0-2, under *Resolution reads the receiver*. P1-1's number is retired, and the other P1 items keep theirs. Open question 5 is answered | +| **D2:** a handler wrapper | P0-7 | +| **D3:** the two skip reasons, the second narrowly | P0-9 | +| **D4:** pin Shouldly | P0-8 | +| **D5:** repair `AddServiceActivator` and watch for it | P0-10 | + +The evidence for each is in `design.md` § *The Experiments* and § *Design Decisions*. + +## What the review found — 2026-10-04 + +Every criterion's instrument was run at `3a79b20`. AC2 → `0 errors` (vacuous until rule 8 exists, which is why AC2 also +asks for its red-proof); AC5 → no output; AC11 → **11** `CS0234 … V4` lines across exactly the six +blocks, red as it should be; AC12 → **299**, red as it should be; AC14's form → **1**. + +| # | Found | Now | +|---:|---|---| +| 1 | AC1, AC3, AC6, AC7, AC13 and AC14 named instruments that do not exist at `3a79b20`: a plant mode, a new `--classify`, an 018 section. A criterion whose instrument cannot run is a criterion with none | Each says what builds its instrument and that it has none until then | +| 2 | AC5's second half was `grep -c … outside § history quotes`. No grep can exclude a quotation | A grep to find the hits, and the reviewer reads each | +| 3 | Open question 3 said 25 *parse* blocks on two pages. 12 + 14 = **26** | 26 | +| 4 | Open question 1 depended on whether `pagelint` takes a path outside `contents/`. It does not: exit 2 | Measured. The recommendation changed from a plant file to a `--plant` mode | +| 5 | Open question 2 depended on committed consumers of `--classify`'s columns. There are none outside `blockcheck` | Measured. The recommendation stands | + +## Quality checklist + +- [x] Readable with no prior context: the topic overview quotes 017's residual, and every class and + term used (*import*, *parse*, *page-type*, STUB, DEFECT, tranche) is defined at first use or + cited to `tools/README.md` +- [x] P0 / P1 / P2 are separate lists, and each of 017's fourteen friction entries is placed +- [x] Specific sources: files, line numbers and refs, not "the tools" +- [x] A command beside every number. The figures quoted from 017 (`162`, `22 on 10`, `30`) are + cited to their 017 section, and marked as 017's +- [x] Every acceptance criterion names its instrument or says *no instrument — checked by reading*, + and who reads it: AC4, AC8, AC9 and AC10. AC9 and AC10 are the two that 017 found unmet, + and they now run at every phase's close +- [x] Criteria say *contains* where they mean it, and name hits by page and block (#79) +- [x] N/A sections are named and marked N/A, not left empty From 1b6f7f9326fa79c5fd1d670d29ef2bdab34026a3 Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 5 Oct 2026 08:20:19 +0100 Subject: [PATCH 2/4] =?UTF-8?q?spec:=20018=20design=20approved=20=E2=80=94?= =?UTF-8?q?=20five=20review=20findings=20repaired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E6 trials the AddServiceActivator watchlist row (22 -> 23 entries, 3 -> 8 silenced; red run 7 sites on 4 pages). Adds the handler wrapper's design, pins Shouldly at 4.3.0 (Brighter's own) with the page repair it needs, makes AC6's control runnable, and completes the requirements' deliverables for P0-7 to P0-10. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjnH2pJ98aMabu1MyTpRxy --- spec/018-compile_residual/.design-approved | 0 spec/018-compile_residual/README.md | 4 +- spec/018-compile_residual/design.md | 68 ++++++++++++++++++++-- spec/018-compile_residual/requirements.md | 8 ++- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 spec/018-compile_residual/.design-approved diff --git a/spec/018-compile_residual/.design-approved b/spec/018-compile_residual/.design-approved new file mode 100644 index 0000000..e69de29 diff --git a/spec/018-compile_residual/README.md b/spec/018-compile_residual/README.md index 9583ac3..a72fec6 100644 --- a/spec/018-compile_residual/README.md +++ b/spec/018-compile_residual/README.md @@ -1,7 +1,7 @@ # Spec 018: Compile Residual **Created:** 2026-10-04 -**Status:** Design Phase — `design.md` drafted 2026-10-04, awaiting `/spec:review` +**Status:** Tasks Phase — design approved 2026-10-05 > **Re-derive this README before executing it.** It was written before anyone looked — check every > count and every named gap against the tree, with the command beside the figure. @@ -96,7 +96,7 @@ Provisional. `/spec:requirements` settles them. - [x] Requirements gathered — `requirements.md`, 2026-10-04 - [x] Requirements reviewed and approved — 2026-10-04 - [x] Documentation outline created — `design.md`, 2026-10-04 -- [ ] Outline reviewed and approved +- [x] Outline reviewed and approved — 2026-10-05 - [ ] Writing tasks identified - [ ] Writing complete - [ ] Documentation reviewed diff --git a/spec/018-compile_residual/design.md b/spec/018-compile_residual/design.md index c9fe03e..ae7af15 100644 --- a/spec/018-compile_residual/design.md +++ b/spec/018-compile_residual/design.md @@ -1,7 +1,7 @@ # Spec 018: Compile Residual — Design **Created:** 2026-10-04 -**Status:** Draft, for `/spec:review` +**Status:** **APPROVED 2026-10-05** — `.design-approved`. D1–D5 ruled 2026-10-04; five review findings repaired (§ *What the Design Review Found*). **Requirements:** approved 2026-10-04 (`.requirements-approved`), seven open questions approved open > Every number here was measured 2026-10-04 at Docs `master` `3a79b20`, Brighter `10.7.0` and @@ -162,6 +162,21 @@ or `ExecuteAsync`, over the whole corpus, by verdict. `base.HandleAsync` and the like, because the `members` wrapper stages them in a `Holder` with no base class (`tools/blockcheck.py:355–368`). This is the `wrapper` bucket of E2 from the other side. +### E6 — the `AddServiceActivator` row, trialled + +**Method.** In a scratch worktree at `f5962ff`, one row is appended to `tools/symbolwatch.tsv`: +`AddServiceActivator`, `brighter`, `AddConsumers`. `python3 tools/symbolcheck.py` then runs twice. + +| Run | Exit | Output | +|---|---:|---| +| **Case**: the row alone | **1** | *"7 site(s) across 4 page(s), from 23 watchlist entries"*: `AwsScheduler.md:290`, `AzureScheduler.md:231`, `FAQ.md:126`, `:580`, `V10MigrationGuide.md:171`, `:199`, `:207` | +| **Control**: plus `` on `FAQ.md` and `V10MigrationGuide.md`, and the two sites rewritten to `AddConsumers` | **0** | *"23 entries, 161 pages checked, 8 silenced"*: `FAQ.md` ×2, `V10MigrationGuide.md` ×3 | + +`symbolcheck` counts silenced **lines**, not pages, so the five discussion lines are five sites. The +worktree was removed afterwards. In phase 3 the opt-out goes on its own line beside the page's first +site, as `S3LuggageStore.md:50` does, and not under the banner, where rule 7 reads the opening +sentence. + ## API Resolved For This Design ```bash @@ -307,14 +322,45 @@ needed: the recompile is the existing `--explain` over a rewritten stage. **Red-proof (AC6).** One recorded run over five named blocks, each with a known answer. A `built-by-using` block (one of the 34); a `defect` behind a `using` (one of the 50 *api*); a `page-type` block; a `parse` block; and the **control**: a block that page-declares a type also -pinned elsewhere (an `Order` block), which must **not** come out `built-by-using`. A second run -with the page-declared check disabled must turn the control `built-by-using`, which shows the -check is what decides it. +pinned elsewhere (an `Order` block), which must **not** come out `built-by-using`. A second run, +from a scratch copy of `blockcheck.py` with the page-declared check removed, must turn the control +`built-by-using`. That shows the check is what decides it, and the scratch copy is never committed. **Reconciled (AC7).** A block-by-block join of the new `--classify` against `probe/run.sh`'s `verdicts.tsv` at the same ref. Every disagreement is listed with its reason. The expected disagreements are E4's 39 and 29, each read by the probe by name and by `--classify` by receiver. +## The Handler Wrapper (D2) + +**Which blocks.** A block that `blockcheck` already shapes `members` (`tools/blockcheck.py:263`), +whose text overrides `Handle`, `HandleAsync`, `Execute` or `ExecuteAsync`. E5 counts **32** FAILED +blocks today, and none BUILT. So no baselined block changes its staging, and the gate cannot lose +a row by this change. + +**What it stages.** The `members` wrapper's `public class Holder` gains a base class. The base class +is read from the method, and its type arguments from the signature: + +| Method | Base | Type arguments | +|---|---|---| +| `Handle(TRequest …)` | `Paramore.Brighter.RequestHandler` | the parameter's type | +| `HandleAsync(TRequest …)` | `Paramore.Brighter.RequestHandlerAsync` | the first parameter's type | +| `Execute(TQuery …)` | `Paramore.Darker.QueryHandler` | the parameter's type; the return type | +| `ExecuteAsync(TQuery …)` | `Paramore.Darker.QueryHandlerAsync` | the first parameter's type; `T` of the returned `Task` | + +**The base is written fully qualified**, so the wrapper adds no `using`. A block that needs +`using Paramore.Brighter;` for its own names still fails without it, and `pagelint` rule 6's debt is +untouched. All four bases are live (§ *API Resolved*). + +**When it does not wrap.** The block keeps today's base-less `Holder` when its signature cannot be +read, or when two overrides name different request types. A one-line regex reads **27** of the 32 +signatures. The other 5 (`BrighterInboxSupport.md` #1, `EFCoreQueryIntegration.md` #1–#3, +`ShowMeTheCode.md` #5) break the signature across lines, so the reader must join lines up to the +opening `{`. Phase 2's task reports how many of the 32 it wraps, and names each one it does not. + +**Red-proof.** Four plants, one per row of the table, each must stage with its base and build. A +control, a members block with no override, must stage exactly as today: `--stage` output +byte-identical to `f5962ff` for every non-handler members block. + ## The V4 Pin ```text @@ -358,7 +404,7 @@ runs in every tranche PR"*, is replaced by rule 8 the gate, which now runs on ev | A forthcoming API, which the page already says ships after the pinned release | 4 | SKIPPED, reason *"forthcoming: ships after Brighter 10.7.0, as the page says at line N"* | **D3** | | A single option, argument or expression shown alone, with no enclosing call on the page | ≤ 64 | SKIPPED, reason *"a single option shown alone; its type is named in the sentence before it"*, where that type resolves in the pin | **D3** | | A handler method shown without its class | 32 | built by the handler wrapper | **D2** | -| Shouldly assertions | 6 | `Shouldly` pinned in `refs.csproj` | **D4** | +| Shouldly assertions | 6 | `Shouldly` **4.3.0** pinned in `refs.csproj`, the version Brighter's own tests use (`Directory.Packages.props:150` at 10.7.0). The page calls `ShouldBe…` and never names the package (`grep -c Shouldly contents/TestDoubleOptions.md` → **0**), so its repair adds `using Shouldly;` to each block and one sentence naming the package | **D4** | | `AddServiceActivator` as current code | 2 sites | rewrite to `AddConsumers`, add a `symbolwatch.tsv` row, and opt out the two V9 discussion pages | **D5** | **And from 017's friction, as standing obligations in `tasks.md` § 1** (P0-6): #68 (both reading @@ -444,7 +490,7 @@ cited from its rows, not restated. | shape / redirects / `--verify` | **unmoved** | **unmoved** | **unmoved** | no `SUMMARY.md` change | | `versioncheck` | **unmoved** | **unmoved** | **unmoved** | no tutorial pin touched. A repair that adds one is predicted in its phase | | `optioncheck` | **unmoved** | **unmoved** | **unmoved** | no option table touched | -| `symbolcheck` | **unmoved** | **unmoved** | phase 3: **entries 22 → 23**, and **silenced** rises by the opt-out sites on `V10MigrationGuide.md` and `FAQ.md` (D5); otherwise unmoved | the `AddServiceActivator` row and its opt-outs | +| `symbolcheck` | **unmoved** | **unmoved** | phase 3: **22 → 23 entries, 3 → 8 silenced** (E6, D5); otherwise unmoved | the `AddServiceActivator` row and its opt-outs | | `blockcheck` corpus | **unmoved, 990** | **unmoved** | **moves both ways**: retags (−3 for E2's JSON) and fence splits (+ for E2's 15 two-snippet and 7 mixed blocks); predicted per phase | a split adds a block | | `blockcheck` BUILT | **unmoved, 299** | **rises by the blocks the handler wrapper (D2), the `Program` fix (#75) and the Shouldly pin (D4) make build**, measured by phase 2's first task before any row is written. Up to 32 + 6, less those that fail for another reason | **rises**, to ≥ 560 by the close | | | `blockcheck` scope lines | unmoved | **+1 line**, the V4 pin's assemblies; the main pin's assemblies **rise** by Shouldly's (D4); the scaffold rule also checks the `v4` column | units rise with each phase's stubs | | @@ -463,6 +509,16 @@ D1–D5 were ruled by the maintainer on 2026-10-04, each as recommended. D6 and | **D6** | **Is the V4 pin's selection per page enough?** | **Yes, today.** E3: every BUILT block on the four V4 pages builds under V4. Per-block selection stays P2-3, triggered only by a page that mixes V3 and V4 blocks | — | | **D7** | **Raise P1-3, the `net10.0` measurement, to P0?** | **No, but run it in phase 2 before the tables are drawn**, as friction #76 asks. Raise it if it moves a verdict | — | +## What the Design Review Found — 2026-10-05 + +| # | Found | Now | +|---:|---|---| +| 1 | `symbolcheck`'s phase 3 movement was *"silenced +n"*, unmeasured | E6 trials the row: **22 → 23 entries, 3 → 8 silenced**, with a red run of 7 sites on 4 pages | +| 2 | D2 was ruled, but the design had no section saying how the wrapper chooses a base class | § *The Handler Wrapper*: four bases, fully qualified; 27 of 32 signatures read by one line, 5 need joined lines; a red-proof with a byte-identical control | +| 3 | D4 pinned Shouldly with no version, and missed that the page never names it | 4.3.0, Brighter's own; the page repair adds `using Shouldly;` and names the package | +| 4 | AC6's control said *"with the check disabled"*, with no way to disable it | a scratch copy of `blockcheck.py`, never committed | +| 5 | `requirements.md` § *Deliverables* lacked P0-7 to P0-10's files, and two rows still deferred to answered questions | rewritten (that section is not quoted by any later step) | + ## Design Quality Checklist - [x] Readable with no prior context: it opens with the subject, the deltas and the experiments, and diff --git a/spec/018-compile_residual/requirements.md b/spec/018-compile_residual/requirements.md index edee78b..f57e981 100644 --- a/spec/018-compile_residual/requirements.md +++ b/spec/018-compile_residual/requirements.md @@ -363,8 +363,12 @@ already declare (`pagelint` rule 2). | Rule 8 documented | `CLAUDE.md` § *Page Conventions* (new subsection) and § *The ledger* (new row); `tools/README.md` rows 2 and 9 and *What each gate actually checks* | | The marker | `contents/PipelineValidation.md`, above block 7 | | The second compile | `tools/blockcheck.py` (`--classify`), and the C# half under `tools/blockcheck/` if a mode is needed there | -| The V4 pin | `tools/blockcheck/refs-v4/refs-v4.csproj`, or as open question 4 decides; `.github/workflows/docs.yml` `blocks` job | -| Repaired pages | the 64 pages, or the subset open question 3 settles, under `contents/` | +| The V4 pin | `tools/blockcheck/refs-v4/refs-v4.csproj`; the `v4` column of `tools/blockcheck/scaffold/pages.tsv`; `.github/workflows/docs.yml` `blocks` job | +| The handler wrapper (P0-7) | `tools/blockcheck.py` (`WRAPPERS`, the shape reader) | +| Shouldly (P0-8) | `tools/blockcheck/refs/refs.csproj`; `contents/TestDoubleOptions.md` | +| The skip reasons (P0-9) | `design.md` § *Page Repair Rules*, beside 017's | +| `AddServiceActivator` (P0-10) | `tools/symbolwatch.tsv`; `contents/AwsScheduler.md`, `AzureScheduler.md`, `FAQ.md`, `V10MigrationGuide.md` | +| Repaired pages | the 64 pages under `contents/`, and the 3 tranche pages that hold V4 blocks | | The baseline | `tools/blockcheck/baseline.tsv`; scaffold units under `tools/blockcheck/scaffold/` and `pages.tsv` | | Instruments that outlive a task | `spec/018-compile_residual/probe/` (#69) | | The record | `spec/018-compile_residual/design.md`, `tasks.md` (ledgers, run tables, § *What 018 shipped*) | From 77b71133f526df434049d3935ea77578d6b99785 Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 5 Oct 2026 08:28:37 +0100 Subject: [PATCH 3/4] =?UTF-8?q?spec:=20018=20tasks=20drafted=20=E2=80=94?= =?UTF-8?q?=20eight=20phases,=2061=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standing obligations cite the programme's seven, 016's 8-12 and 017's 13/14/16/17 (15 replaced by rule 8), and add six from 017's friction (#68 #69 #72 #73 #74 #79). Counts re-derived at 1b6f7f9 by two methods. Phases follow the design: rule 8, instruments, S1-S5 by section group, acceptance. Three defect rows from the design opened. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjnH2pJ98aMabu1MyTpRxy --- spec/018-compile_residual/README.md | 4 +- spec/018-compile_residual/tasks.md | 493 ++++++++++++++++++++++++++++ 2 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 spec/018-compile_residual/tasks.md diff --git a/spec/018-compile_residual/README.md b/spec/018-compile_residual/README.md index a72fec6..91e9283 100644 --- a/spec/018-compile_residual/README.md +++ b/spec/018-compile_residual/README.md @@ -1,7 +1,7 @@ # Spec 018: Compile Residual **Created:** 2026-10-04 -**Status:** Tasks Phase — design approved 2026-10-05 +**Status:** Tasks Phase — `tasks.md` drafted 2026-10-05, awaiting `/spec:review` > **Re-derive this README before executing it.** It was written before anyone looked — check every > count and every named gap against the tree, with the command beside the figure. @@ -97,7 +97,7 @@ Provisional. `/spec:requirements` settles them. - [x] Requirements reviewed and approved — 2026-10-04 - [x] Documentation outline created — `design.md`, 2026-10-04 - [x] Outline reviewed and approved — 2026-10-05 -- [ ] Writing tasks identified +- [x] Writing tasks identified — `tasks.md`, 61 tasks, 2026-10-05 - [ ] Writing complete - [ ] Documentation reviewed - [ ] Spec closed diff --git a/spec/018-compile_residual/tasks.md b/spec/018-compile_residual/tasks.md new file mode 100644 index 0000000..782b2b6 --- /dev/null +++ b/spec/018-compile_residual/tasks.md @@ -0,0 +1,493 @@ +# Spec 018: Compile Residual — Tasks + +**Status:** Draft, for `/spec:review` +**Requirements:** approved 2026-10-04 · **Design:** approved 2026-10-05 + +**Eight phases, 61 tasks, one pull request per phase.** Phases merge under `tools/README.md` +§ *One phase is one pull request*, in order, each before the next branch starts. Phase 1 changes one +page and phase 2 changes none. Phases 3–7 change the published site, and each needs the +maintainer's sign-off, with the head-ref deletion asked for by name. + +--- + +## 1. Standing obligations — stated once, binding every task + +They are not repeated per task. A task that seems to need one restated is a task that has +forgotten this section. + +**The programme's seven:** + +1. **Re-derive any count before quoting it**, with the command beside the figure and **two methods + that agree**. A figure inherited from `requirements.md`, `design.md` or an earlier phase is stale + until re-derived +2. **Record the mismatch before fixing it**, once, as a fact (*said A; measured B*), in this file's + ledgers. Then rewrite the text. Do not narrate how it was found, and do not leave the original + standing beside the correction +3. **A check that has never failed has not been shown to work.** Every new check gets a red-proof + with its output recorded here, and **every control is two-way**: a known-present case and a + known-absent one +4. **Prose and permission ship together**, read in both directions. Any command file a phase + touches is checked: every tool its prose names is granted, and every grant is reached by its + prose +5. **Cite `CLAUDE.md`, `tools/README.md` and `design.md`; never restate them** +6. **Predict gate movement before the work, including "none"**, with the mechanism. Then + reconcile, and explain any difference rather than adopting the new number +7. **Ask before merging anything that changes the published site**, and ask for the head-ref + deletion **by name** in the same breath + +**Inherited, by citation:** 016's obligations 8–12 (`spec/016-compile_gate/tasks.md` § 1), and 017's +13, 14, 16 and 17 (`spec/017-compile_repairs/tasks.md` § 1). 017's 15, *"`probe/attr_mismatch.py` +runs before any baseline row is written"*, is **replaced** by rule 8 from phase 1 on, which runs on +every push. + +**018 adds six, from 017's friction ledger** (requirements P0-6): + +18. **#69: an instrument outlives its session.** Every helper used by more than one task, and each + behaviour run's `Program.cs` with its case and its control, is committed under + `spec/018-compile_residual/probe/`. A run table's row names its file +19. **#68: the reading criteria run at every phase close**, not only at acceptance. The phase's + changed blocks (`git diff -U0` hunks against fence ranges) are read against AC9 (each added + `// ...` against `--explain`) and AC10 (each behavioural claim against a run with a control) +20. **#72: a prediction includes the off-group term.** It is predicted from the recurrence greps + run before any repair, which name the pages and blocks a recurrence will touch +21. **#73: a verified defect is repaired at every recurrence in the phase that finds it.** Only a + change of scope (a page rewritten, a feature removed, an issue filed upstream) is put to the + maintainer. Every repair phase has a *second pass* task where a ruling lands +22. **#74: a defect-ledger row is written only after its grep or probe has run as written.** Its + *Page* column names pages. A carried defect is closed by editing its row, never by opening a + second +23. **#79: a hit is named by page and block, never by line**, in criteria, predictions and + ledgers + +--- + +## 2. Measured at tasks — 2026-10-05, `1b6f7f9` + +`git diff --stat 3a79b20 1b6f7f9 -- contents tools` → empty, so no verdict can have moved since +the design measured. + +| Figure | Method 1 | Method 2 | Agrees? | +|---|---|---|---| +| BUILT | `--report` rows, `cut -f1 \| uniq -c` → **299** | `grep -vc '^#' tools/blockcheck/baseline.tsv` → **299** | **yes** | +| FAILED | `--report` rows → **674** | `--classify \| wc -l` → **674** | **yes** | +| SKIPPED | `--report` rows → **17** | `--report`'s *"17 skipped"* line | **yes** | +| `pagelint` debt | summary line → **524 blocks, 66 pages** | `grep -c 'USING DIRECTIVES'` over its output → **524** | **yes** | +| Rule 8's input | `attr_mismatch.py` → **1**, `PipelineValidation.md` block 7 | design E4: the same block, by `--list` | **yes** | +| Off-tranche FAILED / pages | design E1 → **630 / 64** | `probe/offtable.py` row count → **64**, `failed` column sum → **630** | **yes** | + +--- + +## 3. The phases + +**Why not Research → Core → Supporting → Polish.** That default would split each deliverable across +phases. The design has three deliverable shapes instead: a gate and the one page that satisfies it +(phase 1, `tools/README.md` rule 3); instruments that must exist before any table is drawn (phase 2, +AC8); and repairs grouped by `SUMMARY.md` section (phases 3–7, design § *Target And Phases*). +Acceptance is last. + +```text +Phase 1 rule 8 8 tasks one page (the marker) sign-off +Phase 2 instruments 15 tasks no page — +Phase 3 S1 Scheduler 7 tasks site sign-off +Phase 4 S2 Commands …; Get Started 6 tasks site sign-off +Phase 5 S3 Darker; Understanding 6 tasks site sign-off +Phase 6 S4 External Bus; Transports 6 tasks site sign-off +Phase 7 S5 the remaining five 7 tasks site sign-off +Phase 8 acceptance 6 tasks no page — + ── 61 +``` + +**Dependencies.** Phase 2 depends on phase 1 only through the merge order. Phases 3–7 each need phase +2's tables (task 2.14). Within phase 2, tasks 2.2, 2.5, 2.6 and 2.7 are independent of one another. +2.1 comes before 2.13, 2.2 before 2.3 and 2.4, and all of them come before 2.14. Within a repair +phase, N.1 comes first and the close comes last; the tasks between can run in any order. + +--- + +## Phase 1 — Rule 8, `ATTRIBUTE KIND` *(8 tasks, one PR, changes one page)* + +Design § *Rule 8*. **Predicted:** every gate unmoved, except a new `pagelint --plant` (exit 0). +`pagelint` reads **0 errors, 524 warnings, 162 pages** at both ends, because the marker is an HTML +comment and changes no block's text. + +- [ ] **Task 1.1:** Add `PAIRED` and the `ATTRIBUTE KIND` check to `tools/pagelint.py` + - Input: design § *Rule 8* (what it reads, what it reports, `PAIRED`, the message); + `spec/017-compile_repairs/probe/attr_mismatch.py` (`scan()`, moved unchanged); + `tools/pagelint.py` `:164` (`APPLIES_TO`), `:475` (`check_code_blocks`) + - Output: `PAIRED` beside `APPLIES_TO` with its derivation comment, and a check reporting + `ATTRIBUTE KIND` as an error repo-wide and under `--changed`. `python3 tools/pagelint.py` → + exactly **1** error, on `PipelineValidation.md` block 7, recorded here as the rule's first red + run + - Notes: `PAIRED` is re-derived at both refs as you write it (§ 1 obligation 1); the design's 13 + are the expected answer + +- [ ] **Task 1.2:** Add the `attr-mismatch-intended` opt-out and its three faults + - Input: design § *Rule 8*, *The opt-out*; `tools/blockcheck.py:280–300`, the binding it copies + - Output: a marker binding to the next C# block below it. A marker with no reason, a marker with no + C# block after it, and a second marker on one block are each an error. Honoured markers print + with their reasons and a count line, *"N block(s) marked attr-mismatch-intended"* + +- [ ] **Task 1.3:** Add `pagelint --plant` and record its red-proof + - Input: design § *Rule 8*, the five-plant table; 1.1 and 1.2 + - Output: `python3 tools/pagelint.py --plant; echo $?` → **0**, all five plants behaving. A + red-proof run, with one plant's expectation inverted in a scratch copy, → **1**, recorded here + with its output (AC1, AC3) + +- [ ] **Task 1.4:** Mark `PipelineValidation.md` block 7 + - Input: design § *Rule 8*, *The page* + - Output: the marker line above the fence of block 7. `python3 tools/pagelint.py` → **0 + errors**, *"1 block(s) marked attr-mismatch-intended"*. Recorded here: the run with the marker + removed reports `ATTRIBUTE KIND` on block 7 (AC2). `blockcheck --report` is unmoved, at 990 / 299 + - Notes: changes the published site. Its sign-off is asked for in 1.8's PR + +- [ ] **Task 1.5:** Run rule 8 in CI + - Input: `.github/workflows/docs.yml`, the `check` job + - Output: a bare `- run: python3 tools/pagelint.py --plant` step after the `pagelint` step, with + a comment citing design § *Rule 8* + +- [ ] **Task 1.6:** Write rule 8 into `CLAUDE.md` + - Input: design § *Rule 8*, *`CLAUDE.md`*; `CLAUDE.md` § *The ledger* and § *Complete code blocks* + - Output: one ledger row, and § *Handler attributes match their handler* after *Complete code + blocks*. Then `grep -rn 'rule 7\|seven rules\|rules 1' .claude/commands/` for any command quoting + a rule count this changes, each hit read and fixed (the *Writing Review* rule) + +- [ ] **Task 1.7:** Update `tools/README.md` for rule 8 + - Input: `tools/README.md` row 2, § *What each gate actually checks*, § *The other modes* + - Output: row 2 with the phase's ref (warnings unmoved); `pagelint`'s bullet naming rule 8; *The + other modes* listing `pagelint.py --plant`. AC14's corrected-form count → **1** for any new figure + +- [ ] **Task 1.8:** Close phase 1 + - Input: § 1 obligations 6, 7, 19; the phase's prediction + - Output: § *Phase 1 as executed*, with every gate's figure against the prediction, and the + reading criteria (AC9/AC10: **no block's text changed**, so none applies, stated). Then the PR, + asking for sign-off (one page changed) and for deletion of its head ref by name + +--- + +## Phase 2 — Instruments *(15 tasks, one PR, no page changes)* + +Design § *The Second Compile*, § *The Handler Wrapper*, § *The V4 Pin*. **Predicted:** `blockcheck` +BUILT rises by the blocks the handler wrapper, Shouldly and the `Program` fix make build, a set 2.1 +measures before any row; FAILED falls by the same; the corpus is unmoved, at 990. The main pin's +assemblies rise by Shouldly's, and there is a new scope line for the V4 pin. Every other gate is +unmoved. Tasks 2.8–2.11 are **P1**: one that does not fit is carried to the residual with its +reason, and does not hold the phase. + +- [ ] **Task 2.1:** Measure what phase 2's staging changes will build, before any row + - Input: design § *Gate Movement Predicted*, phase 2's BUILT row; E5's 32 blocks; E2's 6 Shouldly + blocks; `--list` for blocks naming `Program` + - Output: § *Phase 2 as executed*, *Predicted BUILT*: the block list each of 2.5, 2.7 and 2.8 is + expected to make BUILT, with how it was found. This is the set 2.13 reconciles against + +- [ ] **Task 2.2:** Give `--classify` its second compile, receiver-aware + - Input: design § *The Second Compile* (classes, five columns, resolution); `tools/blockcheck.py` + `:254`, `:1198–1204`; `spec/017-compile_repairs/probe/usings.py` (the `using` supply it + promotes) + - Output: `--classify` printing `page·ordinal·class·first·names` with the six classes, its + per-class counts on stderr, exit 0, and exit 2 on nothing to classify. A name is resolved only + when the page does not declare it and the staged base does not supply it + +- [ ] **Task 2.3:** Red-proof the second compile (AC6) + - Input: design § *The Second Compile*, *Red-proof* + - Output: § *Phase 2 as executed*, a five-row table (block, expected class, actual class) with + exactly one row per class named, plus the `Order` control. A second run from a scratch copy with + the page-declared check removed turns the control `built-by-using`, recorded. The scratch copy is + not committed + +- [ ] **Task 2.4:** Reconcile `--classify` with the 018 probe (AC7) + - Input: `probe/run.sh`'s `verdicts.tsv` at the same ref; 2.2's output + - Output: `spec/018-compile_residual/probe/reconcile.py` (obligation 18), and § *Phase 2 as + executed*, a disagreement table: every block where the two differ, each with its reason. E4's + 39 and 29 are expected, in the receiver-aware direction + +- [ ] **Task 2.5:** Add the handler wrapper (P0-7) + - Input: design § *The Handler Wrapper*; `tools/blockcheck.py:263`, `:355` + - Output: members blocks overriding the four methods are staged with their fully qualified + base. § *Phase 2 as executed* records how many of the 32 are wrapped, and names each one that is + not. Red-proof: four plants, one per base, each staging and building. Control: `--stage` output + byte-identical to `1b6f7f9` for every members block that is not a handler (`cmp`, silent) + +- [ ] **Task 2.6:** Add the V4 pin (P0-5) + - Input: design § *The V4 Pin*; `spec/018-compile_residual/probe/v4pin.sh`; + `tools/blockcheck/scaffold/pages.tsv` + - Output: `tools/blockcheck/refs-v4/refs-v4.csproj` with its comment; a `v4` cell on the four + pages; `--report` enforcing the pairing rule and printing the V4 assemblies line. Red-proof, + two-way: a V4 page unmarked, and a marked page with no V4 block, are each a finding (exit 1); + the corrected map → exit 0. AC11's `grep -c 'CS0234.*V4'` over `--explain` of the six → **0** + +- [ ] **Task 2.7:** Pin Shouldly (P0-8) + - Input: design § *Page Repair Rules*, the Shouldly row; `tools/blockcheck/refs/refs.csproj` + - Output: `Shouldly` `4.3.0` in `refs.csproj`, with a comment citing Brighter's + `Directory.Packages.props:150`. The restore is clean and the assembly count is recorded. No page + is touched; `TestDoubleOptions.md`'s repair is 7.4's + +- [ ] **Task 2.8:** *(P1)* Let a `statements` block name `Program` (friction #75) + - Input: friction #75 (`spec/017-compile_repairs/tasks.md` § *Friction ledger*); `WRAPPERS` + - Output: such a block staged as a top-level file. Red-proof: one block naming `Program` builds; + control: a `statements` block that does not name it stages exactly as before (`cmp`) + +- [ ] **Task 2.9:** *(P1)* Make a scaffold unit that does not compile a finding (friction #77) + - Input: friction #77; the unit rule in `--report` + - Output: a compile error in a scaffold unit printed as a finding, exit 1. Red-proof: a planted + broken unit → exit 1; the real units → **0** such findings at `1b6f7f9`, recorded + +- [ ] **Task 2.10:** *(P1)* Give `pagelint` a per-page warning count (friction #80) + - Input: friction #80; `tools/pagelint.py` `main` + - Output: a flag printing `pagewarnings`, sorted, whose column sums to the summary line's + **524**: two methods that must agree + +- [ ] **Task 2.11:** *(P1)* Measure the `net9.0` pin against `net10.0` (friction #76) + - Input: design D7; `refs.csproj`'s `TargetFramework` + - Output: § *Phase 2 as executed*, *net10.0*: a scratch `net10.0` build of the pin, the packages + that resolve to a different version, and `--report`'s verdicts under it against the `net9.0` + ones. If any verdict moves, it is put to the maintainer before 2.14 + +- [ ] **Task 2.12:** Update CI and `tools/README.md` for phase 2's instruments + - Input: `.github/workflows/docs.yml` `blocks` job; `tools/README.md` row 9, § *Reading a number*, + § *The other modes*, the `--classify` bullet + - Output: a `dotnet build tools/blockcheck/refs-v4/refs-v4.csproj -c Release` step before the + gate. Row 9 at the phase's ref. The fourth scope line explained. `--classify`'s description + rewritten for five columns and six classes. AC14's count → **1** for each new figure + +- [ ] **Task 2.13:** Baseline what phase 2 made build + - Input: 2.1's predicted set; `--report` after 2.5–2.8 + - Output: rows in `tools/blockcheck/baseline.tsv` for exactly the blocks that now build; + `--report` exit 0. § *Phase 2 as executed* reconciles them against 2.1, block by block. Each + new BUILT block is clean under rule 8 (`pagelint` 0 errors) + +- [ ] **Task 2.14:** Draw the S1–S5 page tables from `--classify` (AC8) + - Input: `--classify` at phase 2's head; design § *The section groups*; `SUMMARY.md` + - Output: § *The groups*, one table per group, every column derived from `--classify` by the + command written above it, and no probe column. The three tranche pages with V4 blocks are in S4 + and S5. The tables' FAILED column sums to `--report`'s FAILED on the 64 + 3 pages + +- [ ] **Task 2.15:** Close phase 2 + - Input: § 1 obligations 6, 19; the phase's prediction + - Output: § *Phase 2 as executed*, gates against the prediction; readings (no page touched, + stated); the PR, asking for deletion of its head ref by name + +--- + +## Phases 3–7 — the section groups + +**Each repair phase has the same spine.** N.1 predicts; N.2 and N.3 repair, under design § *Page +Repair Rules* and 017's rules (obligation 13); N.4 runs behaviour; N.5 is the second pass +(obligation 21); the last task closes. The page list is § *The groups*' table for that phase, drawn +at 2.14 and not before. **Every phase changes the site.** + +### Phase 3 — S1, Scheduler *(7 tasks, one PR)* + +- [ ] **Task 3.1:** Predict phase 3's gate movement + - Input: § *The groups*, S1; the recurrence greps for every DEFECT in S1's table (obligation 20) + - Output: § *Phase 3 as executed*, *Predicted*: BUILT, FAILED, corpus and `pagelint` debt as + bands, the off-group term listed by page and block, and `symbolcheck` **22 → 23 entries, 3 → 8 + silenced** (design E6) + +- [ ] **Task 3.2:** Repair S1's reachable blocks + - Input: S1's table, the `built-by-using`, `values` and `page-type` rows; 017 § *Scaffold Stub + Rules* + - Output: each block given its `using`s in the block, or a stub in a scaffold unit. Each leaves + BUILT, or listed in § *Blocks that stay FAILED* with its diagnostic + +- [ ] **Task 3.3:** Repair S1's hard blocks + - Input: S1's table, the `parse` and `defect` rows; design E2's buckets; 10.7.0 source for each + defect + - Output: each block in one of the four states. Literal `...` rewritten as `// ...`. The three + JSON fences (`AwsScheduler.md` #13, #14, `AzureScheduler.md` #19) retagged `json`. Splits in + § *Splits*, defects in § *Defect ledger* with their recurrence greps. `AwsScheduler.md` #2 and + #3, the V4 blocks, are judged against the V4 pin + +- [ ] **Task 3.4:** Repair `AddServiceActivator` and watch for it (P0-10) + - Input: design E6 and D5; `tools/symbolwatch.tsv`'s row format + - Output: `AwsScheduler.md` and `AzureScheduler.md` calling `AddConsumers`; one row in + `tools/symbolwatch.tsv`; `` beside the first site + on `FAQ.md` and `V10MigrationGuide.md`. `symbolcheck` → *"23 entries … 8 silenced"*, exit 0; + `symbolcheck --verify-list` → the new row DEAD at both refs. The red run (7 sites, exit 1) is + recorded, and `tools/README.md` row 8 updated + +- [ ] **Task 3.5:** Run S1's behavioural claims (AC10) + - Input: every block 3.2–3.4 changed that asserts behaviour + - Output: § *Phase 3 as executed*, a *Claim / Case / Control* table. Each row names its + `probe/s1//Program.cs` (obligation 18) + +- [ ] **Task 3.6:** Second pass for S1 (obligation 21) + - Input: the recurrences 3.1 listed; any ruling asked for in 3.2–3.5 + - Output: every recurrence repaired, or its ruling recorded; § *Defect ledger* rows' *After* + column filled from their greps, run as written + +- [ ] **Task 3.7:** Close phase 3 + - Input: § 1 obligations 6, 7, 19 + - Output: baseline rows for every new BUILT block; `--report` exit 0; `pagelint` 0 errors; the + phase's readings (AC9, AC10) over its diff; gates reconciled against 3.1; the PR, with sign-off + and head-ref deletion asked by name + +### Phase 4 — S2, Commands, Handlers and Pipelines; Get Started *(6 tasks, one PR)* + +- [ ] **Task 4.1:** Predict phase 4's gate movement + - Input: § *The groups*, S2; recurrence greps for S2's defects + - Output: § *Phase 4 as executed*, *Predicted*, as 3.1's +- [ ] **Task 4.2:** Repair S2's reachable blocks + - Input: S2's table, reachable rows + - Output: as 3.2's, for S2. `ShowMeTheCode.md` is a tutorial, so obligation 17 applies +- [ ] **Task 4.3:** Repair S2's hard blocks + - Input: S2's table, hard rows; 10.7.0 source + - Output: as 3.3's, for S2. Each handler block on an S2 page that 2.5 did not wrap or build, as + named in § *Phase 2 as executed*, is repaired or listed +- [ ] **Task 4.4:** Run S2's behavioural claims (AC10) + - Input: S2's changed blocks + - Output: a *Claim / Case / Control* table, each row naming `probe/s2//Program.cs` +- [ ] **Task 4.5:** Second pass for S2 + - Input: 4.1's recurrences; rulings asked + - Output: as 3.6's +- [ ] **Task 4.6:** Close phase 4 + - Input: § 1 obligations 6, 7, 19 + - Output: as 3.7's + +### Phase 5 — S3, Darker; Understanding Brighter *(6 tasks, one PR)* + +- [ ] **Task 5.1:** Predict phase 5's gate movement + - Input: § *The groups*, S3; recurrence greps for S3's defects + - Output: § *Phase 5 as executed*, *Predicted*, as 3.1's +- [ ] **Task 5.2:** Repair S3's reachable blocks + - Input: S3's table, reachable rows + - Output: as 3.2's, for S3 +- [ ] **Task 5.3:** Repair S3's hard blocks + - Input: S3's table, hard rows; Darker `4.1.1` and Brighter `10.7.0` source + - Output: as 3.3's, for S3. `QueryPipeline.md`'s 12 *parse* blocks are each made whole, skipped + with an accepted reason, or listed +- [ ] **Task 5.4:** Run S3's behavioural claims (AC10) + - Input: S3's changed blocks + - Output: a *Claim / Case / Control* table, each row naming `probe/s3//Program.cs` +- [ ] **Task 5.5:** Second pass for S3 + - Input: 5.1's recurrences; rulings asked + - Output: as 3.6's +- [ ] **Task 5.6:** Close phase 5 + - Input: § 1 obligations 6, 7, 19 + - Output: as 3.7's + +### Phase 6 — S4, Using an External Bus; Transports *(6 tasks, one PR)* + +- [ ] **Task 6.1:** Predict phase 6's gate movement + - Input: § *The groups*, S4; recurrence greps for S4's defects + - Output: § *Phase 6 as executed*, *Predicted*, as 3.1's +- [ ] **Task 6.2:** Repair S4's reachable blocks + - Input: S4's table, reachable rows; the V4 pin for `S3LuggageStore.md` #1 + - Output: as 3.2's, for S4. `S3LuggageStore.md` #1 is BUILT or listed with a diagnostic that is + not `CS0234 … V4` +- [ ] **Task 6.3:** Repair S4's hard blocks + - Input: S4's table, hard rows; 10.7.0 source + - Output: as 3.3's, for S4. `KafkaConfiguration.md`'s 14 *parse* blocks are each made whole, + skipped with an accepted reason, or listed +- [ ] **Task 6.4:** Run S4's behavioural claims (AC10) + - Input: S4's changed blocks + - Output: a *Claim / Case / Control* table, each row naming `probe/s4//Program.cs` +- [ ] **Task 6.5:** Second pass for S4 + - Input: 6.1's recurrences; rulings asked + - Output: as 3.6's +- [ ] **Task 6.6:** Close phase 6 + - Input: § 1 obligations 6, 7, 19 + - Output: as 3.7's + +### Phase 7 — S5, the remaining five sections *(7 tasks, one PR)* + +- [ ] **Task 7.1:** Predict phase 7's gate movement + - Input: § *The groups*, S5; recurrence greps for S5's defects + - Output: § *Phase 7 as executed*, *Predicted*, as 3.1's +- [ ] **Task 7.2:** Repair S5's reachable blocks + - Input: S5's table, reachable rows; the V4 pin for `DistributedLock.md` #2 and + `DynamoDbDistributedLock.md` #1, #2 + - Output: as 3.2's, for S5. The three V4 blocks are BUILT or listed with a diagnostic that is not + `CS0234 … V4` (AC11, with 3.3 and 6.2) +- [ ] **Task 7.3:** Repair S5's hard blocks + - Input: S5's table, hard rows; 10.7.0 source + - Output: as 3.3's, for S5 +- [ ] **Task 7.4:** Repair `TestDoubleOptions.md`'s Shouldly blocks (P0-8) + - Input: design § *Page Repair Rules*, the Shouldly row; 2.7's pin + - Output: `using Shouldly;` in each of the six blocks, and one sentence naming the package. + `grep -c Shouldly contents/TestDoubleOptions.md` → **≥ 7**, from **0**; the six BUILT or listed +- [ ] **Task 7.5:** Run S5's behavioural claims (AC10) + - Input: S5's changed blocks + - Output: a *Claim / Case / Control* table, each row naming `probe/s5//Program.cs` +- [ ] **Task 7.6:** Second pass for S5 + - Input: 7.1's recurrences; rulings asked + - Output: as 3.6's +- [ ] **Task 7.7:** Close phase 7 + - Input: § 1 obligations 6, 7, 19 + - Output: as 3.7's + +--- + +## Phase 8 — Acceptance *(6 tasks, one PR, no page touched)* + +- [ ] **Task 8.1:** Walk the criteria that have no instrument first: AC4, AC8, AC9, AC10, and the + reading halves of AC5 and AC13 + - Input: `requirements.md` § *Acceptance criteria*, word for word; every phase's readings + (obligation 19) + - Output: § *Phase 8 as executed*, one row per criterion: who reads it, what they read, the + walker's finding, the verdict + +- [ ] **Task 8.2:** Walk the instrumented criteria: AC1–AC3, AC5–AC7, AC11–AC14 + - Input: each criterion's command, run at the phase's head + - Output: one row per criterion: the command, its output, the verdict. AC12's BUILT is against + design's **≥ 560**, and the nothing-BUILT figure against **≤ 14** + +- [ ] **Task 8.3:** Check backwards: what changed that should not have + - Input: `git diff --name-only 3a79b20 -- contents/` + - Output: every changed page in the 64 + 3, or justified by a defect-ledger row (a recurrence) or + by P0-10 (`FAQ.md`, `V10MigrationGuide.md`). Any other page is listed and explained + +- [ ] **Task 8.4:** Complete the defect ledger + - Input: § *Defect ledger* + - Output: every row with its recurrence grep, *Before*, *After* **0**, and *Found by*: the + instrument (`--classify`, rule 8, `symbolcheck`), a run, or re-derivation + +- [ ] **Task 8.5:** Write the friction ledger + - Input: every phase's *as executed* section + - Output: § *Friction ledger*, numbered from **81**, each entry with the repair it proposes for + the next spec + +- [ ] **Task 8.6:** Close 018 + - Input: § *Phase 8 as executed*; `--report` and `--classify` at the head + - Output: § *What 018 shipped*, ending in **one sentence naming the residual**, with its figure + re-derived by `--classify`; the README checklist ticked; `PROMPT.md` updated; the PR, asking for + the `.accepted` file and for deletion of its head ref by name + +--- + +## The groups + +*Drawn at task 2.14 from `--classify`, and not before (AC8). Design § *The section groups* holds +the working figures they replace.* + +## Blocks that stay FAILED + +*One row per FAILED block on a reached page: page, block, diagnostic, why it stays, phase (AC13).* + +| Page | # | Diagnostic | Why it stays | Phase | +|---|---:|---|---|---:| + +## Splits + +*One row per split fence: page, old ordinal, new ordinals (017 obligation 16).* + +| Page | Old # | New # | Phase | +|---|---:|---|---:| + +## Defect ledger + +*Each row written only after its grep has run as written (obligation 22). These three were found at +design, and their greps were run there.* + +| # | Defect | Page(s) | Recurrence grep | Before | After | Found by | Phase | +|---:|---|---|---|---:|---:|---|---:| +| 1 | `AddServiceActivator` called as current code; dead at `10.7.0` and `master` (doc comments only) | `AwsScheduler.md`, `AzureScheduler.md` | `grep -rn 'Services.AddServiceActivator(' contents/` | **2** | | re-derivation (design E2); the watchlist missed it | 3 | +| 2 | Shouldly assertions with the package never named and no `using` | `TestDoubleOptions.md` | `grep -c Shouldly contents/TestDoubleOptions.md` | **0** | | the second compile (design E2, *pin-gap*) | 7 | +| 3 | Package spelt `Paramore.Brighter.Inbox.DynamoDb.V4`; the project is `…DynamoDB.V4` | `DynamoInbox.md` (a 017 tranche page; repaired under obligation 21) | `grep -rn 'Inbox.DynamoDb.V4' contents/` | **1** | | re-derivation (requirements, the V4 pin) | 7 | + +## Friction ledger + +*Numbered from 81, continuing 017's 67–80. Written at task 8.5.* + +## What the tasks review found + +*Written at `/spec:review`.* From 6d99310e3f9221bf9ad2bf59ad0dfcb720bd50a3 Mon Sep 17 00:00:00 2001 From: iancooper Date: Mon, 5 Oct 2026 11:28:23 +0100 Subject: [PATCH 4/4] =?UTF-8?q?spec:=20018=20tasks=20approved=20=E2=80=94?= =?UTF-8?q?=20three=20review=20findings=20repaired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 1.6's command-file grep matched nothing; it now reads every `pagelint` hit in .claude/commands/. AC13's join gets a home in 8.2, and 4.3 drops a figure nothing produced. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LjnH2pJ98aMabu1MyTpRxy --- spec/018-compile_residual/.tasks-approved | 0 spec/018-compile_residual/README.md | 2 +- spec/018-compile_residual/tasks.md | 23 +++++++++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 spec/018-compile_residual/.tasks-approved diff --git a/spec/018-compile_residual/.tasks-approved b/spec/018-compile_residual/.tasks-approved new file mode 100644 index 0000000..e69de29 diff --git a/spec/018-compile_residual/README.md b/spec/018-compile_residual/README.md index 91e9283..fb050a9 100644 --- a/spec/018-compile_residual/README.md +++ b/spec/018-compile_residual/README.md @@ -1,7 +1,7 @@ # Spec 018: Compile Residual **Created:** 2026-10-04 -**Status:** Tasks Phase — `tasks.md` drafted 2026-10-05, awaiting `/spec:review` +**Status:** Writing Phase — tasks approved 2026-10-05; phase 1 next > **Re-derive this README before executing it.** It was written before anyone looked — check every > count and every named gap against the tree, with the command beside the figure. diff --git a/spec/018-compile_residual/tasks.md b/spec/018-compile_residual/tasks.md index 782b2b6..9ab9d61 100644 --- a/spec/018-compile_residual/tasks.md +++ b/spec/018-compile_residual/tasks.md @@ -1,6 +1,6 @@ # Spec 018: Compile Residual — Tasks -**Status:** Draft, for `/spec:review` +**Status:** **APPROVED 2026-10-05** — `.tasks-approved`. Three review findings repaired (§ *What the tasks review found*). **Requirements:** approved 2026-10-04 · **Design:** approved 2026-10-05 **Eight phases, 61 tasks, one pull request per phase.** Phases merge under `tools/README.md` @@ -148,8 +148,9 @@ comment and changes no block's text. - [ ] **Task 1.6:** Write rule 8 into `CLAUDE.md` - Input: design § *Rule 8*, *`CLAUDE.md`*; `CLAUDE.md` § *The ledger* and § *Complete code blocks* - Output: one ledger row, and § *Handler attributes match their handler* after *Complete code - blocks*. Then `grep -rn 'rule 7\|seven rules\|rules 1' .claude/commands/` for any command quoting - a rule count this changes, each hit read and fixed (the *Writing Review* rule) + blocks*. Then `grep -rn 'pagelint' .claude/commands/` (**2** files at `77b7113`: `implement.md`, + `review.md`), each hit read against the new rule and fixed where it quotes a rule set or count + (the *Writing Review* rule) - [ ] **Task 1.7:** Update `tools/README.md` for rule 8 - Input: `tools/README.md` row 2, § *What each gate actually checks*, § *The other modes* @@ -429,7 +430,8 @@ at 2.14 and not before. **Every phase changes the site.** - [ ] **Task 8.2:** Walk the instrumented criteria: AC1–AC3, AC5–AC7, AC11–AC14 - Input: each criterion's command, run at the phase's head - Output: one row per criterion: the command, its output, the verdict. AC12's BUILT is against - design's **≥ 560**, and the nothing-BUILT figure against **≤ 14** + design's **≥ 560**, and the nothing-BUILT figure against **≤ 14**. AC13's join command is written + above § *Blocks that stay FAILED* and run there, zero rows either side - [ ] **Task 8.3:** Check backwards: what changed that should not have - Input: `git diff --name-only 3a79b20 -- contents/` @@ -488,6 +490,15 @@ design, and their greps were run there.* *Numbered from 81, continuing 017's 67–80. Written at task 8.5.* -## What the tasks review found +## What the tasks review found — 2026-10-05 -*Written at `/spec:review`.* +| # | Found | Now | +|---:|---|---| +| 1 | Task 1.6's grep for stale rule claims, `'rule 7\|seven rules\|rules 1'`, matches **0** lines in `.claude/commands/`, so it could not find what it was for | `grep -rn 'pagelint' .claude/commands/` → 2 files, each hit read | +| 2 | AC13's join had no home: no task wrote the command | 8.2 writes it above § *Blocks that stay FAILED* and runs it | +| 3 | Task 4.3 named *"15 handler blocks"*, a figure nothing produced | it names the blocks § *Phase 2 as executed* lists | + +Checked and holding: 61 tasks by `grep -c` (8 / 15 / 7 / 6 / 6 / 6 / 7 / 6); every new check has a +red-proof task with a two-way control; the expected outputs that can be read today hold (rule 8's one +hit is `PipelineValidation.md` block 7; `grep -c Shouldly contents/TestDoubleOptions.md` → 0; +E6's 23 / 8; `pagelint` 524).