@erclx/canon 4.82.0 → 4.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/docs-fold/REQUIREMENT.md +13 -0
- package/claude/skills/docs-fold/SKILL.md +20 -0
- package/claude/skills/docs-fold/references/classify.md +78 -0
- package/claude/skills/draft-slides/SKILL.md +1 -1
- package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
- package/claude/skills/draft-wireframes/SKILL.md +7 -4
- package/docs/agents/commands.md +1 -1
- package/docs/agents/context-audit-checks.md +22 -2
- package/docs/agents/context-audit.md +12 -2
- package/docs/agents/context-classify.md +1 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/visual-design-workflow.md +1 -1
- package/governance/rules/claude/520-wireframes.md +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +71 -2
- package/src/context/architecture.ts +73 -0
- package/src/context/audit.ts +18 -2
- package/src/context/gate.ts +44 -6
- package/src/context/wireframe-states.ts +238 -0
- package/standards/architecture.md +11 -1
- package/standards/context.md +4 -1
- package/standards/design.md +6 -0
- package/standards/requirements.md +2 -1
- package/standards/wireframes.md +45 -29
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/CLAUDE.md +1 -1
- package/tooling/claude/seeds/canon/wireframes/index.md +2 -2
|
@@ -17,6 +17,8 @@ The receipt half of that sweep was missing entirely. A review receipt was delete
|
|
|
17
17
|
|
|
18
18
|
The trigger side carries a gap of its own. "Sync the docs" names either corpus to the person saying it, so a description leaving its corpus to the opening clause alone competes with its public-facing sibling on nothing the routing field states, and the planning surface the request was about goes untouched.
|
|
19
19
|
|
|
20
|
+
This skill writes canonical docs at the end of a long build and never reviews what it wrote. An appended figure or a branch-narrated re-measurement lands unchecked the same way a session's own edits do, so what the fold produces carries the exact defect the standards it cites already ban.
|
|
21
|
+
|
|
20
22
|
## Must
|
|
21
23
|
|
|
22
24
|
- Name the `.claude/` corpus in a trigger phrase rather than in the opening clause alone, so a bare request to sync the docs separates this skill from `docs-sync` on something both descriptions state
|
|
@@ -34,6 +36,10 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
|
|
|
34
36
|
- Leave the current branch's review receipt alone, since the chain that wrote it cites it in its own closing line and this skill cannot read whether that citation is still live
|
|
35
37
|
- Land each block of a promotion handoff at the destination its heading names, then delete the file so a later run does not fold it twice
|
|
36
38
|
- Take a promotion destination as already decided, since the operator confirmed it where the page was produced
|
|
39
|
+
- Classify the fold's whole diff baseline through `canon context classify diff`, calling the verb rather than reimplementing its pattern or its prompt in the skill body, since an earlier commit on the branch carries a doc edit the fold is equally responsible for
|
|
40
|
+
- Apply a `REPLACE` or `HISTORY` finding, and a regex-decided `MOVE` finding, against the quote it names, report a model-decided `MOVE` finding naming the surface it belongs on rather than cutting it, or keep a finding with a one-line reason, rather than leaving a finding unanswered
|
|
41
|
+
- Rewrite a restated or superseded statement in place rather than appending the replacement beside it, on every canonical doc type this skill writes
|
|
42
|
+
- Report a refusal or a missing `context classify` subcommand as one line and continue the fold either way, since a check that cannot run is not a reason to leave the fold's own output unshipped
|
|
37
43
|
|
|
38
44
|
## Must not
|
|
39
45
|
|
|
@@ -45,6 +51,13 @@ The trigger side carries a gap of its own. "Sync the docs" names either corpus t
|
|
|
45
51
|
- Overwrite a file a promotion block routes to. A destination that already holds a page is a merge for a person, and folding over it discards work this skill never read.
|
|
46
52
|
- Write an anchor onto a decision the run did not amend, or refresh one without re-reading the number. A date from a pass that measured nothing is the false confidence the marker exists to prevent.
|
|
47
53
|
- Anchor an entry written before the rule, which dates it by blame rather than by a read
|
|
54
|
+
- Pick or configure a classifier backend. The project setting decides it, and this skill reports the resolved `modelLayer` as returned.
|
|
55
|
+
- Stop the fold on a classify finding, a refusal, or a missing subcommand. All three report and continue.
|
|
56
|
+
- Apply a finding against a quote found more than once or not found at all. Report that it could not be located instead of guessing.
|
|
57
|
+
- Re-run the classifier after applying a finding, which loops it over its own edit
|
|
58
|
+
- Cut a model-decided `MOVE` finding. The model's own prompt defines `MOVE` more broadly than the regex layer does, for correct content sitting on the wrong surface rather than a deletion candidate, so cutting one can discard content that belongs elsewhere rather than removing detail that never belonged at all.
|
|
59
|
+
- Paste a regex-decided `MOVE` finding's content into another canonical doc after cutting it. It never belonged on the wireframe surface at all, so cutting is the whole fix, not a relocation.
|
|
60
|
+
- Scope Step 10 to the files Steps 3 and 7 wrote this run. An earlier commit on the branch carries a doc edit the fold is equally responsible for, and the verb's own extraction already answers whether anything in range qualifies.
|
|
48
61
|
|
|
49
62
|
## Guards
|
|
50
63
|
|
|
@@ -108,6 +108,7 @@ Read `ok` and `reason` out of that record rather than the exit. An operator's sh
|
|
|
108
108
|
|
|
109
109
|
- Update only the sections affected by session decisions.
|
|
110
110
|
- Do not rewrite sections unrelated to what changed.
|
|
111
|
+
- Rewrite a restated or superseded statement in place rather than appending the replacement beside it. State the fact that stands and keep the earlier reasoning only where it is the alternative that lost, per `${CLAUDE_SKILL_DIR}/../../standards/context.md` and `${CLAUDE_SKILL_DIR}/../../standards/architecture.md`.
|
|
111
112
|
- Follow `${CLAUDE_SKILL_DIR}/../../standards/markdown.md` and the `write-human` skill for all edits.
|
|
112
113
|
- Close a decision entry in `canon/ARCHITECTURE.md` with its verification anchor whenever this run writes that entry or amends its reasoning and that reasoning cites a measured number. Re-read the number against the tree first, since the marker records the read rather than the edit. `${CLAUDE_SKILL_DIR}/../../standards/architecture.md` fixes the sentence.
|
|
113
114
|
- Leave every decision entry this run did not write alone, anchored or not. The rule is scoped forward, so an entry written before it is dated by blame rather than by a read. Step 5 reports a stale anchor and no step writes one on an entry it did not amend.
|
|
@@ -160,6 +161,7 @@ Reuse the diff from the baseline above, names and content both. For each domain
|
|
|
160
161
|
|
|
161
162
|
- Map the entry's section headings, whether they sit in one flat file or spread across a nested domain's sibling files, to the changed files. An entry is relevant when its prose references files, modules, or decisions touched by the diff.
|
|
162
163
|
- For each relevant entry, rewrite only the sections affected by the diff. Same pattern as `docs-sync`. Do not touch unrelated sections. Never rewrite a split domain's own `index.md` directly, since a regen overwrites it the same way it overwrites the top-level catalog. Rewrite the sibling file the affected section actually lives in instead.
|
|
164
|
+
- Rewrite a restated or superseded statement in place rather than appending beside it, the same rule Step 3 applies to the other four canonical doc types.
|
|
163
165
|
- Write a reference to another entry as the path that entry sits at, rather than as its bare filename. `${CLAUDE_SKILL_DIR}/../../standards/context.md` states the form, and a bare name strands the reference once a domain splits into subfolders.
|
|
164
166
|
|
|
165
167
|
### When the diff removes a capability
|
|
@@ -245,12 +247,30 @@ Output one line per file swept:
|
|
|
245
247
|
|
|
246
248
|
If nothing qualifies, skip this step silently.
|
|
247
249
|
|
|
250
|
+
## Step 10: classify the fold's diff baseline
|
|
251
|
+
|
|
252
|
+
Skip this step silently only when the Diff baseline section could not resolve a base ref at all, reporting `⚠ No diff to scope against. Skipped the classify check.` The verb needs a resolvable ref to run against, which is the one condition it cannot answer for itself.
|
|
253
|
+
|
|
254
|
+
Otherwise resolve `<base>` the way the Diff baseline section already does for Steps 2, 4, 5, and 7, and reuse it rather than resolving a second time. Run the check over the fold's whole baseline rather than scoping it to what Steps 3 and 7 wrote this run: earlier commits on the branch carry doc edits the fold is equally responsible for, and the verb's own extraction already scopes to canonical doc types and reports nothing when the range carries none.
|
|
255
|
+
|
|
256
|
+
The invocation is:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
canon context classify diff --base <base> --json
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Never substitute a different verb for it, such as `canon autoship classify` (a different check, over a different scope) or `canon docs <name>` (a documentation lookup, not a classification). Read `${CLAUDE_SKILL_DIR}/references/classify.md` for the record fields, applying a finding, the one-line keep reason, the unreachable and missing-subcommand lines, and the report shape. `${CLAUDE_SKILL_DIR}` names this skill's own directory, resolved once when the skill loaded, several steps before this one. If Step 10's memory of that path is uncertain, read the reference by that resolved path rather than guessing a `.claude/skills/docs-fold/references/classify.md` path from the toolkit's install-time layout, which is a different root than the one this skill's own files live under. The invocation above runs either way, whether or not that reference resolves.
|
|
263
|
+
|
|
264
|
+
Findings never stop the fold. A refusal or a missing subcommand on an older installed binary reports one line, per that reference, and the fold continues either way.
|
|
265
|
+
|
|
248
266
|
## After completion
|
|
249
267
|
|
|
250
268
|
Output one line per file updated:
|
|
251
269
|
|
|
252
270
|
`✅ Updated: .claude/<filename>`
|
|
253
271
|
|
|
272
|
+
Step 10 adds its own lines when it applied or reported a finding, in the exact shape `${CLAUDE_SKILL_DIR}/references/classify.md` gives them under its own Report section. Do not shorten or paraphrase those lines here or in the reply, since the quote and the reason are what a reader checks the finding against.
|
|
273
|
+
|
|
254
274
|
If no files were updated and nothing was swept, output:
|
|
255
275
|
|
|
256
276
|
`✅ No changes needed.`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Classify the fold's diff baseline
|
|
3
|
+
description: The classify diff invocation, the record fields to read, applying a finding, the one-line keep reason, the unreachable and missing-subcommand lines, and the report shape
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Classify the fold's diff baseline
|
|
7
|
+
|
|
8
|
+
Mechanics for Step 10 of `docs-fold`. The body owns the skip condition and the shared baseline, and this file owns the invocation, what the record carries, and what applying a finding does.
|
|
9
|
+
|
|
10
|
+
## Invocation
|
|
11
|
+
|
|
12
|
+
Call the verb rather than reimplementing its pattern or prompt in this skill:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
canon context classify diff --base <base> --json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Reuse the base the Diff baseline section already resolved. Never pick or configure a backend here. The project setting decides it, and this step reports `modelLayer` as the record returns it.
|
|
19
|
+
|
|
20
|
+
## Reading the record
|
|
21
|
+
|
|
22
|
+
Branch on the record's `decision` field, never on the exit code, which a shell function wrapping `canon` can flatten to zero.
|
|
23
|
+
|
|
24
|
+
- `decision: 'refused'`: a bad range or a file the verb could not read. Report the record's `message` and continue. See "When the verb is absent or refused" below.
|
|
25
|
+
- `decision: 'ok'`: the record carries `modelLayer` (`'off'`, `'ran'`, `'skipped-no-model'`, or `'skipped-unreachable'`) and `findings`.
|
|
26
|
+
|
|
27
|
+
Each finding carries `file`, a `verdict` of `KEEP`, `REPLACE`, `HISTORY`, or `MOVE`, and `decidedBy` (`'regex'` or `'model'`) naming which layer's reading won. Take the `quote` and `reason` from whichever of the finding's `regex` or `model` fields `decidedBy` names. Skip every `KEEP` finding: nothing decided against that text.
|
|
28
|
+
|
|
29
|
+
## Scope
|
|
30
|
+
|
|
31
|
+
Answer every non-`KEEP` finding the verb returns, not only the files Step 3 or Step 7 wrote this run. A branch carrying earlier commits from a prior session has doc edits the fold is equally responsible for, and the verb's own extraction already scopes to canonical doc types and reports nothing when the range carries none, so there is no narrower check to add here.
|
|
32
|
+
|
|
33
|
+
## Applying a finding
|
|
34
|
+
|
|
35
|
+
Locate the finding by its `quote` in the file's current content. The quote is often a narrow fragment rather than the whole clause it sits in, since the regex layer's own match is a short pattern hit, so the edit below targets the sentence or bullet the quote sits in rather than the literal substring alone.
|
|
36
|
+
|
|
37
|
+
- **Found exactly once.** Apply the verdict:
|
|
38
|
+
- `REPLACE`: rewrite the sentence or bullet carrying the quote in place with the fact that now stands, per Step 3 and Step 7's rewrite-in-place rule. State what is current and drop what the quote restated, in the one edit.
|
|
39
|
+
- `HISTORY`: rewrite the sentence or bullet carrying the quote to drop the narration and keep any current fact the same clause states. The quote narrates how the fact arrived rather than stating the fact, which a canonical doc excludes regardless of what wrote it, and a literal cut of the fragment alone would leave the rest of the clause grammatically stranded.
|
|
40
|
+
- `MOVE`, regex-decided (`decidedBy: 'regex'`): cut the sentence or bullet carrying the quote and stop there. Do not also paste it into another canonical doc, however close a match the current diff makes one look. The regex layer only ever returns `MOVE` inside `canon/wireframes/`, on prose naming a source-file path, which is implementation detail a wireframe surface does not carry, so a regex-decided `MOVE` is always safe to remove outright, and removing it is the whole fix.
|
|
41
|
+
- `MOVE`, model-decided (`decidedBy: 'model'`): do not cut. The model's own prompt defines `MOVE` more broadly, for correct content sitting on the wrong surface, such as domain mechanism written into `canon/ARCHITECTURE.md` that belongs in a context entry. Cutting would delete content a fold with no model configured would never have flagged at all. Report the finding instead, naming the surface the `reason` names as where the content belongs, and leave the file unedited.
|
|
42
|
+
- **Found more than once, or not found at all.** Report that the finding could not be located rather than guessing which occurrence or rewriting nothing silently. A model verdict paraphrasing the quote it read is the ordinary way this happens.
|
|
43
|
+
|
|
44
|
+
Run the classifier once per fold. Do not re-run it after applying a finding to check the edit, since a second pass over what this step wrote is the loop the verb's own reference already warns against.
|
|
45
|
+
|
|
46
|
+
## Keeping a finding
|
|
47
|
+
|
|
48
|
+
Keep a non-`KEEP` finding without applying it only when the quote is itself the alternative a nearby decision states and lost, or the file already carries the current fact elsewhere. State the reason in one line in the report. A keep costs one line, where an incorrect apply costs a rewrite of prose nobody asked to change.
|
|
49
|
+
|
|
50
|
+
## When the verb is absent or refused
|
|
51
|
+
|
|
52
|
+
The verb ships with the CLI and this skill ships with the plugin, so a target holding an older installed binary carries no `context classify` subcommand. Report:
|
|
53
|
+
|
|
54
|
+
`⚠ Classify: canon context classify not available on the installed binary. Skipped.`
|
|
55
|
+
|
|
56
|
+
and continue. Never read a missing subcommand as a clean pass, which would report a run that never checked anything as one that found nothing.
|
|
57
|
+
|
|
58
|
+
A refused range reports the same way, naming the verb's own message:
|
|
59
|
+
|
|
60
|
+
`⚠ Classify: <message>`
|
|
61
|
+
|
|
62
|
+
Neither case stops the fold. This step checks what the fold wrote, and a check that cannot run is not a reason to leave the fold's own output unshipped.
|
|
63
|
+
|
|
64
|
+
## Report
|
|
65
|
+
|
|
66
|
+
One line naming what ran:
|
|
67
|
+
|
|
68
|
+
`✅ Classify: <n> findings, regex ran, model <modelLayer>`
|
|
69
|
+
|
|
70
|
+
Then one line per non-`KEEP` finding:
|
|
71
|
+
|
|
72
|
+
- `✏ Rewrote: <file>, "<quote>" (<reason>)`
|
|
73
|
+
- `✂ Cut: <file>, "<quote>" (<reason>)`
|
|
74
|
+
- `➡ Move needed: <file>, "<quote>" (<reason>)`, for a model-decided `MOVE` this step reported rather than cut
|
|
75
|
+
- `⏭ Kept: <file>, "<quote>", <reason for keeping>`
|
|
76
|
+
- `⚠ Not located: <file>, "<quote>" not found or found more than once`
|
|
77
|
+
|
|
78
|
+
When every finding reads `KEEP`, output `✅ Classify: <n> findings, all KEEP` and nothing further.
|
|
@@ -50,7 +50,7 @@ Pass `--variant light` or `--variant dark` to override the source variant for a
|
|
|
50
50
|
Run this once after the first render, then stop.
|
|
51
51
|
|
|
52
52
|
1. Convert the deck to images: `soffice --headless --convert-to pdf <deck>.pptx` then `pdftoppm -r 90 -png <deck>.pdf slide`. If `soffice` or `pdftoppm` is missing, skip the image pass and say so. Do not fail the render.
|
|
53
|
-
2.
|
|
53
|
+
2. Open and inspect every rendered image in this session, checking each slide for overlap, overflow, low contrast, and empty regions.
|
|
54
54
|
3. Fix the reported issues in `SLIDES.md` once, re-render, and stop. Do not loop indefinitely on aesthetics.
|
|
55
55
|
|
|
56
56
|
## Response
|
|
@@ -11,7 +11,7 @@ Without this skill, a session drafting a wireframe for a surface with no file ye
|
|
|
11
11
|
|
|
12
12
|
## Must
|
|
13
13
|
|
|
14
|
-
- Read `standards/wireframes.md` before drafting, since the frontmatter contract, the
|
|
14
|
+
- Read `standards/wireframes.md` before drafting, since the frontmatter contract, the regions list, the states table, the exclusions section, and the Transcription-wireframes branch are what make the file arguable against a sibling
|
|
15
15
|
- Walk the whole `canon/wireframes/` tree, including a grouped surface's own subfolder, before drafting, since a top-level-only check misses a nested match
|
|
16
16
|
- Detect an existing higher tier from `canon/DESIGN.md` and the wireframes tree and report it, never build a companion render for it, since no shipped mechanism produces one
|
|
17
17
|
- Draft in transcription mode, citing the real source, when the surface names an already-built component. Draft in role-intent mode otherwise.
|
|
@@ -11,7 +11,7 @@ A project the surface move has not reached keeps its wireframes folder under `.c
|
|
|
11
11
|
|
|
12
12
|
Read these files in parallel:
|
|
13
13
|
|
|
14
|
-
- `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`: the
|
|
14
|
+
- `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`: the four questions a wireframe must answer, its frontmatter, the regions list and states table, the exclusions section, the Transcription-wireframes branch, and what moves to a context entry instead
|
|
15
15
|
- `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: banned words, punctuation, and formatting for the prose around the fences
|
|
16
16
|
- The `write-human` skill: voice, rhythm, and sentence construction for the prose around the fences
|
|
17
17
|
|
|
@@ -25,15 +25,18 @@ This skill stays fully independent of `docs-fold`'s wireframe coverage sweep, wh
|
|
|
25
25
|
## Tier detection
|
|
26
26
|
|
|
27
27
|
- Read `canon/DESIGN.md` and every existing `canon/wireframes/` file for a tier signal: a Stitch, Excalidraw, or Figma reference, or a marker naming one of them.
|
|
28
|
-
- State the detected tier at the confirm step. Always draft the
|
|
28
|
+
- State the detected tier at the confirm step. Always draft the regions-and-states shape regardless of what is detected, since that is the only shape this skill or any other shipped mechanism produces, adding the ASCII sketch fence only when the layout is not built yet. Report a higher tier rather than attempting a companion render for it.
|
|
29
29
|
- Default silently to tier 0 when nothing is detected.
|
|
30
30
|
|
|
31
31
|
## Draft
|
|
32
32
|
|
|
33
33
|
- Decide the mode before drafting. When the surface names an already-built component or file, open that source and draft in transcription mode, citing the render function, the stylesheet rule, or the built file each region and label traces to, per the standard's Transcription-wireframes section.
|
|
34
34
|
- Draft in role-intent mode otherwise: label each region by its role, never by a class name or a token value.
|
|
35
|
-
- Draft `title` and `description` frontmatter, then
|
|
36
|
-
-
|
|
35
|
+
- Draft `title` and `description` frontmatter, then a lead paragraph naming what the surface is for and what it covers, then `## Regions` as a bullet list naming every region and where it sits relative to the others.
|
|
36
|
+
- Draw an ASCII `plaintext` fence with `←` role annotations only when the layout is not built yet. Skip the fence in transcription mode, which already cites the built source instead.
|
|
37
|
+
- Draft `## States` as a table listing every state a visitor can reach, what reaches it, what it shows in words, and its evidence folder, reading `not captured` in the evidence cell until a capture lands.
|
|
38
|
+
- Follow with `## Copy`, `## Behavior`, and `## Not on this surface`, against `${CLAUDE_SKILL_DIR}/../../standards/wireframes.md`'s template.
|
|
39
|
+
- Give a layout that changes across a breakpoint or state its own entry in the regions list, named by what triggers it, never for a spacing difference alone.
|
|
37
40
|
- Leave out algorithms, event-handler code, framework prop or class names outside transcription mode, and anything else the standard sends to a context entry instead.
|
|
38
41
|
|
|
39
42
|
## Confirm
|
package/docs/agents/commands.md
CHANGED
|
@@ -66,7 +66,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
|
|
|
66
66
|
| `canon worktrees list` | Report which worktrees are reclaimable, keyed on the pull request having merged, with every refusal and the removal route named (`--json`) |
|
|
67
67
|
| `canon worktrees reclaim` | Remove every reclaimable worktree and the branch behind it, reporting without acting under `--dry-run` (`--json`) |
|
|
68
68
|
| `canon comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
69
|
-
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and
|
|
69
|
+
| `canon context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record's word weight, and wireframe states against their evidence folders |
|
|
70
70
|
| `canon context classify diff` | Classify the chunks a git range changed, each with its enclosing section, as keep, replace, history, or move (`--base`, `--doc-types`, `--json`) |
|
|
71
71
|
| `canon context classify sweep` | Classify every section of the five canonical doc types, split at H3, as keep, rewrite, or move (`--doc-types`, `--json`) |
|
|
72
72
|
| `canon context classifier show` | Report the resolved classifier backend and model and which source decided them (`--json`) |
|
|
@@ -55,6 +55,8 @@ The table check reports a catalog that grows a row per shipped thing, not a tabl
|
|
|
55
55
|
|
|
56
56
|
The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open.
|
|
57
57
|
|
|
58
|
+
A change number matches at any digit count, so `PR #7` and `#49` report alongside a four-digit number. Displayed spans are masked before the match runs, which is what keeps a heading's own auto-derived link destination, `(#7-open-questions)`, and a hex color in a code span, `` `#000` ``, from reading as a change reference: both are text an entry shows rather than a claim it makes. <!-- canon-allow-reference: illustrates the input shape the rule reads -->
|
|
59
|
+
|
|
58
60
|
A date stamping a measurement is excluded, because the standard cuts a date attached to a change and permits one dating a figure. The check reads the clause in front of the date, back to the nearest sentence boundary, for one of five verbs: measured, verified, driven, passed, and fired. The noun `run` counts only where it sits against the date, so `A run on 2026-08-14` is excluded and `Runs on erclx/canon#632 and erclx/canon#634 landed 2026-08-02` reports. The set is closed, and a date it cannot place reports as a change marker rather than as a state of its own, which names one date too many rather than clearing one the rule cuts. <!-- canon-allow-reference: illustrates the input shape the rule reads -->
|
|
59
61
|
|
|
60
62
|
A release label reports with or without its leading `v` at three segments, since the rule cuts the label rather than a spelling of it. Two segments still need the `v`, which keeps a dollar cost and a duration out. Another tool's version reports too, and the check cannot tell one from a release, so treat a version beside a tool name as a line to read rather than one to cut.
|
|
@@ -89,7 +91,7 @@ The JSON record carries the findings per entry as `entries[].narration` and the
|
|
|
89
91
|
|
|
90
92
|
## The architecture record
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
Three findings read `canon/ARCHITECTURE.md` rather than a folder, and only the first is a fact.
|
|
93
95
|
|
|
94
96
|
The length check compares the record against the ceiling it derives for itself, and only a record that states its own allowances has one. No standard sets a length rule for this document, so the numbers belong to whichever record declares them. The check reads a frame allowance and an allowance per decision out of the record's own prose and puts the ceiling at the frame plus the allowance times the decision count. The JSON record carries what it read as `architecture.allowances` and the reading as `architecture.lines` against `architecture.ceiling`.
|
|
95
97
|
|
|
@@ -103,9 +105,27 @@ Three limits are stated on every run rather than hidden. The countable signal re
|
|
|
103
105
|
|
|
104
106
|
The report gates nothing. Deciding whether a sentence states a claim is a judgment no parser settles, so the output names candidates for a reader. This answers a different question from the verification anchors `standards/architecture.md` describes, which record that one cited number was re-read. That mechanism says whether a marked figure held, and this one says how much of the record could be checked at all.
|
|
105
107
|
|
|
108
|
+
The third finding is a word count, measured in words rather than lines: one figure for the whole record and one per decision. `standards/architecture.md` asks a session to judge the file's weight by reading it rather than by counting it, and reads the word figure alongside that judgment when one is available. A paragraph written one source line to a paragraph passes the rendered-line measure other checks use while still reading heavy, which is the gap a word count closes without turning into a second cap.
|
|
109
|
+
|
|
110
|
+
The `## Risks / open questions` section is weighed the same way, reported apart from the whole-record figure since the standard singles it out for holding only what is still open. This finding gates nothing under any mode. The JSON record carries the whole-record figure as `architecture.words`, the section figure as `architecture.risksWords` where the record carries the heading, and the per-decision figure as `architecture.decisions[].words`.
|
|
111
|
+
|
|
112
|
+
## Wireframe states
|
|
113
|
+
|
|
114
|
+
Every entry under `canon/wireframes/` carrying a `## States` table is checked against its evidence folders on disk. `State` and `Evidence` are matched by header text rather than column position, since the standard's own template could still move a column during its own review. An entry with no such table is out of scope and reports nothing.
|
|
115
|
+
|
|
116
|
+
An evidence cell's path is read as written and resolved against the project root, since the standard puts the path in the cell rather than a state name a convention would have to guess at. A state whose cited path resolves to no directory is a missing folder, measured in states: one finding per row. A directory sitting under a root at least one row already cites, but named in no row itself, is an unlisted folder, measured in folders: one finding per directory.
|
|
117
|
+
|
|
118
|
+
Both are counted rather than only listed, since a count is what a reader compares run to run. A `not captured` cell is the one value the standard exempts from having a folder, and it drops out of both counts.
|
|
119
|
+
|
|
120
|
+
The third finding reads the whole entry rather than one row: whether it carries a `plaintext`-fenced sketch while a state's evidence already exists on disk. The standard asks the fence to come out the moment a layout has a capture to show instead, so a sketch surviving past that point is a boolean per entry rather than a count.
|
|
121
|
+
|
|
122
|
+
The unit here is the entry rather than the layout the standard's own rule is stated against. A table names states, not the layouts a wireframe's `## Regions` section can split into at a breakpoint, so this finding has no narrower unit to match a sketch against the evidence for its own layout alone. An entry with a captured default layout and a sketch for a breakpoint layout that is not built yet is conforming, and this finding still reports it, which is why it stays advisory under every mode rather than joining the states-mismatch gate below.
|
|
123
|
+
|
|
124
|
+
All three wireframe findings are printed under a bare run. The states-mismatch finding, missing and unlisted folders together, gates at `2` under `--gate`, alongside a missing required section and index drift. The sketch finding stays advisory under both modes, for the reason above. The JSON record carries them per entry under `wireframes[]`, as `rows`, `missingFolders`, `unlistedFolders`, and `sketchWithEvidence` alongside `sketchLine`.
|
|
125
|
+
|
|
106
126
|
## Which folders each check reaches
|
|
107
127
|
|
|
108
|
-
The provenance, required-section, and narration checks cover `canon/context/` alone, the reference-form check covers the split folders inside it,
|
|
128
|
+
The provenance, required-section, and narration checks cover `canon/context/` alone, the reference-form check covers the split folders inside it, length and the table finding reach every audited folder, and the wireframe states check covers `canon/wireframes/` alone.
|
|
109
129
|
|
|
110
130
|
What narrows the three is stated in `standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
|
|
111
131
|
|
|
@@ -37,9 +37,11 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
|
|
|
37
37
|
|
|
38
38
|
## Exit codes
|
|
39
39
|
|
|
40
|
-
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`, and a record stating none is reported and never gated.
|
|
40
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. An architecture record that states its own line allowances gates when it is past the ceiling those derive, on any run that measures it, which is every mode except `--citations-only`, and a record stating none is reported and never gated.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Entry length, reference form, table, provenance, narration, the record's claim classification, and every word figure print and return `0` under every mode, because each is a judgment or a weight read alongside one, and failing a push on either would make the check something to route around. Narration is the weakest of the printed measures, since whether two bullets share a subject is a call the measure approximates from structure alone, and one of the shapes it matches is the rejected alternative the standard asks an entry to keep.
|
|
43
|
+
|
|
44
|
+
Required-section, index, and wireframe-states findings sit between the two. All three are answerable from the file rather than weighed, so `--gate` promotes them to failing codes while a bare run leaves them advisory. Sketch-with-evidence sits with the printed measures instead, for the reason `context-audit-checks.md` states: it reads a whole entry against whether any of its states has evidence, not one layout against its own evidence, so a conforming file can still trip it. The toolkit runs the bare form against itself and the widened form against the seed tree, described below.
|
|
43
45
|
|
|
44
46
|
## The seed gate
|
|
45
47
|
|
|
@@ -81,3 +83,11 @@ Append `<!-- audit-ignore-citations: <path> -->` to the source line in either ca
|
|
|
81
83
|
The marker itself stays out of anything that installs. A seed, a plugin skill body, and a stack reference all reach a target, so a marker there lands as toolkit bookkeeping in someone else's tree. Reword those lines to drop the path instead, and where a stop message has to spell it, move that message into a fenced block, which this check already skips.
|
|
82
84
|
|
|
83
85
|
The pattern spells both record-root prefixes, so a citation into a folder that has moved still resolves and a folder resolved at the project root is measured by every other check while contributing nothing here. A pattern fixed at one root matches nothing after a move and reports nothing, which is a stale reference passing the check written to find it. Widening it to a bare `docs/x.md` would match prose that references nothing, which is a separate decision from where entries come from. A run whose folders all resolved at the root says the check is out of scope rather than reporting that zero paths resolved, and the same run under `--citations-only` refuses, because a gate exiting clean on a scope it could not build is the failure the gate exists to catch.
|
|
86
|
+
|
|
87
|
+
## The wireframe states gate
|
|
88
|
+
|
|
89
|
+
Every entry under `canon/wireframes/` carrying a `## States` table is checked one-to-one against its evidence folders, matching the table's `State` and `Evidence` columns by header text rather than position, so a project reordering the template's own columns still resolves. An entry with no such table reports nothing, which is what leaves a project on an older wireframe shape silent until it adopts one.
|
|
90
|
+
|
|
91
|
+
An evidence cell resolves as the literal path it names, taken from the project root. A state whose cited folder does not exist is a states mismatch, and so is a folder sitting under a cited evidence root that no row names. A `not captured` cell is the one exception the standard admits, and it excuses that row from both readings. A `plaintext` sketch still carried once a state's evidence exists on disk is a third finding this check adds, reading the entry as a whole rather than one row.
|
|
92
|
+
|
|
93
|
+
The states mismatch is printed under a bare run and gates at `2` under `--gate`, alongside a missing required section and index drift. The sketch finding is printed and stays advisory under both modes: it has no way to pair a sketched layout with that layout's own evidence, only the entry with any evidence at all, so a conforming file, a captured default layout beside a sketch of a breakpoint layout nobody has built, can still trip it.
|
|
@@ -5,7 +5,7 @@ description: Classifying canonical-doc content as keep, replace/rewrite, history
|
|
|
5
5
|
|
|
6
6
|
# Doc classifier
|
|
7
7
|
|
|
8
|
-
`canon context classify` reads content from the five canonical doc types (`context/<domain>.md`, `ARCHITECTURE.md`, `wireframes/<surface>.md`, `DESIGN.md`, `REQUIREMENTS.md`) and reports whether each piece states the project as it stands or should be rewritten, narrated elsewhere, or moved to another surface. It reports and never gates: a finding is a judgment for a writer to act on, not a fact a push fails over. Wiring it into
|
|
8
|
+
`canon context classify` reads content from the five canonical doc types (`context/<domain>.md`, `ARCHITECTURE.md`, `wireframes/<surface>.md`, `DESIGN.md`, `REQUIREMENTS.md`) and reports whether each piece states the project as it stands or should be rewritten, narrated elsewhere, or moved to another surface. It reports and never gates: a finding is a judgment for a writer to act on, not a fact a push fails over. The `docs-fold` skill calls this verb in diff mode over its own diff baseline. Wiring it into the merge gate or `canon context audit` is separate work this verb does not do.
|
|
9
9
|
|
|
10
10
|
Two modes read different units. Diff mode reads the chunks a git range changed, each with the section it landed in, which is what a session checks right after an edit. Sweep mode reads every section of the five doc types, split at H3, which is what a cleanup pass or a periodic re-scan runs over a whole corpus.
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ canon/
|
|
|
19
19
|
├── REQUIREMENTS.md ← goals, non-goals, MVP scope
|
|
20
20
|
├── ARCHITECTURE.md ← technical design decisions
|
|
21
21
|
├── DESIGN.md ← visual intent and token decisions (UI projects)
|
|
22
|
-
├── wireframes/ ←
|
|
22
|
+
├── wireframes/ ← regions, states, UI copy, and interaction rules (UI projects)
|
|
23
23
|
├── context/ ← per-domain narrative loaded on demand via index.md
|
|
24
24
|
└── decisions/ ← decision history a canonical doc points at, never loaded eagerly
|
|
25
25
|
|
|
@@ -14,7 +14,7 @@ Two tools anchor tier 1 and tier 2. Stitch is the agent-addressable default thro
|
|
|
14
14
|
|
|
15
15
|
## Tier 0: prose only
|
|
16
16
|
|
|
17
|
-
The default. `canon/DESIGN.md` holds visual intent as prose plus token tables for color, typography, spacing, borders, motion, and iconography. `.claude/WIREFRAMES.md` holds
|
|
17
|
+
The default. `canon/DESIGN.md` holds visual intent as prose plus token tables for color, typography, spacing, borders, motion, and iconography. `.claude/WIREFRAMES.md` holds each surface's regions and states.
|
|
18
18
|
|
|
19
19
|
Claude Code reads both and writes the implementation. Works for CLI tools, internal dashboards, admin panels, and backend-focused projects.
|
|
20
20
|
|
|
@@ -9,5 +9,5 @@ paths:
|
|
|
9
9
|
|
|
10
10
|
## Authority
|
|
11
11
|
|
|
12
|
-
- Follow the wireframes standard for layout and interaction intent:
|
|
12
|
+
- Follow the wireframes standard for layout and interaction intent: regions, the states table and its evidence links, copy, behavior, exclusions, and what moves to `canon/context/`. It is the single source. Read it with `canon standards wireframes`.
|
|
13
13
|
- Read it before adding or revising a surface.
|
package/package.json
CHANGED
package/src/commands/context.ts
CHANGED
|
@@ -59,6 +59,10 @@ import {
|
|
|
59
59
|
PRONOUN_HEADING,
|
|
60
60
|
VERB_HEADING,
|
|
61
61
|
} from '@/context/narration'
|
|
62
|
+
import {
|
|
63
|
+
measureWireframeFolder,
|
|
64
|
+
type WireframeStatesReport,
|
|
65
|
+
} from '@/context/wireframe-states'
|
|
62
66
|
import { RENDER_WIDTH } from '@/markdown/structure'
|
|
63
67
|
import {
|
|
64
68
|
frameError,
|
|
@@ -94,7 +98,7 @@ export function register(program: Command): void {
|
|
|
94
98
|
context
|
|
95
99
|
.command('audit')
|
|
96
100
|
.description(
|
|
97
|
-
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift,
|
|
101
|
+
'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, index drift, the architecture record against its own ceiling and its word weight, and wireframe states against their evidence folders',
|
|
98
102
|
)
|
|
99
103
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
100
104
|
.helpOption('-h, --help', 'Show this help message')
|
|
@@ -662,6 +666,15 @@ async function runAudit(
|
|
|
662
666
|
// are different answers, and one value for both reports the second as the
|
|
663
667
|
// first.
|
|
664
668
|
const record = gateOnly ? undefined : await measureArchitecture(root)
|
|
669
|
+
const wireframes = gateOnly
|
|
670
|
+
? []
|
|
671
|
+
: (
|
|
672
|
+
await Promise.all(
|
|
673
|
+
folders
|
|
674
|
+
.filter((folder) => folder.name === 'wireframes')
|
|
675
|
+
.map((folder) => measureWireframeFolder(root, folder)),
|
|
676
|
+
)
|
|
677
|
+
).flat()
|
|
665
678
|
|
|
666
679
|
if (gateOnly) {
|
|
667
680
|
reportGate(citations)
|
|
@@ -677,6 +690,7 @@ async function runAudit(
|
|
|
677
690
|
reportNarration(entries, folders, narration)
|
|
678
691
|
reportDrift(drift)
|
|
679
692
|
reportRecord(record, root)
|
|
693
|
+
reportWireframeStates(wireframes)
|
|
680
694
|
outro()
|
|
681
695
|
}
|
|
682
696
|
|
|
@@ -713,6 +727,11 @@ async function runAudit(
|
|
|
713
727
|
// target that never wrote one is entitled to. Absent says the run
|
|
714
728
|
// never looked, which is `--citations-only`.
|
|
715
729
|
architecture: gateOnly ? undefined : (record ?? null),
|
|
730
|
+
// Absent under `--citations-only`, for the same reason as above. An
|
|
731
|
+
// empty array under the ordinary run says the project carries no
|
|
732
|
+
// wireframes folder or no entry carrying a States table, which is a
|
|
733
|
+
// fact rather than an unmeasured run.
|
|
734
|
+
wireframes: gateOnly ? undefined : wireframes,
|
|
716
735
|
checkpoints: {
|
|
717
736
|
lines: LENGTH_CHECKPOINT,
|
|
718
737
|
renderWidth: RENDER_WIDTH,
|
|
@@ -742,6 +761,7 @@ async function runAudit(
|
|
|
742
761
|
recordOverLength: record !== undefined && isOverLength(record),
|
|
743
762
|
sections,
|
|
744
763
|
drift,
|
|
764
|
+
wireframes,
|
|
745
765
|
widened,
|
|
746
766
|
})
|
|
747
767
|
|
|
@@ -1202,6 +1222,15 @@ function reportRecord(
|
|
|
1202
1222
|
const decisions = report.decisions.length
|
|
1203
1223
|
const { allowances } = report
|
|
1204
1224
|
|
|
1225
|
+
logInfo(
|
|
1226
|
+
`${plural(report.words, 'word')} across ${plural(report.lines, 'line')}, read alongside the weight judgment a session makes by reading the file. This never gates.`,
|
|
1227
|
+
)
|
|
1228
|
+
if (report.risksWords !== undefined) {
|
|
1229
|
+
logInfo(
|
|
1230
|
+
`\`## Risks / open questions\` holds ${plural(report.risksWords, 'word')}, weighed the same way and read alongside the same judgment.`,
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1205
1234
|
if (allowances === undefined) {
|
|
1206
1235
|
logInfo(
|
|
1207
1236
|
`Covers ${report.rel} alone. No standard sets a length rule for it and this record states none, so its ${plural(report.lines, 'line')} across ${plural(decisions, 'decision')} are reported and nothing is gated.`,
|
|
@@ -1261,7 +1290,7 @@ function reportRecord(
|
|
|
1261
1290
|
entry.checks.length > 0
|
|
1262
1291
|
? `\n checked by ${entry.checks.join(', ')}`
|
|
1263
1292
|
: ''
|
|
1264
|
-
return `${report.rel}:${entry.line} ${kind}${evidence}\n ${entry.heading}${checks}`
|
|
1293
|
+
return `${report.rel}:${entry.line} ${kind}${evidence} ${plural(entry.words, 'word')}\n ${entry.heading}${checks}`
|
|
1265
1294
|
})
|
|
1266
1295
|
.join('\n'),
|
|
1267
1296
|
)
|
|
@@ -1283,3 +1312,43 @@ function reportDrift(drift: readonly FolderDrift[]): void {
|
|
|
1283
1312
|
logWarn(plural(lines.length, 'disagreement'))
|
|
1284
1313
|
pipeOutput(lines.join('\n'))
|
|
1285
1314
|
}
|
|
1315
|
+
|
|
1316
|
+
function reportWireframeStates(
|
|
1317
|
+
wireframes: readonly WireframeStatesReport[],
|
|
1318
|
+
): void {
|
|
1319
|
+
logStep('Wireframe states')
|
|
1320
|
+
|
|
1321
|
+
const withRows = wireframes.filter((entry) => entry.rows.length > 0)
|
|
1322
|
+
if (withRows.length === 0) {
|
|
1323
|
+
logInfo(
|
|
1324
|
+
'No wireframe carries a States table, so nothing was checked against its evidence folders.',
|
|
1325
|
+
)
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const lines = withRows.flatMap((entry) => [
|
|
1330
|
+
...entry.missingFolders.map(
|
|
1331
|
+
(finding) =>
|
|
1332
|
+
`${entry.rel}:${finding.line} ${finding.state} no folder at ${finding.path}`,
|
|
1333
|
+
),
|
|
1334
|
+
...entry.unlistedFolders.map(
|
|
1335
|
+
(finding) =>
|
|
1336
|
+
`${entry.rel} ${finding.root}/${finding.folder} named in no row`,
|
|
1337
|
+
),
|
|
1338
|
+
...(entry.sketchWithEvidence
|
|
1339
|
+
? [
|
|
1340
|
+
`${entry.rel}:${entry.sketchLine} a plaintext sketch sits beside evidence that already exists`,
|
|
1341
|
+
]
|
|
1342
|
+
: []),
|
|
1343
|
+
])
|
|
1344
|
+
|
|
1345
|
+
if (lines.length === 0) {
|
|
1346
|
+
logInfo(
|
|
1347
|
+
`${plural(withRows.length, 'wireframe')} checked, every state matched one-to-one with its evidence folder.`,
|
|
1348
|
+
)
|
|
1349
|
+
return
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
logWarn(plural(lines.length, 'finding'))
|
|
1353
|
+
pipeOutput(lines.join('\n'))
|
|
1354
|
+
}
|
|
@@ -57,11 +57,25 @@ export interface DecisionReport {
|
|
|
57
57
|
* claim some check happens to cover without the entry saying so.
|
|
58
58
|
*/
|
|
59
59
|
readonly checks: readonly string[]
|
|
60
|
+
/**
|
|
61
|
+
* The entry's own word count, read alongside the weight judgment the
|
|
62
|
+
* standard asks a session to make by reading the file rather than counting
|
|
63
|
+
* it. Never gates, per `standards/architecture.md`'s `## Length` section.
|
|
64
|
+
*/
|
|
65
|
+
readonly words: number
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
export interface ArchitectureReport {
|
|
63
69
|
readonly rel: string
|
|
64
70
|
readonly lines: number
|
|
71
|
+
/** The whole record's word count, reported for the same reason. */
|
|
72
|
+
readonly words: number
|
|
73
|
+
/**
|
|
74
|
+
* Word count of the `## Risks / open questions` section, absent when the
|
|
75
|
+
* record carries no such heading. The standard asks that section to hold
|
|
76
|
+
* only what is still open, so its weight is read the same way the file's is.
|
|
77
|
+
*/
|
|
78
|
+
readonly risksWords?: number
|
|
65
79
|
/** What the record declared, absent when it states no length rule. */
|
|
66
80
|
readonly allowances?: Allowances
|
|
67
81
|
/** The frame plus the per-decision allowance, absent alongside it. */
|
|
@@ -71,6 +85,8 @@ export interface ArchitectureReport {
|
|
|
71
85
|
|
|
72
86
|
const DECISION_HEADING = /^###\s+(.+?)\s*$/
|
|
73
87
|
const SECTION_HEADING = /^##\s+\S/
|
|
88
|
+
/** The one H2 the standard's `## Length` section asks to be weighed by words. */
|
|
89
|
+
const RISKS_HEADING = /^##\s+Risks\s*\/\s*open questions\s*$/i
|
|
74
90
|
const CODE_SPAN = /`[^`]*`/g
|
|
75
91
|
/** Dropped ahead of the figure scan, since an anchor date is not a claim. */
|
|
76
92
|
const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
|
|
@@ -238,6 +254,54 @@ export function splitDecisions(source: string): RawDecision[] {
|
|
|
238
254
|
return decisions
|
|
239
255
|
}
|
|
240
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Counts whitespace-delimited tokens, which is the unit the standard's
|
|
259
|
+
* `## Length` section reads alongside the weight judgment a session makes by
|
|
260
|
+
* reading the file. It is a report figure rather than a gate, so a fenced
|
|
261
|
+
* example or a code span inflating the count costs nothing a reader corrects
|
|
262
|
+
* for by reading, the same trade the line count above it already takes.
|
|
263
|
+
*/
|
|
264
|
+
function wordCount(text: string): number {
|
|
265
|
+
return text.match(/\S+/g)?.length ?? 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extracts the `## Risks / open questions` section, or nothing when the
|
|
270
|
+
* record carries no such heading.
|
|
271
|
+
*
|
|
272
|
+
* The section is read the same way `splitDecisions` reads a decision: capture
|
|
273
|
+
* starts at the heading and stops at the next H2, or at the end of the file
|
|
274
|
+
* when the section is last, which is where the standard's template puts it.
|
|
275
|
+
*/
|
|
276
|
+
export function risksSection(source: string): string | undefined {
|
|
277
|
+
const lines = bodyLines(source)
|
|
278
|
+
const body: string[] = []
|
|
279
|
+
let capturing = false
|
|
280
|
+
let found = false
|
|
281
|
+
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
if (line.fenced) {
|
|
284
|
+
if (capturing) body.push(line.text)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (RISKS_HEADING.test(line.text)) {
|
|
289
|
+
capturing = true
|
|
290
|
+
found = true
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (capturing && SECTION_HEADING.test(line.text)) {
|
|
295
|
+
capturing = false
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (capturing) body.push(line.text)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return found ? body.join('\n') : undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
241
305
|
/** Cardinals a record spells rather than writes, which the corpus does for both. */
|
|
242
306
|
const SPELLED: Record<string, number> = {
|
|
243
307
|
one: 1,
|
|
@@ -332,13 +396,22 @@ export async function measureArchitecture(
|
|
|
332
396
|
figures,
|
|
333
397
|
...(quantified !== undefined && { quantified }),
|
|
334
398
|
checks: await namedChecks(root, entry.body),
|
|
399
|
+
words: wordCount(entry.body),
|
|
335
400
|
}
|
|
336
401
|
}),
|
|
337
402
|
)
|
|
338
403
|
|
|
404
|
+
const risks = risksSection(source)
|
|
405
|
+
|
|
339
406
|
return {
|
|
340
407
|
rel,
|
|
341
408
|
lines: source.replace(/\n$/, '').split('\n').length,
|
|
409
|
+
words: wordCount(
|
|
410
|
+
bodyLines(source)
|
|
411
|
+
.map((line) => line.text)
|
|
412
|
+
.join('\n'),
|
|
413
|
+
),
|
|
414
|
+
...(risks !== undefined && { risksWords: wordCount(risks) }),
|
|
342
415
|
...(allowances !== undefined && {
|
|
343
416
|
allowances,
|
|
344
417
|
ceiling: ceilingFor(allowances, raw.length),
|