@erclx/canon 5.43.0 → 5.45.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.
Files changed (245) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/backlog-triage/references/apply.md +6 -6
  4. package/claude/skills/canon-cli/SKILL.md +4 -5
  5. package/claude/skills/canvas-board/REQUIREMENT.md +1 -1
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/context-fold/references/context.md +1 -1
  8. package/claude/skills/create-skill/REQUIREMENT.md +1 -1
  9. package/claude/skills/create-skill/SKILL.md +2 -2
  10. package/claude/skills/create-skill/references/skill-requirement.md +68 -9
  11. package/claude/skills/create-skill/references/skill.md +1 -1
  12. package/claude/skills/design-identity/REQUIREMENT.md +40 -0
  13. package/claude/skills/design-identity/SKILL.md +138 -0
  14. package/claude/skills/{draft-identity → design-identity}/references/card-route.md +2 -2
  15. package/claude/skills/design-taste/REQUIREMENT.md +6 -6
  16. package/claude/skills/design-taste/SKILL.md +2 -2
  17. package/claude/skills/design-taste/references/kinds.md +1 -1
  18. package/claude/skills/design-taste/references/preflight.md +1 -1
  19. package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -69
  20. package/claude/skills/draft-and-pick/SKILL.md +3 -153
  21. package/claude/skills/draft-doc/REQUIREMENT.md +9 -30
  22. package/claude/skills/draft-doc/SKILL.md +4 -76
  23. package/claude/skills/draft-figure/REQUIREMENT.md +8 -42
  24. package/claude/skills/draft-figure/SKILL.md +3 -77
  25. package/claude/skills/draft-identity/REQUIREMENT.md +8 -23
  26. package/claude/skills/draft-identity/SKILL.md +3 -130
  27. package/claude/skills/draft-ready/REQUIREMENT.md +8 -37
  28. package/claude/skills/draft-ready/SKILL.md +3 -128
  29. package/claude/skills/draft-screencast/REQUIREMENT.md +8 -23
  30. package/claude/skills/draft-screencast/SKILL.md +3 -62
  31. package/claude/skills/draft-slides/REQUIREMENT.md +8 -27
  32. package/claude/skills/draft-slides/SKILL.md +3 -72
  33. package/claude/skills/draw-figure/REQUIREMENT.md +59 -0
  34. package/claude/skills/draw-figure/SKILL.md +85 -0
  35. package/claude/skills/{draft-figure → draw-figure}/references/architecture-set.md +2 -2
  36. package/claude/skills/{draft-figure → draw-figure}/references/hand-drawn.md +2 -2
  37. package/claude/skills/{draft-figure → draw-figure}/references/mermaid-path.md +2 -2
  38. package/claude/skills/{draft-figure → draw-figure}/references/mermaid.md +1 -1
  39. package/claude/skills/make-deck/REQUIREMENT.md +44 -0
  40. package/claude/skills/make-deck/SKILL.md +80 -0
  41. package/claude/skills/markdown-craft/REQUIREMENT.md +1 -1
  42. package/claude/skills/markdown-craft/SKILL.md +2 -2
  43. package/claude/skills/pick-by-eye/REQUIREMENT.md +36 -0
  44. package/claude/skills/pick-by-eye/SKILL.md +161 -0
  45. package/claude/skills/plan-feature/SKILL.md +2 -1
  46. package/claude/skills/plan-feature/references/plan.md +24 -10
  47. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  48. package/claude/skills/plan-intake/SKILL.md +1 -1
  49. package/claude/skills/plan-ready/REQUIREMENT.md +54 -0
  50. package/claude/skills/plan-ready/SKILL.md +136 -0
  51. package/claude/skills/{draft-ready → plan-ready}/references/assembly.md +1 -1
  52. package/claude/skills/{draft-ready → plan-ready}/references/ready.md +1 -1
  53. package/claude/skills/record-screencast/REQUIREMENT.md +3 -3
  54. package/claude/skills/record-screencast/SKILL.md +2 -2
  55. package/claude/skills/role-orchestrator/REQUIREMENT.md +16 -99
  56. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +5 -2
  57. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  58. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +18 -13
  59. package/claude/skills/role-planner/REQUIREMENT.md +15 -73
  60. package/claude/skills/role-planner/SKILL.md +2 -1
  61. package/claude/skills/role-worker/REQUIREMENT.md +13 -41
  62. package/claude/skills/role-worker/SKILL.md +9 -0
  63. package/claude/skills/script-screencast/REQUIREMENT.md +40 -0
  64. package/claude/skills/script-screencast/SKILL.md +70 -0
  65. package/claude/skills/{draft-screencast → script-screencast}/references/beats-template.md +2 -2
  66. package/claude/skills/search-craft/REQUIREMENT.md +4 -4
  67. package/claude/skills/search-craft/SKILL.md +1 -1
  68. package/claude/skills/target-setup/SKILL.md +3 -2
  69. package/claude/skills/target-setup/references/detect.md +0 -1
  70. package/claude/skills/target-setup/references/verify.md +2 -2
  71. package/claude/skills/task-board/SKILL.md +11 -9
  72. package/claude/skills/task-board/references/board.md +15 -12
  73. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  74. package/claude/skills/teach-workspace/SKILL.md +1 -1
  75. package/claude/skills/teach-workspace/references/lesson-craft.md +2 -2
  76. package/claude/skills/teach-workspace/references/promotion.md +5 -5
  77. package/claude/skills/ui-checklist/SKILL.md +2 -0
  78. package/claude/skills/write-doc/REQUIREMENT.md +46 -0
  79. package/claude/skills/write-doc/SKILL.md +83 -0
  80. package/claude/skills/{draft-doc → write-doc}/references/context.md +2 -2
  81. package/claude/skills/{draft-doc → write-doc}/references/docs.md +2 -2
  82. package/claude/skills/{draft-doc → write-doc}/references/readme.md +2 -2
  83. package/claude/skills/{draft-doc → write-doc}/references/wiki-kind.md +3 -3
  84. package/docs/agents/install-and-sync.md +2 -2
  85. package/docs/agents/markdown-audit.md +4 -2
  86. package/docs/agents/migrate.md +2 -2
  87. package/docs/agents/output-shape.md +1 -2
  88. package/docs/target/migrations.md +35 -4
  89. package/docs/target/projects.md +2 -3
  90. package/docs/target/scenarios.md +4 -1
  91. package/docs/workflow/parallel-features.md +3 -2
  92. package/docs/workflow/skill-map-on-request.md +19 -13
  93. package/docs/workflow/skill-map.md +13 -12
  94. package/docs/workflow/visual-design-workflow.md +2 -2
  95. package/governance/detect.toml +4 -0
  96. package/governance/ledger/framework/astro.md +73 -0
  97. package/governance/ledger/framework/fastapi.md +76 -0
  98. package/governance/ledger/framework/nextjs.md +86 -0
  99. package/governance/ledger/framework/react.md +83 -0
  100. package/governance/ledger/framework/shadcn.md +60 -0
  101. package/governance/ledger/framework/tailwind.md +55 -0
  102. package/governance/ledger/lang/go.md +61 -0
  103. package/governance/ledger/lang/php.md +50 -0
  104. package/governance/ledger/lang/python.md +68 -0
  105. package/governance/ledger/lang/typescript.md +64 -0
  106. package/governance/ledger/lib/database.md +65 -0
  107. package/governance/ledger/lib/e2e-reliability.md +57 -0
  108. package/governance/ledger/lib/pydantic.md +61 -0
  109. package/governance/ledger/lib/security-server.md +62 -0
  110. package/governance/ledger/lib/security-web.md +51 -0
  111. package/governance/ledger/lib/tanstack-query.md +57 -0
  112. package/governance/ledger/lib/test-scope.md +43 -0
  113. package/governance/ledger/lib/testing-go.md +57 -0
  114. package/governance/ledger/lib/testing-php.md +51 -0
  115. package/governance/ledger/lib/testing-py.md +67 -0
  116. package/governance/ledger/lib/testing-ts.md +63 -0
  117. package/governance/ledger/lib/zod.md +47 -0
  118. package/governance/ledger/tooling/ci-workflow.md +33 -0
  119. package/governance/ledger/tooling/code-comments.md +48 -0
  120. package/governance/ledger/tooling/config-comments.md +27 -0
  121. package/governance/ledger/tooling/dependencies.md +38 -0
  122. package/governance/ledger/ui/a11y.md +55 -0
  123. package/governance/ledger/ui/copy.md +49 -0
  124. package/governance/ledger/ui/design-reference.md +42 -0
  125. package/governance/ledger/ui/design-taste.md +19 -0
  126. package/governance/ledger/ui/forms.md +71 -0
  127. package/governance/ledger/ui/interface-casing.md +47 -0
  128. package/governance/ledger/ui/keyboard.md +68 -0
  129. package/governance/ledger/ui/link-behavior.md +29 -0
  130. package/governance/ledger/ui/motion.md +33 -0
  131. package/governance/ledger/ui/surface-capture.md +69 -0
  132. package/governance/ledger/ui/ux-completeness.md +79 -0
  133. package/governance/rules/framework/astro.md +12 -14
  134. package/governance/rules/framework/fastapi.md +20 -24
  135. package/governance/rules/framework/nextjs.md +19 -27
  136. package/governance/rules/framework/react.md +22 -33
  137. package/governance/rules/framework/shadcn.md +11 -15
  138. package/governance/rules/framework/tailwind.md +12 -14
  139. package/governance/rules/lang/go.md +7 -16
  140. package/governance/rules/lang/php.md +3 -3
  141. package/governance/rules/lang/python.md +9 -18
  142. package/governance/rules/lang/typescript.md +9 -19
  143. package/governance/rules/lib/database.md +5 -5
  144. package/governance/rules/lib/e2e-reliability.md +4 -6
  145. package/governance/rules/lib/pydantic.md +14 -16
  146. package/governance/rules/lib/security-server.md +4 -4
  147. package/governance/rules/lib/security-web.md +6 -10
  148. package/governance/rules/lib/tanstack-query.md +10 -12
  149. package/governance/rules/lib/test-scope.md +4 -4
  150. package/governance/rules/lib/testing-go.md +1 -2
  151. package/governance/rules/lib/testing-php.md +4 -5
  152. package/governance/rules/lib/testing-py.md +11 -19
  153. package/governance/rules/lib/testing-ts.md +7 -16
  154. package/governance/rules/lib/zod.md +6 -8
  155. package/governance/rules/tooling/code-comments.md +2 -4
  156. package/governance/rules/tooling/config-comments.md +1 -4
  157. package/governance/rules/tooling/dependencies.md +1 -1
  158. package/governance/rules/ui/a11y.md +16 -45
  159. package/governance/rules/ui/copy.md +6 -9
  160. package/governance/rules/ui/forms.md +13 -19
  161. package/governance/rules/ui/interface-casing.md +5 -7
  162. package/governance/rules/ui/keyboard.md +38 -0
  163. package/governance/rules/ui/link-behavior.md +3 -3
  164. package/governance/rules/ui/motion.md +3 -3
  165. package/governance/rules/ui/surface-capture.md +15 -27
  166. package/governance/rules/ui/ux-completeness.md +16 -39
  167. package/governance/rules/writing/markdown.md +1 -1
  168. package/package.json +1 -1
  169. package/scripts/core/install-check.sh +1 -1
  170. package/src/claude/cases/authoring.ts +15 -3
  171. package/src/claude/cases/misc.ts +12 -0
  172. package/src/claude/cases/workflow.ts +19 -1
  173. package/src/claude/skills-audit.ts +113 -1
  174. package/src/cli/help.ts +0 -1
  175. package/src/cli.ts +0 -2
  176. package/src/commands/claude.ts +34 -11
  177. package/src/commands/demo.ts +1 -1
  178. package/src/commands/gov.ts +4 -0
  179. package/src/commands/init.ts +2 -2
  180. package/src/commands/markdown.ts +1 -1
  181. package/src/commands/migrate.ts +4 -0
  182. package/src/commands/pr.ts +30 -6
  183. package/src/commands/tasks.ts +383 -2
  184. package/src/commands/tooling.ts +18 -5
  185. package/src/context/audit.ts +1 -1
  186. package/src/demo/beats.ts +1 -1
  187. package/src/design/components.ts +1 -1
  188. package/src/gov/audit.ts +25 -4
  189. package/src/init/plan.ts +1 -8
  190. package/src/init/steps.ts +0 -8
  191. package/src/inventory/subjects.ts +1 -1
  192. package/src/markdown/scope.ts +49 -35
  193. package/src/migrate/record-layout.ts +1 -1
  194. package/src/migrate/scratch-evidence.ts +12 -4
  195. package/src/migrate/skill-names.ts +68 -21
  196. package/src/pr/frames.ts +14 -3
  197. package/src/records/backup.ts +3 -3
  198. package/src/records/validate.ts +31 -0
  199. package/src/roots/record.ts +17 -8
  200. package/src/tasks/board.ts +854 -0
  201. package/src/tasks/record.ts +20 -1
  202. package/src/tasks/validate.ts +206 -9
  203. package/src/tooling/inject.ts +26 -7
  204. package/src/tooling/package.ts +32 -0
  205. package/src/tooling/read.ts +46 -3
  206. package/src/tooling/scan.ts +8 -2
  207. package/tooling/astro/configs/card-src/pages/og-card.astro +1 -1
  208. package/tooling/astro/reference.md +64 -93
  209. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +1 -1
  210. package/tooling/base/configs/scripts/verify.ts +1 -1
  211. package/tooling/base/reference.md +1 -9
  212. package/tooling/base/seeds/canon/context/{ci.md → development/ci.md} +1 -1
  213. package/tooling/base/seeds/canon/context/development/index.md +11 -0
  214. package/tooling/base/seeds/canon/context/{development.md → development/setup.md} +8 -12
  215. package/tooling/base/seeds/canon/context/index.md +1 -2
  216. package/tooling/claude/manifest.toml +1 -5
  217. package/tooling/claude/reference.md +36 -59
  218. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +65 -0
  219. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -7
  220. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -7
  221. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +3 -10
  222. package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +2 -7
  223. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +2 -4
  224. package/tooling/claude/seeds/.claude/settings.json +4 -0
  225. package/tooling/nextjs/reference.md +0 -6
  226. package/tooling/php/reference.md +0 -4
  227. package/tooling/web/configs/.github/workflows/verify.yml +28 -0
  228. package/tooling/web/configs/.oxlintrc.json +7 -0
  229. package/tooling/web/configs/e2e/capture-types.ts +35 -0
  230. package/tooling/web/configs/e2e/screenshot.ts +23 -72
  231. package/tooling/web/manifest.toml +1 -1
  232. package/tooling/web/reference.md +77 -124
  233. package/tooling/web/seeds/e2e/capture/cases.ts +47 -0
  234. package/src/commands/wiki.ts +0 -100
  235. package/src/wiki/init.ts +0 -85
  236. /package/claude/skills/{draft-figure → draw-figure}/references/figures.md +0 -0
  237. /package/claude/skills/{draft-slides → make-deck}/references/deck.md +0 -0
  238. /package/claude/skills/{draft-and-pick → pick-by-eye}/references/live-arms.md +0 -0
  239. /package/claude/skills/{draft-doc → write-doc}/references/readme/agent-facing.md +0 -0
  240. /package/claude/skills/{draft-doc → write-doc}/references/readme/application.md +0 -0
  241. /package/claude/skills/{draft-doc → write-doc}/references/readme/cli.md +0 -0
  242. /package/claude/skills/{draft-doc → write-doc}/references/readme/library.md +0 -0
  243. /package/claude/skills/{draft-doc → write-doc}/references/readme/plugin.md +0 -0
  244. /package/claude/skills/{draft-doc → write-doc}/references/requirements.md +0 -0
  245. /package/claude/skills/{draft-doc → write-doc}/references/wiki.md +0 -0
