diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c0db8a2..ebe2b4a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,6 +34,11 @@ jobs: # blocking unrelated work. - run: python3 tools/pagelint.py + # Spec 018 phase 1, design § Rule 8. Rule 8 (ATTRIBUTE KIND) has one hit in + # the corpus and it is marked, so the step above cannot show the rule still + # fires. These five in-memory plants can: exit 0 only if every one behaves. + - run: python3 tools/pagelint.py --plant + # On a pull request the code rules become errors, but only for blocks that # overlap the diff. Fixing a typo on a 700-line page therefore obliges # nothing beyond the typo. diff --git a/CLAUDE.md b/CLAUDE.md index 432fd00..f3fe689 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -373,6 +373,32 @@ never silenced. It still counts towards the debt and still says so in its own wo Without that, relocating a block verbatim is indistinguishable from writing a new one, and a page split cannot honour "move the text, do not improve it". +### Handler attributes match their handler + +A sync handler attribute decorates `Handle`; its `…Async` twin decorates `HandleAsync`. +`[UsePolicy]` on `HandleAsync` **compiles**, so the compiler is no help: Brighter throws +`ConfigurationException` when it builds the pipeline, and `ValidatePipelines()` reports it +at startup. A block can therefore build, enter `blockcheck`'s baseline, and still teach +the reader a pipeline that fails. Rule 8 (`ATTRIBUTE KIND`) reads every C# block for it. + +The attributes it knows are the ones Brighter ships in both forms, held as `PAIRED` +beside `APPLIES_TO` in `tools/pagelint.py`, with the command that re-derives them at a +version bump. Edit them there and nowhere else. + +A block that shows the mismatch on purpose carries a marker on the line above its fence: + +```markdown + +``` + +That is `PipelineValidation.md`'s, the one block in the corpus that needs it. The marker +silences rule 8 for **that block only**, and every honoured marker prints with its +reason. It is per block rather than per page because that page has correct examples +beside the deliberate one, and a page-wide marker would have hidden a real mismatch +among them. A marker with no reason, with no C# block after it, or that is a block's +second, is itself an error. `python3 tools/pagelint.py --plant` proves the rule still +fires, since the corpus's one hit is marked. + ### The opening sentence Every page's first sentence after the banner has to survive being read on its own. @@ -543,6 +569,7 @@ failure the claim in this paragraph is meant to prevent: | Language tag on every fence | 4 | error | error | | "Dispatcher", not "ServiceActivator" or "Service Activator", in prose | 5 | error | error | | `using` directives in C# blocks | 6 | warning, counted | error, unless the block marks its omission `// ...` | +| 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 | | An opening sentence exists | 7 (`SUMMARY MISSING`) | error | error | | It is ≤ 200 characters **rendered** | 7 (`SUMMARY TOO LONG`) | error | error | | It does not end in a colon | 7 (`SUMMARY ENDS IN COLON`) | error | error | diff --git a/contents/PipelineValidation.md b/contents/PipelineValidation.md index 86c6a22..1e8de34 100644 --- a/contents/PipelineValidation.md +++ b/contents/PipelineValidation.md @@ -244,6 +244,7 @@ An async handler must use async versions of pipeline attributes. The example bel **Before** (error): + ```csharp public class OrderHandler : RequestHandlerAsync { diff --git a/spec/018-compile_residual/tasks.md b/spec/018-compile_residual/tasks.md index 9ab9d61..bd40de9 100644 --- a/spec/018-compile_residual/tasks.md +++ b/spec/018-compile_residual/tasks.md @@ -110,7 +110,7 @@ Design § *Rule 8*. **Predicted:** every gate unmoved, except a new `pagelint -- `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` +- [x] **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`) @@ -121,48 +121,114 @@ comment and changes no block's text. - 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 +- [x] **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 +- [x] **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 +- [x] **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 +- [x] **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` +- [x] **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 '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 +- [x] **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 +- [x] **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 1 as executed — 2026-10-05 + +**Measured at phase start, `94ad0ed`.** `master` moved after tasks were approved: #195, a bugfix +outside the programme, changed six pages and added `bugfixes/0001-unmapped-type-guidance/`. Two +figures moved with it. The prediction above was made at `1b6f7f9` and stands as written: + +| Figure | Said (`1b6f7f9`) | Measured (`94ad0ed`), method 1 | Method 2 | Cause | +|---|---:|---|---|---| +| `pagelint` warnings | 524 | summary line → **512 / 66 pages** | `grep -c 'USING DIRECTIVES'` → **512** | #195 repaired 12 debt blocks on its six pages | +| `linkcheck` files | 165 | **166** | a worktree at `019ef8e` → **165** | #195's `bugfix.md` is inside the walk | +| `blockcheck` | 990 / 299 / 674 / 17 | `--report` rows → **990 / 299 / 674 / 17** | `grep -vc '^#' baseline.tsv` → **299** | unmoved | +| `PAIRED` | 13 | the derivation at Brighter `10.7.0` → **13** | at Brighter `origin/master` → the same 13 | — | + +`tools/README.md` rows 1 and 2 now carry `94ad0ed`'s figures with their refs. + +**Rule 8's first red run** (1.1), the whole repo before the marker, exit 1: + +```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 + +1 errors, 512 warnings (using-directive debt: 512 blocks across 66 pages) across 162 pages. +``` + +`--changed origin/master` reports the same error, exit 1, so the rule is an error at both levels. + +**The marker's three faults** (1.2), each planted on `PipelineValidation.md` and reverted. Each is +an error, and the first and third leave the block checked: + +```text +contents/PipelineValidation.md:248: ATTRIBUTE KIND MARKER: the block at line 249 already has a marker, at line 247. The block below is still checked +contents/PipelineValidation.md:420: ATTRIBUTE KIND MARKER: no C# block follows it, so it marks nothing +contents/PipelineValidation.md:247: ATTRIBUTE KIND MARKER: no reason given; write . The block below is still checked +``` + +**`--plant`'s red-proof** (1.3, AC1, AC3): a scratch copy with the matched-pair plant expecting 1 +hit rather than 0, exit **1**. The real one exits **0**, 5 of 5. `--plant x` exits 2. + +```text +FAILED matched pair, sync on Handle: ATTRIBUTE KIND x0, expected x1 +4 of 5 plants behave. +``` + +**The page** (1.4, AC2): with the marker, `pagelint` reports **0 errors** and *"1 block(s) marked +attr-mismatch-intended"*. Without it, the red run above. `blockcheck --report` gives +`PipelineValidation.md`'s 16 verdicts identically with and without the marker, and the corpus stays +at 990 / 299. + +**Gates against the prediction** (*every gate unmoved; `pagelint` 0 errors at both ends*): + +| Gate | Predicted | Measured | Why any difference | +|---|---|---|---| +| `linkcheck` | unmoved | 166, 0 broken | 165 → 166 is #195, before the phase | +| `pagelint` | 0 errors, 524 warnings | **0 errors, 512 warnings, 162 pages**; 1 marked | 524 → 512 is #195; the phase moved none | +| `pagelint --plant` | exit 0 | exit **0**, 5 of 5 | — | +| `symbolcheck` | unmoved | 22 entries, 161 pages, 3 silenced, 0 found | — | +| `blockcheck` | 990 / 299 | **990 / 299 / 674 / 17** | — | + +**Reading criteria** (obligation 19): the marker is an HTML comment above a fence, so **no block's +text changed**. Neither AC9 nor AC10 applies. + +**Commands that cite what changed** (1.6): `grep -rn 'pagelint' .claude/commands/` → `implement.md` +(`:69` rule 6, `:80` the gate commands) and `review.md` (`:2`, `:12`, `:25`, `:81`). None quotes a +rule count that rule 8 moves. The ledger's three review-only conventions are still three, and +`review.md`'s error filter keeps the new marker lines. + --- ## Phase 2 — Instruments *(15 tasks, one PR, no page changes)* diff --git a/tools/README.md b/tools/README.md index 9744e46..e33a7b4 100644 --- a/tools/README.md +++ b/tools/README.md @@ -23,9 +23,10 @@ not be in a command. `412fd34` is `master` as of 2026-09-13 — spec 014 phase 4, PR #161. Re-derive these before quoting them; the command is in the row so that you can. -**`linkcheck` reads 165 here and read 164 at `fc77c42`, one merge earlier.** The file you are -reading is the +1: `tools/` is inside `linkcheck`'s walk, so this file entered its corpus the day -it was written. Both numbers are true at their refs, which is what the refs are for. +**`linkcheck`'s file count moves whenever a file lands anywhere it walks, not only under +`contents/`.** It read 164 at `fc77c42`; this file made it 165, because `tools/` is inside the +walk; and `bugfixes/0001-unmapped-type-guidance/bugfix.md` made it 166 at `94ad0ed`. Each number is +true at its ref, which is what the refs are for. **Two rows carry a second ref, `3be2a78` — spec 015 phase 4, the triage repairs.** `symbolcheck` went from 5 watchlist entries to **22** when the triage's seventeen confirmed-dead names were @@ -80,8 +81,8 @@ package added. `pagelint` fell 537 → **524**, on blocks the repairs gave their | # | Gate | Command | Expected at `412fd34` | |---:|---|---|---| -| 1 | `linkcheck` | `python3 tools/linkcheck.py` | **165 files, 0 broken** | -| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 524 warnings, 162 pages** — at `bd95ee0`; it read **537** at `5981b12`, **616** at `ca6a0b2`, **658** at `2223fcb`, **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` | +| 1 | `linkcheck` | `python3 tools/linkcheck.py` | **166 files, 0 broken** — at `94ad0ed`; it read **165** at `bd95ee0` | +| 2 | `pagelint` | `python3 tools/pagelint.py` | **0 errors, 512 warnings, 162 pages**, with **1 block marked `attr-mismatch-intended`** — at `94ad0ed` plus spec 018 phase 1, which moved no warning; it read **524** at `bd95ee0`, **537** at `5981b12`, **616** at `ca6a0b2`, **658** at `2223fcb`, **706** at `5407298`, **743** at `b941837`, **744** at `3be2a78` and **757** at `412fd34` | | 3 | shape | `python3 tools/urlmap.py --check-shape` | **161 pages, 12 sections, widest 12 of 20, deepest 4 of 4** | | 4 | redirects | `python3 tools/urlmap.py --check-redirects` | **77 entries, 7858 bytes** | | 5 | `versioncheck` | `python3 tools/versioncheck.py` | **0 stale pins of 18, across 5 pages** | @@ -169,6 +170,7 @@ python3 tools/linkcheck.py contents/Glossary.md # specific files; skips the python3 tools/pagelint.py contents/Glossary.md # specific pages python3 tools/pagelint.py --changed origin/master # strict on code blocks overlapping the diff python3 tools/pagelint.py --fix # repair banners, fences, descriptions +python3 tools/pagelint.py --plant # rule 8's five in-memory plants; exit 0 only if all behave python3 tools/symbolcheck.py contents/Glossary.md # specific pages python3 tools/symbolcheck.py --census # open-world report, never a gate, always exit 0 python3 tools/symbolcheck.py --verify-list # is every watchlist row still dead? @@ -232,14 +234,18 @@ that file is a diff someone reads, not a command someone reruns until the build `EMPTY TARGET`, and `ORPHAN` — a page under `contents/` that `SUMMARY.md` never links to. Orphans are reported only on a whole-repo run. **Its corpus is the repository, not the published tree**: it walks everything except `.git`, - `.github`, `.claude`, `.repomix`, `spec/` and `node_modules`, which is why its file count (165) + `.github`, `.claude`, `.repomix`, `spec/` and `node_modules`, which is why its file count (166) is higher than `pagelint`'s page count (162) and why *this file* is in it. It resolves a link to `CLAUDE.md` but has no index of its headings, so **an anchored link into `CLAUDE.md` reports `MISSING ANCHOR` even when the heading exists.** Cite `CLAUDE.md` sections as prose here, not as anchored links. - **`pagelint`** — the authoring conventions, rule by rule, over `contents/` plus the root `README.md`. `CLAUDE.md` § *Enforcement* carries the ledger that maps every convention to its - rule and back; do not restate it here. + rule and back; do not restate it here. **Rule 8, `ATTRIBUTE KIND`**, is the one rule that reads + what a block *means* rather than how it is formed: a handler attribute of the wrong kind for its + method compiles and fails at pipeline build. Its one corpus hit is marked, so CI runs `--plant` + beside the whole-repo step to show the rule still fires — `CLAUDE.md` § *Handler attributes + match their handler*. - **`urlmap --check-shape`** — the navigation's shape: at least 2 pages per section, at most 20 top-level entries, at most 4 URL segments, and no `SUMMARY.md` heading with leading whitespace. - **`urlmap --check-redirects`** — every redirect in `.gitbook.yaml` resolves to a file that diff --git a/tools/pagelint.py b/tools/pagelint.py index 8b16077..d54c13c 100644 --- a/tools/pagelint.py +++ b/tools/pagelint.py @@ -17,6 +17,10 @@ SERVICEACTIVATOR "ServiceActivator" in prose where "Dispatcher" is meant USING DIRECTIVES a C# block with no `using` lines (warning, counted; stays a warning under --changed if marked `// ...`) + ATTRIBUTE KIND a sync handler attribute on HandleAsync, or an `...Async` + one on Handle: it compiles, and Brighter rejects it when + it builds the pipeline + ATTRIBUTE KIND MARKER an attr-mismatch-intended marker that binds nothing SUMMARY MISSING no prose after the banner to summarise the page with SUMMARY TOO LONG an opening sentence over 200 characters, rendered SUMMARY ENDS IN COLON an opening sentence promising a list an index drops @@ -106,6 +110,7 @@ python3 tools/pagelint.py contents/Glossary.md # specific pages python3 tools/pagelint.py --changed origin/master # strict on changed blocks python3 tools/pagelint.py --fix # repair, then report what is left + python3 tools/pagelint.py --plant # rule 8's planted cases; exit 0 if all behave Cross-page uniqueness is a property of the corpus, so when given explicit paths the tool still loads every page for context and only reports on the ones asked @@ -163,6 +168,15 @@ # apply_banners.py imports it. APPLIES_TO = ('Brighter V10 and Darker V4', 'Brighter V10', 'Darker V4') +# Rule 8. 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') + BANNER_RE = re.compile( r'^> \*\*(Tutorial|How-to|Reference|Explanation)\*\*' # page type r' · Applies to \*\*(' + '|'.join(APPLIES_TO) + r')\*\*' # longest first @@ -506,6 +520,129 @@ def check_code_blocks(page, strict_ranges): return findings +# -------------------------------------------------------------------------- +# Rule 8 — a handler attribute's kind matches its handler's +# -------------------------------------------------------------------------- +# +# `[UsePolicy]` on `HandleAsync` compiles. Brighter throws ConfigurationException +# when it builds the pipeline, and ValidatePipelines() reports it at startup, so +# a block can enter blockcheck's baseline and still be wrong. The compiler cannot +# see it; this can. +# +# scan() is spec 017's probe/attr_mismatch.py, moved unchanged, so its 25 runs +# there stand as this rule's history. It reads every C# block, BUILT or not, and +# does not consult the banner: PAIRED names Brighter attributes only, and Darker +# 4.1.1 has no async twins, so a Darker page cannot hit. +# +# The opt-out is per block, not per page, because the one page that shows the +# mistake on purpose (PipelineValidation.md) has correct blocks beside it, and a +# page-wide marker would have hidden a real mismatch among them. It binds the way +# blockcheck's skip does -- see blockcheck.scan_skips() -- and fails the same +# three ways, each reported rather than ignored. + +ATTR_KIND_RE = re.compile(r'^\s*\[\s*(' + '|'.join(PAIRED) + r')(Async)?\s*\(') +ATTR_OPT_OUT_RE = re.compile(r'^$') +ATTR_OPT_OUT_ANY_RE = re.compile(r'^' + + +def scan(lines): + """lines: [(lineno, text)]. Yields (lineno, attribute, method-kind).""" + for i, (n, text) in enumerate(lines): + m = ATTR_KIND_RE.search(text) + if not m: continue + j = i + 1 + while j < len(lines) and (lines[j][1].strip().startswith('[') or not lines[j][1].strip()): j += 1 + if j == len(lines): continue + target = lines[j][1] + is_async = bool(re.search(r'\bHandleAsync\b', target)) + is_sync = bool(re.search(r'\bHandle\s*\(', target)) and not is_async + if (m.group(2) is None and is_async) or (m.group(2) and is_sync): + yield n, m.group(1) + (m.group(2) or ''), 'async' if is_async else 'sync' + + +def check_attribute_kind(page): + """Rule 8. Returns (findings, honoured) -- honoured is [(lineno, reason)]. + + A marker binds the next C# fence that opens after it. It binds nothing, and + is reported, when it has no reason, when no C# block follows it, or when the + block already has one. + """ + findings, honoured, bound = [], [], {} + starts = sorted(b['start'] for b in page.blocks + if b['info'].split(',')[0].lower() in CSHARP_TAGS) + for lineno, line in page.prose: + stripped = line.strip() + if not ATTR_OPT_OUT_ANY_RE.match(stripped): + continue + match = ATTR_OPT_OUT_RE.match(stripped) + target = next((s for s in starts if s > lineno), None) + if not match: + problem = f'no reason given; write {ATTR_OPT_OUT_EXAMPLE}' + elif target is None: + problem = 'no C# block follows it, so it marks nothing' + elif target in bound: + problem = (f'the block at line {target} already has a marker, at line ' + f'{bound[target]}') + else: + bound[target] = lineno + honoured.append((lineno, match.group(1))) + continue + if target is not None: + problem += '. The block below is still checked' + findings.append(error(page.rel, lineno, 'ATTRIBUTE KIND MARKER', problem)) + + for block in page.blocks: + if block['start'] not in starts or block['start'] in bound: + continue + for lineno, attr, kind in scan(block['body']): + if kind == 'async': + why = (f'[{attr}] is the sync attribute, on HandleAsync. ' + f'Use [{attr}Async]') + else: + why = (f'[{attr}] is the async attribute, on Handle. ' + f'Use [{attr[:-len("Async")]}]') + findings.append(error( + page.rel, lineno, 'ATTRIBUTE KIND', + f'{why}, or mark the block {ATTR_OPT_OUT_EXAMPLE} if the ' + 'mismatch is the point')) + return findings, honoured + + +def plant(): + """Rule 8's red-proof, in memory: exit 0 only if every plant behaves. + + A mode rather than a file under contents/, because pagelint refuses paths + outside it and a plant page would publish. The first three are the probe's; + the last two exist because the opt-out does. + """ + marked = '' + sync_on_async = ['[UsePolicy("retry", step: 1)]', + 'public override async Task HandleAsync(X c, CancellationToken t)'] + async_on_sync = ['[RequestLoggingAsync(0, HandlerTiming.Before)]', + 'public override X Handle(X c)'] + matched = ['[UsePolicy("retry", step: 1)]', '[RequestLogging(0, HandlerTiming.Before)]', + 'public override X Handle(X c)'] + plants = ( # (name, lines above the fence, block body, rule expected, hits) + ('sync attribute on HandleAsync', [], sync_on_async, 'ATTRIBUTE KIND', 1), + ('...Async attribute on Handle', [], async_on_sync, 'ATTRIBUTE KIND', 1), + ('matched pair, sync on Handle', [], matched, 'ATTRIBUTE KIND', 0), + ('mismatch under a marker with a reason', [marked], sync_on_async, 'ATTRIBUTE KIND', 0), + ('a marker with no reason', [''], + sync_on_async, 'ATTRIBUTE KIND MARKER', 1), + ) + failed = 0 + for name, above, body, rule, want in plants: + lines = ['# Plant', ''] + above + ['```csharp'] + body + ['```'] + findings, _ = check_attribute_kind(Page('', '', lines)) + got = sum(1 for f in findings if f.rule == rule) + ok = got == want + failed += not ok + print(f'{"OK " if ok else "FAILED"} {name}: {rule} x{got}, expected x{want}') + print(f'\n{len(plants) - failed} of {len(plants)} plants behave.') + return 1 if failed else 0 + + # -------------------------------------------------------------------------- # --fix — the two rules with exactly one correct answer # -------------------------------------------------------------------------- @@ -1200,6 +1337,11 @@ def main(argv): merge_base = args.pop(0) elif arg == '--fix': fix = True + elif arg == '--plant': + if len(argv) != 1: + print('--plant takes no other arguments', file=sys.stderr) + return 2 + return plant() elif arg.startswith('-'): print(f'unknown option: {arg}', file=sys.stderr) return 2 @@ -1270,7 +1412,7 @@ def main(argv): pages = load_pages() print() - findings = [] + findings, honoured = [], [] for rel in reported: page = pages[rel] # A root page takes rule 7 only -- see ROOT_PAGES. @@ -1279,6 +1421,9 @@ def main(argv): findings += check_banner(page) findings += check_code_blocks(page, strict.get(rel, [])) findings += check_terminology(page) + kind, marks = check_attribute_kind(page) + findings += kind + honoured += [(rel, lineno, reason) for lineno, reason in marks] findings += check_headings(pages, [r for r in reported if not pages[r].is_root]) findings += check_summaries(pages, reported) @@ -1290,6 +1435,13 @@ def main(argv): label += ' (warning)' print(f'{finding.path}:{finding.line}: {label}: {finding.message}') + # Every honoured marker is printed with its reason, symbolcheck's + # convention: a silenced check should be as visible as a failing one. + for rel, lineno, reason in honoured: + print(f'{rel}:{lineno}: attr-mismatch-intended: {reason}') + if honoured: + print(f'{len(honoured)} block(s) marked attr-mismatch-intended') + errors = sum(1 for f in findings if f.severity == 'error') warnings = len(findings) - errors