@@ -11,21 +11,18 @@ paths:
11
11
 
12
12
  ## Punctuation
13
13
 
14
- - Do not use em dashes (`—`) or semicolons (`;`). Use a comma or period, or restructure the sentence.
15
- - Do not add punctuation to button labels or short action strings.
14
+ - Write no em dash (`—`) or semicolon in rendered copy. Use a comma or a period, or restructure the sentence.
15
+ - Add no trailing punctuation to a button label or a short action string.
16
16
 
17
17
  ## Button copy
18
18
 
19
- - Use imperative verbs for actions: `Save`, `Delete`, `Connect`, `Cancel`.
20
- - Avoid padded phrasing: `Save` over `Save changes`, `Delete` over `Click to delete`.
19
+ - Label a button with the verb of the action it performs, such as `Save` or `Delete`, never with filler such as `Click to delete`.
21
20
 
22
21
  ## Error messages
23
22
 
24
- - State what to do, not what went wrong: `Enter a valid email` over `Email is invalid`.
25
- - Keep messages short and actionable.
26
- - One sentence maximum.
23
+ - Tell the person how to fix the error, such as `Enter an email address` over `Email is invalid`.
24
+ - Keep an error message to one short sentence in plain language, with no technical jargon or error codes.
27
25
 
28
26
  ## Placeholder text
29
27
 
30
- - Show format examples over instructions: `name@example.com` over `Enter your email here`.
31
- - Reserve instructions for labels or helper text below the input.
28
+ - Never use placeholder text as a label, a hint, or an example. Put a format example such as `name@example.com` in hint text under the label.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Form validation timing, save-blocking, and derived state for rendered forms
2
+ description: Form validation timing, submission, error placement, and derived state for rendered forms
3
3
  paths:
4
4
  - '**/*.tsx'
5
5
  - '**/*.jsx'
@@ -11,35 +11,29 @@ paths:
11
11
 
12
12
  ## Validation timing
13
13
 
14
- - Validate on blur, not on every keystroke. Exception: realtime feedback that is explicitly part of the feature (e.g. character count, live search).
15
- - After a field shows its first error (on blur), re-validate on every change so the error clears as soon as the input becomes valid.
16
- - Apply the same validation timing to all fields in a form. Do not mix blur-only and change-only validation within one form.
17
- - Do not show an error on a field the user has never touched.
14
+ - Validate when the person submits or continues, never when focus leaves a field. Realtime feedback the feature exists for, such as a character count, is the exception.
15
+ - Re-validate a field on every change once it shows an error, so the error clears as soon as the input is valid.
18
16
 
19
- ## Save blocking
17
+ ## Submission
20
18
 
21
- - Block save when any required field is empty, invalid, or has never been touched.
22
- - Treat whitespace-only values as empty for required fields.
19
+ - Refuse a submit while a required field is empty, whitespace-only, or invalid, and show each error. Keep the submit button enabled rather than disabling it.
23
20
 
24
21
  ## Error placement
25
22
 
26
- - Show the error directly under the field that caused it, not under an unrelated field.
27
- - When a change to field A causes a conflict involving field B, show the error under field A (the field the user edited), not under field B.
28
-
29
- ## Submission errors
30
-
31
- - When a failed submission carries several errors, move focus to a summary that links to each failing field. Keep the inline error beside each field.
23
+ - Show each error next to the field that caused it, never under an unrelated field.
24
+ - Show a cross-field conflict under the field the person edited, not under the field it conflicts with.
25
+ - Move focus to an error summary linking to each failing field when a submission fails, and keep the inline error beside each field.
32
26
 
33
27
  ## Authentication and re-entry
34
28
 
35
- - Let a password manager fill sign-in fields and accept a pasted value. Do not block paste or autofill on a password or code field (WCAG 2.2 Accessible Authentication, 3.3.8).
36
- - Do not ask for information the user already gave earlier in the same flow. Prefill it or offer it for selection (WCAG 2.2 Redundant Entry, 3.3.7).
29
+ - Let a password manager fill sign-in fields and accept a pasted value. Never block paste or autofill on a password or code field (WCAG 3.3.8).
30
+ - Never ask again for information the person gave earlier in the same flow. Prefill it or offer it for selection (WCAG 3.3.7).
37
31
 
38
32
  ## Input sanitization
39
33
 
40
- - Trim leading and trailing whitespace from text inputs on blur or submit, not on every keystroke.
41
- - If a field is case-sensitive, surface that constraint near the field. Do not silently treat `Foo` and `foo` as different values without warning.
34
+ - Trim leading and trailing whitespace from a text input on submit, never on every keystroke.
35
+ - Say near a case-sensitive field that it is case-sensitive, rather than silently treating `Foo` and `foo` as different values.
42
36
 
43
37
  ## Derived display state
44
38
 
45
- - Reset status indicators, counts, and derived display values when their source input changes.
39
+ - Reset a status indicator, count, or derived value to neutral when its source input changes.
@@ -13,19 +13,17 @@ paths:
13
13
 
14
14
  ## Sentence case
15
15
 
16
- - Use sentence case for all rendered interface text: headings, labels, nav items, tab titles, and dialog titles.
17
- - Retain original casing for proper nouns and product names: `GitHub`, `macOS`, `TypeScript`.
18
- - Treat title case on every heading as a template tell, since it reads as a default rather than a choice.
16
+ - Use sentence case for rendered headings, labels, nav items, tab titles, and dialog titles, since title case on every heading reads as a template default.
17
+ - Keep the original casing of a proper noun or product name, such as `GitHub`, `macOS`, or `TypeScript`.
19
18
 
20
19
  ## Uppercase labels
21
20
 
22
- - Keep small uppercase labels above headings to one per three sections, with the first screen counting as one. More than `ceil(sections / 3)` is too many, and the fix is usually to delete the label, since a section's position already says what it is.
23
- - Treat the count as a judgment, since a glob cannot check it.
21
+ - Keep small uppercase labels above headings to `ceil(sections / 3)`, the first screen counting as one. Delete the extra labels, since position already names a section.
24
22
 
25
23
  ## Where it applies
26
24
 
27
- - The rule covers markup, MDX, and content modules under a `content/` folder. Copy kept elsewhere, such as a locale file, is not reached, so apply the same casing there without the rule firing.
25
+ - Apply the same casing to interface copy kept outside the globs, such as a locale file, which this rule never fires on.
28
26
 
29
27
  ## Overriding
30
28
 
31
- - Depart from sentence case where the case needs it, such as an acronym, a brand mark, or a set-in-caps identity the design chose on purpose.
29
+ - Depart from sentence case only where the case needs it, such as an acronym, a brand mark, or a set-in-caps identity the design chose on purpose.
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Focus styles, keyboard operation, focus management, and pointer targets for rendered UI
3
+ paths:
4
+ - '**/*.tsx'
5
+ - '**/*.jsx'
6
+ - '**/*.astro'
7
+ - '**/*.html'
8
+ - '**/*.css'
9
+ ---
10
+
11
+ # Keyboard and focus standards
12
+
13
+ ## Focus styles
14
+
15
+ - Style focus through the project's one focus ring rather than a per-component focus style.
16
+ - Never set `outline: none` or `outline-none` without a replacement focus indicator in the same rule.
17
+ - Give the focus ring 3:1 contrast against its background in every theme the project ships.
18
+
19
+ ## Keyboard operation
20
+
21
+ - Close a dialog, popover, menu, or inline confirmation on Escape, and cancel an in-progress edit the same way.
22
+ - Bind no single printable character as a shortcut unless a person can turn it off or remap it, or it fires only while its control has focus (WCAG 2.1.4).
23
+ - Give every author-defined drag a single-pointer alternative and a keyboard path, such as move buttons or arrow-key reordering (WCAG 2.5.7).
24
+
25
+ ## Composite widgets
26
+
27
+ - Keep one item of a composite widget in the tab sequence, and move between its items with the arrow keys through roving `tabindex` or `aria-activedescendant`.
28
+ - Open a combobox popup on Down Arrow, accept the focused option on Enter, and close the popup on Escape without changing the value.
29
+
30
+ ## Focus management
31
+
32
+ - Move focus into a dialog when it opens, and return it to the element that opened it when it closes.
33
+ - Never let a sticky header, footer, or overlay fully cover the focused control. Scroll it clear of them (WCAG 2.4.11).
34
+
35
+ ## Pointer targets
36
+
37
+ - Size a web pointer target at least 24 by 24 CSS pixels, or space it so a 24 pixel circle centered on it touches no other target (WCAG 2.5.8).
38
+ - Size a native touch target at the platform figure, 44 points on iOS and 48 dp on Android, and never swap the web and native figures.
@@ -11,9 +11,9 @@ paths:
11
11
 
12
12
  ## Default target
13
13
 
14
- - Open a link in the same tab. Reserve `target="_blank"` for a destination that would discard in-progress work if it replaced the current page, such as a document a user is midway through elsewhere.
15
- - Do not add `target="_blank"` as a default for every external link.
14
+ - Open a link in the same tab. Reserve `target="_blank"` for a page the person must consult while finishing a task in the current one.
15
+ - Never add `target="_blank"` as a default for every external link.
16
16
 
17
17
  ## mailto: links
18
18
 
19
- - Open a `mailto:` link in the same tab. It hands off to the mail client rather than replacing page content, so a new tab leaves an empty tab behind.
19
+ - Open a `mailto:` link in the same tab. It hands off to the mail client rather than replacing the page, so a new tab leaves an empty tab behind.
@@ -12,12 +12,12 @@ paths:
12
12
 
13
13
  ## Moving content
14
14
 
15
- - Give any content that moves, blinks, or scrolls automatically for more than five seconds a control to pause, stop, or hide it (WCAG Pause, Stop, Hide, 2.2.2).
15
+ - Give any content that moves, blinks, or scrolls automatically for more than five seconds a control to pause, stop, or hide it (WCAG 2.2.2).
16
16
  - Halt an auto-advancing carousel or ticker while it holds keyboard focus or pointer hover.
17
- - Do not flash content more than three times in any one second.
17
+ - Never flash content more than three times in any one second (WCAG 2.3.1).
18
18
 
19
19
  ## Reduced motion
20
20
 
21
21
  - Honor `prefers-reduced-motion: reduce` for every animation and transition that conveys no state. Remove it or replace it with an instant change.
22
22
  - Keep essential state changes visible under reduced motion, using a fade or a static indicator instead of movement.
23
- - Do not start video or animated backgrounds on their own when the preference is set.
23
+ - Start no video or animated background on its own when the preference is set.
@@ -12,39 +12,27 @@ paths:
12
12
 
13
13
  ## What a surface is
14
14
 
15
- - A surface is anything the project renders for a person to look at. A framework route is one. A page the project generates is another, whoever generates it.
16
- - Judge a generated page by the same rule as a route. Reading its markup reports nothing about how it composes.
17
- - A component the production build strips out is not a surface, such as a dev-only scenario switcher gated behind a build flag. The build removes it before anything renders, so no comparison exists to capture.
15
+ - Treat anything the project renders for a person as a surface, a generated page as much as a route.
16
+ - Skip a component the production build strips out, such as a dev-only switcher.
18
17
 
19
18
  ## When to capture
20
19
 
21
- - Run the project's capture command after changing what a surface renders. Report it rather than proceeding silently when the project has none.
22
- - Capture against a running preview server. Do not capture against a dev server.
23
- - Capture every theme the surface ships. Do not capture the default theme alone.
24
- - Capture again after fixing a defect a capture found. A repair inside a shared stylesheet can cancel a rule written earlier in the same file.
20
+ - Run the project's capture command against a preview server, never a dev server, after changing a surface. Report it when the project has none.
21
+ - Capture every theme the surface ships, and again after fixing a defect a capture found.
25
22
 
26
23
  ## What a capture covers
27
24
 
28
- - Capture the full page. Do not capture a component in isolation.
29
- - Declare the widths a layout case captures. Always include 320, the reflow floor from the reflow criterion in `a11y`, then add each breakpoint bucket the project writes rules for, read from the `## Layout` section of `DESIGN.md`.
30
- - Fall back to the 320 floor alone when the project has no `DESIGN.md` or its record carries no `## Layout` section. Do not impose another project's breakpoints.
31
- - Add a layout case at each bound when a surface renders persistent chrome a person can resize, such as a draggable sidebar or a split pane, one with the chrome at its narrowest and one at its widest. Take both at the narrowest declared width where the chrome stays persistent rather than collapsing, since a frame where it has turned into an overlay shows none of the width it takes.
32
- - Read those bounds from the code that clamps the chrome, since `## Layout` has no slot for them. Judge each frame by the content width left beside the chrome rather than by the window width, against the 320 floor `a11y` holds for that region.
33
- - Apply the declaration to a case that covers layout only. A case driving a menu open or an answer chosen tests a state, so it takes one width rather than the full set.
34
- - Read a capture at 320 as proof that a frame was taken there, not that the page reflows correctly. The criterion is stated in `a11y` and nothing here enforces it.
35
- - Write the capture into a folder named `evidence`. The image is collected for the pull request comment when any segment of its path is literally `evidence` and its filename carries an image extension, so the folder name is fixed and its position in the tree is the project's own.
36
- - Several `evidence/` folders in one project is the ordinary shape rather than a problem to consolidate. One per surface is how a project with more than one rendered surface ends up.
37
- - Drive a state a screenshot cannot reach. An initial render reports nothing about a menu that opens, an answer that is chosen, or a rail that tracks scrolling.
38
- - Add a case to the capture record when adding a surface.
39
- - Remove a surface's case in the change that removes the surface.
25
+ - Capture the full page, never a component in isolation.
26
+ - Capture a layout case at 320, the reflow floor in `a11y`, plus each breakpoint in the `## Layout` section of `DESIGN.md`, or at 320 alone when there is none.
27
+ - Capture resizable chrome at its narrowest and widest clamp, at the narrowest width keeping it persistent. Judge the content width beside it.
28
+ - Capture a state case at one width rather than the full set.
29
+ - Drive a state an initial render cannot reach, such as an open menu or a rail tracking scroll.
30
+ - Add a surface's case with the surface, and remove it with the surface.
40
31
 
41
32
  ## Sharing a capture
42
33
 
43
- - Do not commit the sweep. It stays ignored.
44
- - Commit a flagged case's evidence output so the pull request carries the comparison, rather than attaching it by hand.
45
- - A project whose own established convention already commits that same comparison may route a flagged case's evidence there instead of a separate evidence folder. Only an established convention already committing the same comparison qualifies, not a preference for skipping a second folder.
46
- - Commit an evidence case for the first time only after running the capture twice with no code change between the runs and confirming the two outputs are byte-identical.
47
- - Mask the element carrying a value that moves between runs, such as an elapsed-time counter, a clock, or a relative date, in the state's capture case before the double capture, so the check still runs on everything else in the frame. Mask the smallest element holding the value, never the state.
48
- - Do not commit the state as evidence when the masked outputs still differ, which happens when the value's width reflows its neighbors. Name the variance in the pull request instead, and never re-run toward a match.
49
- - Recommitting an unbounded sweep on every run reaches a gigabyte of repository history inside a hundred merges. A small, committed evidence set is what a reviewer needs and what lets GitHub draw its own before-and-after comparison on the pull request.
50
- - `git-pr` and `git-followup` maintain a comparison comment on the pull request automatically once a case is committed, so a reviewer never has to open Files Changed to see it.
34
+ - Write a capture into a folder named `evidence`, which the pull request comment collects from.
35
+ - Commit a flagged case's evidence output and never the sweep, unless the project already commits the same comparison elsewhere.
36
+ - Commit a new evidence case only after two captures with no code change between them match byte for byte.
37
+ - Mask the smallest element holding a value that moves between runs, such as a clock, before the double capture.
38
+ - Name the variance in the pull request when masked outputs still differ, never re-running toward a match.
@@ -10,56 +10,33 @@ paths:
10
10
 
11
11
  # UX completeness standards
12
12
 
13
- ## Mutation feedback
13
+ ## Feedback
14
14
 
15
- - Every user-initiated mutation (save, delete, sync, import, export) must show inline feedback on success.
16
- - Feedback must be transient: auto-dismiss after 2-3 seconds without requiring user action.
17
- - Show inline error feedback when a mutation fails. Do not fail silently.
15
+ - Confirm every user-initiated mutation, such as a save, delete, or import, inline on success, and show an inline error when it fails.
16
+ - Auto-dismiss a success message only when its outcome stays visible elsewhere. Keep a message carrying the only copy of its information until dismissed (WCAG 2.2.1).
17
+ - Show no optimistic status, such as a green dot or "connected", while unsaved edits are pending.
18
18
 
19
19
  ## Empty states
20
20
 
21
- - Every list, table, or collection view must handle the empty state explicitly.
22
- - Empty states must include a next action: a button, link, or instruction that tells the user what to do.
23
- - Distinguish between "nothing exists yet" (onboarding) and "nothing matches your filter" (recovery). Use different copy for each.
24
- - Filter-driven empty states must include a way to clear the filter (link or button).
21
+ - Give every list, table, or collection an explicit empty state carrying a next action, such as a button or link.
22
+ - Write different copy for "nothing exists yet" and "nothing matches your filter", and give the filtered state a way to clear the filter.
25
23
 
26
24
  ## Loading states
27
25
 
28
- - Every async operation visible to the user must show a loading indicator (spinner, skeleton, or disabled state).
29
- - Do not show stale content while new content is loading. Either show a loading state or keep the previous content with a progress indicator.
26
+ - Show a progress indicator for any operation a person waits on for longer than about one second.
27
+ - Mark previous content as updating while new content loads, rather than showing it unmarked as current.
30
28
 
31
29
  ## Destructive actions
32
30
 
33
- - Destructive actions (delete, disconnect, discard) must require confirmation before executing.
34
- - Confirmation UI must be dismissible via Escape in addition to a cancel button.
31
+ - Confirm a destructive action, such as a delete, disconnect, or discard, before it runs, unless it can be undone afterward.
35
32
 
36
- ## Inline confirmations
33
+ ## Element states
37
34
 
38
- - All inline confirmation rows (delete, discard, cancel) must be dismissible via Escape.
39
- - Confirmation labels and destructive buttons must have sufficient contrast in both light and dark modes.
35
+ - Give every interactive element distinct hover, active, focus, and disabled styles.
36
+ - Never flash an intermediate state, such as a light frame before a dark theme or a layout jump before data arrives. Reserve the space or settle it first.
40
37
 
41
- ## Status indicators
38
+ ## Overflow
42
39
 
43
- - Status indicators (dots, badges, connection status) must reset to a neutral state when their source input changes.
44
- - Do not show optimistic status (green dot, "connected") while unsaved edits are pending.
45
-
46
- ## State transitions
47
-
48
- - Do not flash intermediate states during transitions (e.g., white flash on theme load, layout jump before data arrives).
49
- - Use CSS transitions or deferred rendering to prevent visible flicker between states.
50
-
51
- ## Interactive element states
52
-
53
- - All interactive elements must visually respond to hover, active, focus, and disabled states.
54
- - Disabled elements must use reduced opacity or muted colors and `cursor: not-allowed`.
55
- - Do not rely on a single state (e.g., focus only) to communicate interactivity.
56
-
57
- ## Overflow and truncation
58
-
59
- - Long text in constrained containers must truncate with ellipsis, not overflow or wrap unexpectedly.
60
- - Truncated text must be accessible via `title` attribute or expandable interaction.
61
-
62
- ## Scroll and clipping
63
-
64
- - Focus rings and interactive borders must not be clipped by parent `overflow: hidden` or `overflow: auto` containers.
65
- - Add padding or adjust overflow to prevent clipping on edge items in scrollable lists.
40
+ - Truncate long text in a constrained container with an ellipsis rather than letting it overflow.
41
+ - Expose truncated text through an expand control or full text on focus, never through a `title` tooltip alone.
42
+ - Keep focus rings and interactive borders clear of a parent's `overflow` clipping, padding the edge items of a scrollable list.
@@ -21,4 +21,4 @@ paths:
21
21
 
22
22
  ## Figures
23
23
 
24
- - Load the `canon:draft-figure` skill before drafting or revising a Mermaid fence or a figure, and report it rather than proceeding silently when it does not resolve.
24
+ - Load the `canon:draw-figure` skill before drafting or revising a Mermaid fence or a figure, and report it rather than proceeding silently when it does not resolve.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.43.0",
4
+ "version": "5.45.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -100,7 +100,7 @@ log_step "Assert scaffold"
100
100
  # Authoring references name no path because they ship inside the plugin's
101
101
  # skills rather than installing into a target. The two
102
102
  # rule paths stand for the `claude` and `canon` groups `base` takes whole.
103
- for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "canon/context/index.md" \
103
+ for path in "CLAUDE.md" ".claude" "canon/context/index.md" \
104
104
  ".oxfmtrc.json" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.ts" \
105
105
  ".claude/rules/canon/claude/output.md" ".claude/rules/canon/canon/scratch.md"; do
106
106
  if [ ! -e "$TARGET_DIR/$path" ]; then
@@ -22,26 +22,38 @@ export const AUTHORING_CASES: readonly SkillCase[] = [
22
22
  {
23
23
  prompt:
24
24
  'Write a brand-new docs page for the capture command, nothing under docs/ covers it yet.',
25
+ expect: 'write-doc',
26
+ },
27
+ {
28
+ prompt:
29
+ 'I typed draft-doc and it said the doc skill was renamed to write-doc.', // canon-keep-retired
30
+ // The pointer answers under its old name on purpose. canon-keep-retired
25
31
  expect: 'draft-doc',
26
32
  },
27
33
  {
28
34
  prompt:
29
35
  'I already wrote the finished skill files, package them as a ready folder with a plan and a task for a worker to copy.',
36
+ expect: 'plan-ready',
37
+ },
38
+ {
39
+ prompt:
40
+ 'I typed draft-ready and it said the ready skill was renamed to plan-ready.', // canon-keep-retired
41
+ // The pointer answers under its old name on purpose. canon-keep-retired
30
42
  expect: 'draft-ready',
31
43
  },
32
44
  {
33
45
  prompt: 'This project has no README.md at all, write one from scratch.',
34
- expect: 'draft-doc',
46
+ expect: 'write-doc',
35
47
  },
36
48
  {
37
49
  prompt:
38
50
  'Write a context entry for the payments domain, there is no canon/context page for it yet.',
39
- expect: 'draft-doc',
51
+ expect: 'write-doc',
40
52
  },
41
53
  {
42
54
  prompt:
43
55
  'Write a wiki reference page for Claude Code output styles, no page covers that subject yet.',
44
- expect: 'draft-doc',
56
+ expect: 'write-doc',
45
57
  },
46
58
  {
47
59
  prompt: 'This passage reads flat and robotic, give it some real cadence.',
@@ -10,6 +10,12 @@ export const MISC_CASES: readonly SkillCase[] = [
10
10
  {
11
11
  prompt:
12
12
  'Show me a few different treatments for this callout so I can pick one by looking.',
13
+ expect: 'pick-by-eye',
14
+ },
15
+ {
16
+ prompt:
17
+ 'I typed draft-and-pick and it said the pick skill was renamed to pick-by-eye.', // canon-keep-retired
18
+ // The pointer answers under its old name on purpose. canon-keep-retired
13
19
  expect: 'draft-and-pick',
14
20
  },
15
21
  {
@@ -20,6 +26,12 @@ export const MISC_CASES: readonly SkillCase[] = [
20
26
  {
21
27
  prompt:
22
28
  'This project has no logo yet. Draft one and give me a social card to go with it.',
29
+ expect: 'design-identity',
30
+ },
31
+ {
32
+ prompt:
33
+ 'I typed draft-identity and it said the identity skill was renamed to design-identity.', // canon-keep-retired
34
+ // The pointer answers under its old name on purpose. canon-keep-retired
23
35
  expect: 'draft-identity',
24
36
  },
25
37
  {
@@ -22,12 +22,18 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
22
22
  },
23
23
  {
24
24
  prompt: 'Draw me a diagram of how the pieces of this system connect.',
25
+ expect: 'draw-figure',
26
+ },
27
+ {
28
+ prompt:
29
+ 'I typed draft-figure and it said the figure skill was renamed to draw-figure.', // canon-keep-retired
30
+ // The pointer answers under its old name on purpose. canon-keep-retired
25
31
  expect: 'draft-figure',
26
32
  },
27
33
  {
28
34
  prompt:
29
35
  'This onboarding doc needs a hand-drawn picture of the signup-to-active path, sketch it in and make sure it actually renders correctly.',
30
- expect: 'draft-figure',
36
+ expect: 'draw-figure',
31
37
  },
32
38
  {
33
39
  prompt:
@@ -108,10 +114,22 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
108
114
  },
109
115
  {
110
116
  prompt: 'Draft me a script with beats for a screencast recording.',
117
+ expect: 'script-screencast',
118
+ },
119
+ {
120
+ prompt:
121
+ 'I typed draft-screencast and it said the screencast skill was renamed to script-screencast.', // canon-keep-retired
122
+ // The pointer answers under its old name on purpose. canon-keep-retired
111
123
  expect: 'draft-screencast',
112
124
  },
113
125
  {
114
126
  prompt: 'Turn this topic into a slide deck I can render.',
127
+ expect: 'make-deck',
128
+ },
129
+ {
130
+ prompt:
131
+ 'I typed draft-slides and it said the slides skill was renamed to make-deck.', // canon-keep-retired
132
+ // The pointer answers under its old name on purpose. canon-keep-retired
115
133
  expect: 'draft-slides',
116
134
  },
117
135
  // Rot across documents nobody changed is what this reads, rather than a
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { isFamilyKey } from '@/claude/skills-families'
4
4
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
5
- import { bodyLines, maskDisplayed } from '@/markdown/scan'
5
+ import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
6
6
 
7
7
  /** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
8
8
  export const EXIT_MISSING_REQUIREMENT = 2
@@ -13,6 +13,19 @@ export const DESCRIPTION_LIMIT = 1024
13
13
  /** The two headings the requirement template declares, matched at any level. */
14
14
  export const REQUIREMENT_SECTIONS: readonly string[] = ['Gap', 'Must']
15
15
 
16
+ /**
17
+ * The caps `claude/skills/create-skill/references/skill-requirement.md` states.
18
+ * Words count body tokens carrying a letter or digit, so a heading or bullet
19
+ * marker costs nothing and frontmatter is left out.
20
+ */
21
+ export const REQUIREMENT_LIMITS = { bullets: 5, words: 400 } as const
22
+
23
+ /** The section the standard retired into Must not, matched at any level. */
24
+ export const RETIRED_REQUIREMENT_SECTION = 'Guards'
25
+
26
+ /** The section that states today's failures, where history is reported. */
27
+ const GAP_SECTION = 'Gap'
28
+
16
29
  /** The one tree that ships to a target, and the only one the practice list names. */
17
30
  export const SHIPPED_CORPUS = join('claude', 'skills')
18
31
 
@@ -100,6 +113,13 @@ const SECTION = /^##\s+(.+?)\s*$/
100
113
  */
101
114
  const ISO_DATE = /\b\d{4}-\d{2}-\d{2}\b/g
102
115
 
116
+ /** An issue or pull request number, which a heading's `#` never matches. */
117
+ const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
118
+
119
+ const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
120
+
121
+ const WORD = /\S*[\p{L}\p{N}]\S*/gu
122
+
103
123
  export interface SkillFinding {
104
124
  readonly rel: string
105
125
  readonly detail: string
@@ -126,6 +146,7 @@ export interface SkillsAudit {
126
146
  readonly nameMismatch: readonly SkillFinding[]
127
147
  readonly longDescription: readonly SkillFinding[]
128
148
  readonly requirementSections: readonly SkillFinding[]
149
+ readonly requirementLimits: readonly SkillFinding[]
129
150
  readonly datedProvenance: readonly SkillFinding[]
130
151
  readonly practiceShape: readonly SkillFinding[]
131
152
  readonly referenceContents: readonly SkillFinding[]
@@ -142,6 +163,7 @@ interface SkillSource {
142
163
  readonly family: string | undefined
143
164
  /** Undefined when the folder carries no `REQUIREMENT.md` at all. */
144
165
  readonly requirementHeadings: readonly string[] | undefined
166
+ readonly requirement: string | undefined
145
167
  /** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
146
168
  readonly sections: readonly string[]
147
169
  readonly hasLedger: boolean
@@ -198,6 +220,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
198
220
  nameMismatch: sources.flatMap(nameFindings),
199
221
  longDescription: sources.flatMap(lengthFindings),
200
222
  requirementSections: sources.flatMap(sectionFindings),
223
+ requirementLimits: sources.flatMap(limitFindings),
201
224
  datedProvenance: sources.flatMap((source) =>
202
225
  source.bodies.flatMap(dateFindings),
203
226
  ),
@@ -269,6 +292,7 @@ async function readSkill(
269
292
  family: declaredFamily(fields?.fields.metadata),
270
293
  requirementHeadings:
271
294
  requirement === undefined ? undefined : headings(requirement),
295
+ requirement,
272
296
  sections: sectionsOf(body),
273
297
  hasLedger: existsSync(join(skillDir, PRACTICE_LEDGER)),
274
298
  bodies: [
@@ -349,6 +373,94 @@ function sectionFindings(source: SkillSource): SkillFinding[] {
349
373
  ]
350
374
  }
351
375
 
376
+ /**
377
+ * Reports every cap the standard states, each as its own finding so a file
378
+ * breaching two reads as two edits. Silent on an absent requirement for the
379
+ * reason `sectionFindings` is.
380
+ */
381
+ function limitFindings(source: SkillSource): SkillFinding[] {
382
+ const { requirement, requirementHeadings } = source
383
+ if (requirement === undefined || requirementHeadings === undefined) return []
384
+
385
+ const rel = join(source.rel, 'REQUIREMENT.md')
386
+ const lines = bodyLines(requirement)
387
+ const details = [
388
+ ...bulletDetails(lines),
389
+ ...wordDetails(lines),
390
+ ...gapHistoryDetails(lines),
391
+ ...(requirementHeadings.includes(RETIRED_REQUIREMENT_SECTION)
392
+ ? [`## ${RETIRED_REQUIREMENT_SECTION}: retired, fold it into Must not`]
393
+ : []),
394
+ ]
395
+
396
+ return details.map((detail) => ({ rel, detail }))
397
+ }
398
+
399
+ /**
400
+ * Counts top-level bullets per H2, since the template holds one list per
401
+ * section. A nested bullet elaborates its parent rather than adding to it.
402
+ */
403
+ function bulletDetails(lines: readonly BodyLine[]): string[] {
404
+ const counts = new Map<string, number>()
405
+ let section: string | undefined
406
+
407
+ for (const line of lines) {
408
+ if (line.fenced) continue
409
+ const heading = SECTION.exec(line.text)?.[1]
410
+ if (heading !== undefined) {
411
+ section = heading
412
+ continue
413
+ }
414
+ if (section === undefined || !TOP_LEVEL_BULLET.test(line.text)) continue
415
+ counts.set(section, (counts.get(section) ?? 0) + 1)
416
+ }
417
+
418
+ return [...counts]
419
+ .filter(([, count]) => count > REQUIREMENT_LIMITS.bullets)
420
+ .map(
421
+ ([name, count]) =>
422
+ `## ${name}: ${count} bullets, cap ${REQUIREMENT_LIMITS.bullets}`,
423
+ )
424
+ }
425
+
426
+ function wordDetails(lines: readonly BodyLine[]): string[] {
427
+ const words = lines.reduce(
428
+ (total, line) => total + (line.text.match(WORD)?.length ?? 0),
429
+ 0,
430
+ )
431
+ if (words <= REQUIREMENT_LIMITS.words) return []
432
+ return [`${words} words, cap ${REQUIREMENT_LIMITS.words}`]
433
+ }
434
+
435
+ /**
436
+ * Gap states what fails today, so a date or a number naming the incident that
437
+ * earned a line is history git already holds. A code span is quoted data, such
438
+ * as a sample filename, and is masked before matching.
439
+ */
440
+ function gapHistoryDetails(lines: readonly BodyLine[]): string[] {
441
+ const details: string[] = []
442
+ let inGap = false
443
+
444
+ for (const line of lines) {
445
+ if (line.fenced) continue
446
+ const heading = HEADING.exec(line.text)?.[1]
447
+ if (heading !== undefined) {
448
+ inGap = heading === GAP_SECTION
449
+ continue
450
+ }
451
+ if (!inGap) continue
452
+ const masked = maskDisplayed(line.text)
453
+ for (const match of [
454
+ ...masked.matchAll(ISO_DATE),
455
+ ...masked.matchAll(ISSUE_NUMBER),
456
+ ]) {
457
+ details.push(`line ${line.number}: ${match[0]} in ${GAP_SECTION}`)
458
+ }
459
+ }
460
+
461
+ return details
462
+ }
463
+
352
464
  /**
353
465
  * A listed folder the shipped corpus does not hold, such as a renamed skill or
354
466
  * a misspelled append, would otherwise read as a pass. Silent where the