@erclx/canon 5.44.0 → 5.46.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 (253) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/REQUIREMENT.md +15 -31
  4. package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
  5. package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
  6. package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
  7. package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
  8. package/claude/skills/canon-cli/SKILL.md +0 -1
  9. package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
  10. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
  11. package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
  12. package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
  13. package/claude/skills/code-craft/REQUIREMENT.md +14 -28
  14. package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
  15. package/claude/skills/context-fold/REQUIREMENT.md +15 -39
  16. package/claude/skills/context-fold/references/context.md +3 -3
  17. package/claude/skills/create-rule/REQUIREMENT.md +10 -24
  18. package/claude/skills/create-rule/SKILL.md +3 -0
  19. package/claude/skills/create-skill/REQUIREMENT.md +10 -21
  20. package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
  21. package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
  22. package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
  23. package/claude/skills/design-identity/REQUIREMENT.md +14 -20
  24. package/claude/skills/design-taste/REQUIREMENT.md +16 -57
  25. package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
  26. package/claude/skills/document-health/REQUIREMENT.md +15 -29
  27. package/claude/skills/document-health/SKILL.md +2 -2
  28. package/claude/skills/document-repair/REQUIREMENT.md +32 -0
  29. package/claude/skills/document-repair/SKILL.md +76 -0
  30. package/claude/skills/document-repair/references/batches.md +44 -0
  31. package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
  32. package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
  33. package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
  34. package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
  35. package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
  36. package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
  37. package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
  38. package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
  39. package/claude/skills/git-branch/REQUIREMENT.md +12 -26
  40. package/claude/skills/git-commit/REQUIREMENT.md +11 -15
  41. package/claude/skills/git-followup/REQUIREMENT.md +11 -22
  42. package/claude/skills/git-issue/REQUIREMENT.md +9 -15
  43. package/claude/skills/git-pr/REQUIREMENT.md +13 -32
  44. package/claude/skills/git-pr/SKILL.md +10 -2
  45. package/claude/skills/git-pr/references/closing-issues.md +47 -0
  46. package/claude/skills/git-pr/references/pr.md +4 -0
  47. package/claude/skills/git-ship/REQUIREMENT.md +13 -26
  48. package/claude/skills/git-split/REQUIREMENT.md +10 -17
  49. package/claude/skills/git-stage/REQUIREMENT.md +12 -18
  50. package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
  51. package/claude/skills/make-deck/REQUIREMENT.md +13 -21
  52. package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
  53. package/claude/skills/pick-by-eye/REQUIREMENT.md +17 -67
  54. package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
  55. package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
  56. package/claude/skills/plan-groundwork/SKILL.md +5 -2
  57. package/claude/skills/plan-groundwork/references/output.md +47 -1
  58. package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
  59. package/claude/skills/plan-intake/references/intake.md +2 -2
  60. package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
  61. package/claude/skills/plan-intake-answer/SKILL.md +7 -1
  62. package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
  63. package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
  64. package/claude/skills/read-frames/REQUIREMENT.md +3 -8
  65. package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
  66. package/claude/skills/review-address/REQUIREMENT.md +14 -38
  67. package/claude/skills/review-craft/REQUIREMENT.md +14 -32
  68. package/claude/skills/review-pr/REQUIREMENT.md +15 -34
  69. package/claude/skills/review-ui/REQUIREMENT.md +14 -39
  70. package/claude/skills/role-orchestrator/REQUIREMENT.md +16 -99
  71. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
  72. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
  73. package/claude/skills/role-planner/REQUIREMENT.md +15 -73
  74. package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
  75. package/claude/skills/role-reviewer/SKILL.md +5 -1
  76. package/claude/skills/role-worker/REQUIREMENT.md +13 -41
  77. package/claude/skills/role-worker/SKILL.md +2 -0
  78. package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
  79. package/claude/skills/search-craft/REQUIREMENT.md +12 -36
  80. package/claude/skills/search-craft/SKILL.md +1 -1
  81. package/claude/skills/search-craft/references/adopted.md +2 -2
  82. package/claude/skills/session-compact/REQUIREMENT.md +13 -23
  83. package/claude/skills/session-map/REQUIREMENT.md +13 -29
  84. package/claude/skills/session-relay/REQUIREMENT.md +14 -58
  85. package/claude/skills/session-relay/SKILL.md +6 -0
  86. package/claude/skills/session-resume/REQUIREMENT.md +13 -27
  87. package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
  88. package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
  89. package/claude/skills/target-check/REQUIREMENT.md +16 -23
  90. package/claude/skills/target-setup/REQUIREMENT.md +11 -34
  91. package/claude/skills/target-setup/SKILL.md +2 -2
  92. package/claude/skills/target-setup/references/detect.md +0 -1
  93. package/claude/skills/target-setup/references/verify.md +2 -2
  94. package/claude/skills/task-board/REQUIREMENT.md +13 -22
  95. package/claude/skills/task-board/SKILL.md +7 -0
  96. package/claude/skills/task-board/references/tasks.md +1 -1
  97. package/claude/skills/task-complete/REQUIREMENT.md +11 -24
  98. package/claude/skills/task-complete/references/archive.md +1 -0
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
  100. package/claude/skills/teach-workspace/references/promotion.md +5 -5
  101. package/claude/skills/test-craft/REQUIREMENT.md +15 -46
  102. package/claude/skills/test-first/REQUIREMENT.md +14 -20
  103. package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
  104. package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
  105. package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
  106. package/claude/skills/video-craft/REQUIREMENT.md +14 -27
  107. package/claude/skills/write-doc/REQUIREMENT.md +14 -24
  108. package/claude/skills/write-human/REQUIREMENT.md +15 -27
  109. package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
  110. package/docs/agents/context-audit.md +19 -12
  111. package/docs/agents/install-and-sync.md +2 -2
  112. package/docs/agents/markdown-audit.md +15 -10
  113. package/docs/agents/output-shape.md +1 -2
  114. package/docs/target/migrations.md +18 -0
  115. package/docs/target/projects.md +3 -2
  116. package/docs/target/scenarios.md +4 -1
  117. package/docs/workflow/skill-map-on-request.md +1 -1
  118. package/docs/workflow/skill-map.md +6 -5
  119. package/governance/detect.toml +4 -0
  120. package/governance/ledger/lang/go.md +61 -0
  121. package/governance/ledger/lang/php.md +50 -0
  122. package/governance/ledger/lang/python.md +68 -0
  123. package/governance/ledger/lang/typescript.md +64 -0
  124. package/governance/ledger/lib/database.md +65 -0
  125. package/governance/ledger/lib/e2e-reliability.md +57 -0
  126. package/governance/ledger/lib/pydantic.md +61 -0
  127. package/governance/ledger/lib/security-server.md +62 -0
  128. package/governance/ledger/lib/security-web.md +51 -0
  129. package/governance/ledger/lib/tanstack-query.md +57 -0
  130. package/governance/ledger/lib/test-scope.md +43 -0
  131. package/governance/ledger/lib/testing-go.md +57 -0
  132. package/governance/ledger/lib/testing-php.md +51 -0
  133. package/governance/ledger/lib/testing-py.md +67 -0
  134. package/governance/ledger/lib/testing-ts.md +63 -0
  135. package/governance/ledger/lib/zod.md +47 -0
  136. package/governance/ledger/tooling/ci-workflow.md +33 -0
  137. package/governance/ledger/tooling/code-comments.md +48 -0
  138. package/governance/ledger/tooling/config-comments.md +27 -0
  139. package/governance/ledger/tooling/dependencies.md +38 -0
  140. package/governance/ledger/ui/a11y.md +55 -0
  141. package/governance/ledger/ui/copy.md +49 -0
  142. package/governance/ledger/ui/design-reference.md +42 -0
  143. package/governance/ledger/ui/design-taste.md +19 -0
  144. package/governance/ledger/ui/forms.md +71 -0
  145. package/governance/ledger/ui/interface-casing.md +47 -0
  146. package/governance/ledger/ui/keyboard.md +68 -0
  147. package/governance/ledger/ui/link-behavior.md +29 -0
  148. package/governance/ledger/ui/motion.md +33 -0
  149. package/governance/ledger/ui/surface-capture.md +69 -0
  150. package/governance/ledger/ui/ux-completeness.md +79 -0
  151. package/governance/renames.toml +0 -2
  152. package/governance/rules/claude/output.md +5 -1
  153. package/governance/rules/code/routing.md +5 -1
  154. package/governance/rules/lang/go.md +7 -16
  155. package/governance/rules/lang/php.md +3 -3
  156. package/governance/rules/lang/python.md +9 -18
  157. package/governance/rules/lang/typescript.md +9 -19
  158. package/governance/rules/lib/database.md +5 -5
  159. package/governance/rules/lib/e2e-reliability.md +4 -6
  160. package/governance/rules/lib/pydantic.md +14 -16
  161. package/governance/rules/lib/security-server.md +4 -4
  162. package/governance/rules/lib/security-web.md +6 -10
  163. package/governance/rules/lib/tanstack-query.md +10 -12
  164. package/governance/rules/lib/test-scope.md +4 -4
  165. package/governance/rules/lib/testing-go.md +1 -2
  166. package/governance/rules/lib/testing-php.md +4 -5
  167. package/governance/rules/lib/testing-py.md +11 -19
  168. package/governance/rules/lib/testing-ts.md +7 -16
  169. package/governance/rules/lib/zod.md +6 -8
  170. package/governance/rules/tooling/code-comments.md +2 -4
  171. package/governance/rules/tooling/config-comments.md +1 -4
  172. package/governance/rules/tooling/dependencies.md +1 -1
  173. package/governance/rules/ui/a11y.md +16 -45
  174. package/governance/rules/ui/copy.md +6 -9
  175. package/governance/rules/ui/forms.md +13 -19
  176. package/governance/rules/ui/interface-casing.md +5 -7
  177. package/governance/rules/ui/keyboard.md +38 -0
  178. package/governance/rules/ui/link-behavior.md +3 -3
  179. package/governance/rules/ui/motion.md +3 -3
  180. package/governance/rules/ui/surface-capture.md +15 -27
  181. package/governance/rules/ui/ux-completeness.md +16 -39
  182. package/governance/stacks/base.toml +0 -1
  183. package/package.json +1 -1
  184. package/scripts/core/install-check.sh +1 -1
  185. package/src/claude/cases/workflow.ts +12 -0
  186. package/src/cli/help.ts +0 -1
  187. package/src/cli.ts +0 -2
  188. package/src/commands/context.ts +57 -3
  189. package/src/commands/gov.ts +4 -0
  190. package/src/commands/init.ts +2 -2
  191. package/src/commands/labels.ts +83 -3
  192. package/src/commands/markdown.ts +188 -21
  193. package/src/commands/sessions.ts +36 -3
  194. package/src/commands/tasks.ts +180 -0
  195. package/src/commands/tooling.ts +18 -5
  196. package/src/commands/worktrees.ts +2 -0
  197. package/src/context/coverage.ts +210 -0
  198. package/src/context/gate.ts +12 -3
  199. package/src/design/components.ts +1 -1
  200. package/src/gate/stages.ts +1 -1
  201. package/src/gov/audit.ts +22 -2
  202. package/src/init/plan.ts +1 -8
  203. package/src/init/steps.ts +44 -7
  204. package/src/intake/items.ts +14 -0
  205. package/src/inventory/subjects.ts +1 -1
  206. package/src/labels/closing.ts +46 -0
  207. package/src/markdown/gated.ts +250 -0
  208. package/src/markdown/scope.ts +119 -2
  209. package/src/migrate/rename.ts +6 -0
  210. package/src/migrate/scratch-evidence.ts +12 -4
  211. package/src/records/backup.ts +3 -3
  212. package/src/records/validate.ts +3 -3
  213. package/src/roots/record.ts +17 -8
  214. package/src/sessions/claim.ts +7 -0
  215. package/src/sessions/live.ts +53 -6
  216. package/src/sessions/registry.ts +11 -0
  217. package/src/sessions/resolve.ts +32 -5
  218. package/src/tasks/closes.ts +136 -0
  219. package/src/tasks/record.ts +1 -1
  220. package/src/tooling/inject.ts +26 -7
  221. package/src/tooling/package.ts +32 -0
  222. package/src/tooling/read.ts +46 -3
  223. package/src/tooling/scan.ts +8 -2
  224. package/src/worktrees/reclaim.ts +16 -1
  225. package/tooling/astro/reference.md +64 -93
  226. package/tooling/base/configs/.github/workflows/phase-label-gate.yml +1 -1
  227. package/tooling/base/configs/scripts/verify.ts +2 -2
  228. package/tooling/base/reference.md +1 -9
  229. package/tooling/base/seeds/canon/context/{ci.md → development/ci.md} +1 -1
  230. package/tooling/base/seeds/canon/context/development/index.md +11 -0
  231. package/tooling/base/seeds/canon/context/{development.md → development/setup.md} +8 -12
  232. package/tooling/base/seeds/canon/context/index.md +1 -2
  233. package/tooling/claude/manifest.toml +1 -5
  234. package/tooling/claude/reference.md +36 -59
  235. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
  236. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +2 -7
  237. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +2 -7
  238. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +3 -10
  239. package/tooling/claude/seeds/.claude/hooks/search-reminder.sh +2 -7
  240. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +2 -4
  241. package/tooling/claude/seeds/CLAUDE.md +2 -2
  242. package/tooling/nextjs/reference.md +0 -6
  243. package/tooling/php/reference.md +0 -4
  244. package/tooling/web/configs/.github/workflows/verify.yml +28 -0
  245. package/tooling/web/configs/.oxlintrc.json +7 -0
  246. package/tooling/web/configs/e2e/capture-types.ts +35 -0
  247. package/tooling/web/configs/e2e/screenshot.ts +23 -72
  248. package/tooling/web/manifest.toml +1 -1
  249. package/tooling/web/reference.md +77 -124
  250. package/tooling/web/seeds/e2e/capture/cases.ts +47 -0
  251. package/governance/rules/canon/task-scope.md +0 -12
  252. package/src/commands/wiki.ts +0 -100
  253. package/src/wiki/init.ts +0 -85
@@ -0,0 +1,210 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
+ import type { AuditedFolder } from '@/context/folders'
4
+
5
+ /** Where a project declares the code roots its context entries must cover. */
6
+ export const CONTEXT_CONFIG_REL = join('canon', 'config', 'context.toml')
7
+
8
+ /** The root assumed when a project declares none and carries the folder. */
9
+ export const DEFAULT_ROOT = 'src'
10
+
11
+ /**
12
+ * The roots a project asked the coverage check to measure.
13
+ *
14
+ * `none` says the project carries no declaration and no `src/`, which reports
15
+ * and never gates so a seed tree audited at its own root keeps passing.
16
+ * `opted-out` is an explicit `roots = []`. `invalid` is a declaration that
17
+ * exists and cannot be used, which gates rather than falling back to `src`,
18
+ * since a project that wrote one meant it.
19
+ */
20
+ export type RootDeclaration =
21
+ | { readonly kind: 'roots'; readonly patterns: readonly string[] }
22
+ | { readonly kind: 'opted-out' }
23
+ | { readonly kind: 'none' }
24
+ | { readonly kind: 'invalid'; readonly reason: string }
25
+
26
+ export interface DoubleClaim {
27
+ readonly folder: string
28
+ readonly rels: readonly string[]
29
+ }
30
+
31
+ export interface CoverageFinding {
32
+ /** Folders under a declared root that no entry's Layout names. */
33
+ readonly unnamed: readonly string[]
34
+ /** Folders taking a Layout H3 in more than one entry. */
35
+ readonly doubled: readonly DoubleClaim[]
36
+ /** Declared roots or globs that matched no folder on disk. */
37
+ readonly missingRoots: readonly string[]
38
+ }
39
+
40
+ export type CoverageReport =
41
+ | {
42
+ readonly kind: 'measured'
43
+ readonly roots: readonly string[]
44
+ readonly findings: CoverageFinding
45
+ }
46
+ | { readonly kind: 'opted-out' }
47
+ | { readonly kind: 'no-root' }
48
+ | { readonly kind: 'invalid'; readonly reason: string }
49
+
50
+ /** Reads the declaration, falling back to `src` when that folder exists. */
51
+ export function readRoots(root: string): RootDeclaration {
52
+ const path = join(root, CONTEXT_CONFIG_REL)
53
+
54
+ if (existsSync(path)) {
55
+ let parsed: Record<string, unknown>
56
+ try {
57
+ parsed = Bun.TOML.parse(readFileSync(path, 'utf8')) as Record<
58
+ string,
59
+ unknown
60
+ >
61
+ } catch {
62
+ return {
63
+ kind: 'invalid',
64
+ reason: `${CONTEXT_CONFIG_REL} is not valid TOML`,
65
+ }
66
+ }
67
+
68
+ const declared = parsed.roots
69
+ if (
70
+ !Array.isArray(declared) ||
71
+ !declared.every((entry) => typeof entry === 'string' && entry !== '')
72
+ ) {
73
+ return {
74
+ kind: 'invalid',
75
+ reason: `${CONTEXT_CONFIG_REL} needs roots as a list of folder or glob strings`,
76
+ }
77
+ }
78
+
79
+ return declared.length === 0
80
+ ? { kind: 'opted-out' }
81
+ : { kind: 'roots', patterns: declared as string[] }
82
+ }
83
+
84
+ return isDirectory(join(root, DEFAULT_ROOT))
85
+ ? { kind: 'roots', patterns: [DEFAULT_ROOT] }
86
+ : { kind: 'none' }
87
+ }
88
+
89
+ function isDirectory(path: string): boolean {
90
+ try {
91
+ return statSync(path).isDirectory()
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+
97
+ /** Expands each pattern to the folders it matches, relative to `root`. */
98
+ function expandRoots(root: string, patterns: readonly string[]) {
99
+ const roots: string[] = []
100
+ const missing: string[] = []
101
+
102
+ for (const pattern of patterns) {
103
+ const matched = [
104
+ ...new Bun.Glob(pattern).scanSync({ cwd: root, onlyFiles: false }),
105
+ ]
106
+ .map((path) => path.replace(/\/$/, ''))
107
+ .filter((path) => isDirectory(join(root, path)))
108
+ .sort()
109
+
110
+ if (matched.length === 0) missing.push(pattern)
111
+ roots.push(...matched)
112
+ }
113
+
114
+ return { roots: [...new Set(roots)], missing }
115
+ }
116
+
117
+ /** Lists folders directly under a root, skipping hidden ones and `node_modules`. */
118
+ function listFolders(root: string, rel: string): string[] {
119
+ return readdirSync(join(root, rel), { withFileTypes: true })
120
+ .filter(
121
+ (entry) =>
122
+ entry.isDirectory() &&
123
+ !entry.name.startsWith('.') &&
124
+ entry.name !== 'node_modules',
125
+ )
126
+ .map((entry) => `${rel}/${entry.name}`)
127
+ .sort()
128
+ }
129
+
130
+ function layoutSection(source: string): string {
131
+ const lines = source.split('\n')
132
+ const start = lines.findIndex((line) => /^##\s+Layout\s*$/.test(line))
133
+ if (start === -1) return ''
134
+
135
+ const end = lines.findIndex(
136
+ (line, index) => index > start && /^##\s/.test(line),
137
+ )
138
+ return lines.slice(start + 1, end === -1 ? undefined : end).join('\n')
139
+ }
140
+
141
+ function backtickedPaths(text: string): string[] {
142
+ return [...text.matchAll(/`([^`\s]+)`/g)].map((match) =>
143
+ match[1].replace(/\/$/, ''),
144
+ )
145
+ }
146
+
147
+ /**
148
+ * Measures the folders under the declared roots against the Layout sections.
149
+ *
150
+ * A folder is named when any Layout text cites it in backticks, a bullet
151
+ * included, and claimed when it is an H3 title. Only the H3 counts toward
152
+ * uniqueness, since a bullet legitimately cites another domain's folder and a
153
+ * grep cannot tell a citation from a claim.
154
+ */
155
+ export function measureCoverage(
156
+ root: string,
157
+ declaration: RootDeclaration,
158
+ folders: readonly AuditedFolder[],
159
+ ): CoverageReport {
160
+ if (declaration.kind === 'opted-out') return { kind: 'opted-out' }
161
+ if (declaration.kind === 'none') return { kind: 'no-root' }
162
+ if (declaration.kind === 'invalid') {
163
+ return { kind: 'invalid', reason: declaration.reason }
164
+ }
165
+
166
+ const named = new Set<string>()
167
+ const claims = new Map<string, Set<string>>()
168
+
169
+ for (const folder of folders) {
170
+ for (const entry of folder.entries) {
171
+ const rel = relative(root, entry)
172
+ const layout = layoutSection(readFileSync(entry, 'utf8'))
173
+
174
+ for (const path of backtickedPaths(layout)) named.add(path)
175
+
176
+ for (const line of layout.split('\n')) {
177
+ const heading = line.match(/^###\s+`([^`\s]+)`\s*$/)
178
+ if (!heading) continue
179
+ const path = heading[1].replace(/\/$/, '')
180
+ claims.set(path, (claims.get(path) ?? new Set()).add(rel))
181
+ }
182
+ }
183
+ }
184
+
185
+ const { roots, missing } = expandRoots(root, declaration.patterns)
186
+ const onDisk = roots.flatMap((rel) => listFolders(root, rel))
187
+
188
+ return {
189
+ kind: 'measured',
190
+ roots,
191
+ findings: {
192
+ unnamed: onDisk.filter((path) => !named.has(path)),
193
+ doubled: onDisk
194
+ .filter((path) => (claims.get(path)?.size ?? 0) > 1)
195
+ .map((folder) => ({
196
+ folder,
197
+ rels: [...(claims.get(folder) ?? [])].sort(),
198
+ })),
199
+ missingRoots: missing,
200
+ },
201
+ }
202
+ }
203
+
204
+ /** Whether the report holds a finding that should fail a widened gate. */
205
+ export function hasCoverageFindings(report: CoverageReport): boolean {
206
+ if (report.kind === 'invalid') return true
207
+ if (report.kind !== 'measured') return false
208
+ const { unnamed, doubled, missingRoots } = report.findings
209
+ return unnamed.length > 0 || doubled.length > 0 || missingRoots.length > 0
210
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  SectionFinding,
4
4
  ShapeFinding,
5
5
  } from '@/context/audit'
6
+ import { type CoverageReport, hasCoverageFindings } from '@/context/coverage'
6
7
  import type { FolderDrift } from '@/context/index-drift'
7
8
 
8
9
  export interface GateInput {
@@ -25,6 +26,12 @@ export interface GateInput {
25
26
  */
26
27
  readonly shape: readonly ShapeFinding[]
27
28
  readonly drift: readonly FolderDrift[]
29
+ /**
30
+ * Folders under the declared code roots that no Layout names or two entries
31
+ * claim, gating under `--gate` alone. A project with no code root reads as
32
+ * not measured and never gates.
33
+ */
34
+ readonly coverage: CoverageReport
28
35
  /**
29
36
  * Whether the caller asked for the widened gate. False leaves a missing
30
37
  * section and a drifted index advisory, which is what the project-root stage
@@ -48,8 +55,8 @@ export function hasDrift(drift: readonly FolderDrift[]): boolean {
48
55
  * for itself, which makes it a fact rather than a threshold a reader weighs.
49
56
  * The findings `--gate` adds are the ones answerable from the file itself: a
50
57
  * required section it does not declare, an H2 outside the closed set, a shape
51
- * break in a folder rebuilt to the shape, and an index disagreeing with its
52
- * folder. Entry length, depth, bullet, table, provenance, and the record's
58
+ * break in a folder rebuilt to the shape, an index disagreeing with its
59
+ * folder, and a code folder no entry names or two entries claim. Entry length, depth, bullet, table, provenance, and the record's
53
60
  * claim coverage are judgments, so they stay out under both modes.
54
61
  */
55
62
  export function isGating({
@@ -59,6 +66,7 @@ export function isGating({
59
66
  extraSections,
60
67
  shape,
61
68
  drift,
69
+ coverage,
62
70
  widened,
63
71
  }: GateInput): boolean {
64
72
  if (unresolvedCitations > 0) return true
@@ -69,6 +77,7 @@ export function isGating({
69
77
  sections.length > 0 ||
70
78
  extraSections.length > 0 ||
71
79
  shape.some((finding) => finding.gated) ||
72
- hasDrift(drift)
80
+ hasDrift(drift) ||
81
+ hasCoverageFindings(coverage)
73
82
  )
74
83
  }
@@ -520,7 +520,7 @@ html.sb-shut .pane { border-left: 0; }
520
520
  }
521
521
 
522
522
  /* A bar link is its own tap target rather than a line of text, so it carries
523
- the 24 pixel minimum the a11y rule sets. */
523
+ the 24 pixel minimum the keyboard rule sets. */
524
524
  .mast a,
525
525
  .mast summary {
526
526
  min-height: 1.6rem;
@@ -334,7 +334,7 @@ export const STAGES: readonly Stage[] = [
334
334
  kind: 'cli',
335
335
  argv: ['context', 'audit', '--gate'],
336
336
  failure:
337
- 'A cited context path does not resolve, an entry lacks a required section or carries an H2 outside Overview, Layout, and Gotchas, an entry under a shape-gated folder breaks the section shape, or an index disagrees with its folder. Run bun src/cli.ts context audit --gate.',
337
+ 'A cited context path does not resolve, an entry lacks a required section or carries an H2 outside Overview, Layout, and Gotchas, an entry under a shape-gated folder breaks the section shape, an index disagrees with its folder, or a code folder is named in no Layout or claimed by two entries. Run bun src/cli.ts context audit --gate.',
338
338
  },
339
339
  ],
340
340
  success: 'Context citations resolve',
package/src/gov/audit.ts CHANGED
@@ -36,6 +36,21 @@ export const RULE_GATED_FOLDERS: readonly string[] = [
36
36
  join('governance', 'rules', 'framework') + sep,
37
37
  join('governance', 'rules', 'writing') + sep,
38
38
  join('internal', 'rules') + sep,
39
+ join('governance', 'rules', 'lang') + sep,
40
+ join('governance', 'rules', 'lib') + sep,
41
+ join('governance', 'rules', 'ui') + sep,
42
+ join('governance', 'rules', 'tooling') + sep,
43
+ ]
44
+
45
+ /**
46
+ * Rule files in a gated folder whose findings `--gate` still passes, one path
47
+ * each and never a folder, so a new rule beside one is gated the day it lands.
48
+ *
49
+ * `ui/canvas.md` carries a shape finding and no ledger, and v149.2 removes the
50
+ * rule, so this entry goes with it.
51
+ */
52
+ export const RULE_GATE_EXEMPT: readonly string[] = [
53
+ join('governance', 'rules', 'ui', 'canvas.md'),
39
54
  ]
40
55
 
41
56
  /** The folders whose rules claim outside practice, so each carries a ledger. */
@@ -429,13 +444,18 @@ export function hasFinding(reading: RuleReading): boolean {
429
444
  */
430
445
  export function auditExit(
431
446
  report: Extract<RuleAuditReport, { kind: 'measured' }>,
432
- options: { readonly gate: boolean; readonly gated: readonly string[] },
447
+ options: {
448
+ readonly gate: boolean
449
+ readonly gated: readonly string[]
450
+ readonly exempt?: readonly string[]
451
+ },
433
452
  ): number {
434
453
  const failing = report.rules.filter(
435
454
  (reading) =>
436
455
  hasFinding(reading) &&
437
456
  (!options.gate ||
438
- options.gated.some((prefix) => reading.file.startsWith(prefix))),
457
+ (options.gated.some((prefix) => reading.file.startsWith(prefix)) &&
458
+ !options.exempt?.includes(reading.file))),
439
459
  )
440
460
  return failing.length > 0 ? 2 : 0
441
461
  }
package/src/init/plan.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const SKIPPABLE_DOMAINS = ['wiki', 'governance', 'records'] as const
1
+ export const SKIPPABLE_DOMAINS = ['governance', 'records'] as const
2
2
 
3
3
  export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
4
4
 
@@ -97,13 +97,6 @@ export function planInit(flags: InitFlags): InitPlan {
97
97
  })
98
98
  }
99
99
 
100
- if (!flags.skip.skipped.has('wiki')) {
101
- preview.push({
102
- level: 'info',
103
- text: 'wiki (.claude/wiki/ with a stub index)',
104
- })
105
- }
106
-
107
100
  if (!flags.skip.skipped.has('records')) {
108
101
  preview.push({
109
102
  level: 'info',
package/src/init/steps.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { logInfo, logWarn, plural } from '@/cli/ui'
1
2
  import { type InitFlags, resolveStack } from '@/init/plan'
2
3
  import type { DomainStep } from '@/init/run'
4
+ import { GATED_CONFIG_REL, gateScopeRoots } from '@/markdown/gated'
3
5
 
4
6
  /** Builds the child-process invocation for one domain. */
5
7
  export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
@@ -55,13 +57,15 @@ export function buildSteps(
55
57
  })
56
58
  }
57
59
 
58
- if (!flags.skip.skipped.has('wiki')) {
59
- steps.push({
60
- kind: 'run',
61
- label: 'Wiki',
62
- run: child(['wiki', 'init', resolved]),
63
- })
64
- }
60
+ // In-process rather than a child verb, since the audit reads the working
61
+ // directory and every child here is handed the target as an argument. It
62
+ // runs after every seeding domain so the roots it measures hold what they
63
+ // will ship with.
64
+ steps.push({
65
+ kind: 'run',
66
+ label: 'Markdown gating',
67
+ run: () => gateMarkdown(resolved),
68
+ })
65
69
 
66
70
  if (!flags.skip.skipped.has('records')) {
67
71
  // `skip` here means there is nothing to run non-interactively, not that
@@ -76,6 +80,39 @@ export function buildSteps(
76
80
  return steps
77
81
  }
78
82
 
83
+ /**
84
+ * Starts a new project with every density root gated that passes now. A root
85
+ * with a finding is named and left out, since gating it would fail the first
86
+ * push on prose a scaffolder wrote.
87
+ */
88
+ async function gateMarkdown(root: string): Promise<boolean> {
89
+ const result = await gateScopeRoots(root)
90
+
91
+ if (result.kind === 'exists') {
92
+ logInfo(`${GATED_CONFIG_REL} already exists, so its list is kept.`)
93
+ return true
94
+ }
95
+
96
+ // A scaffold such as `npm create vite` makes no repository, and nothing else
97
+ // in init needs one, so this is a skip rather than a failed domain.
98
+ if (result.kind === 'unavailable') {
99
+ logWarn(
100
+ "Skipped: not a git repository, so no root was measured or gated. Run 'canon markdown gated add <prefix>' once it is one.",
101
+ )
102
+ return true
103
+ }
104
+
105
+ logInfo(
106
+ `Gated ${result.gated.length === 0 ? 'nothing' : result.gated.join(', ')} in ${GATED_CONFIG_REL}.`,
107
+ )
108
+ for (const { prefix, findings } of result.left) {
109
+ logWarn(
110
+ `Left ${prefix} ungated with ${plural(findings, 'finding')}. Repair it, then run 'canon markdown gated add ${prefix}'.`,
111
+ )
112
+ }
113
+ return true
114
+ }
115
+
79
116
  /**
80
117
  * Builds the `gov install` argv. The run and the recovery command a skip prints
81
118
  * come from here both, so the command a caller is told to paste installs what
@@ -44,6 +44,10 @@ export interface IntakeItem {
44
44
  readonly answerLine: number | undefined
45
45
  /** Text in the slot, absent when the slot is empty and the item is unread. */
46
46
  readonly answer: string | undefined
47
+ /** What is wrong today, absent when the item carries no `Problem:` line. */
48
+ readonly problem: string | undefined
49
+ /** The one change proposed, absent when the item carries no `Fix:` line. */
50
+ readonly fix: string | undefined
47
51
  readonly open: string | undefined
48
52
  readonly suggested: string | undefined
49
53
  readonly worth: string | undefined
@@ -55,6 +59,8 @@ interface Draft {
55
59
  line: number
56
60
  answerLine: number | undefined
57
61
  answer: string | undefined
62
+ problem: string | undefined
63
+ fix: string | undefined
58
64
  open: string | undefined
59
65
  suggested: string | undefined
60
66
  worth: string | undefined
@@ -89,6 +95,8 @@ export function readItems(text: string): IntakeItem[] {
89
95
  line: line.number,
90
96
  answerLine: undefined,
91
97
  answer: undefined,
98
+ problem: undefined,
99
+ fix: undefined,
92
100
  open: undefined,
93
101
  suggested: undefined,
94
102
  worth: undefined,
@@ -114,6 +122,12 @@ export function readItems(text: string): IntakeItem[] {
114
122
  draft.answerLine = line.number
115
123
  draft.answer = value === '' ? undefined : value
116
124
  break
125
+ case 'problem':
126
+ draft.problem = value
127
+ break
128
+ case 'fix':
129
+ draft.fix = value
130
+ break
117
131
  case 'open':
118
132
  draft.open = value
119
133
  break
@@ -27,7 +27,7 @@ export interface Subject {
27
27
 
28
28
  /**
29
29
  * Reports what each element changes about itself when it takes focus, which is
30
- * the reading `governance/rules/ui/a11y.md` states three rules against and
30
+ * the reading `governance/rules/ui/keyboard.md` states three rules against and
31
31
  * nothing measures.
32
32
  *
33
33
  * The difference between rest and focus is the treatment, rather than the
@@ -0,0 +1,46 @@
1
+ import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
2
+
3
+ /**
4
+ * The keywords GitHub reads as closing an issue on merge, each with its
5
+ * tenses. The keyword must start a word, so `disclosed #12` closes nothing,
6
+ * and GitHub reads only the one number that follows it, so `Closes #12, #13`
7
+ * closes #12 alone.
8
+ */
9
+ const CLOSING =
10
+ /(?<!\w)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?[ \t]+#(\d+)/gi
11
+
12
+ export interface ClosingCheck {
13
+ /** Expected issues the body carries no closing reference for. */
14
+ readonly missing: readonly number[]
15
+ /** Issues the body closes that the expectation did not name. */
16
+ readonly stray: readonly number[]
17
+ }
18
+
19
+ /**
20
+ * Reads the issue numbers a body closes, in order and deduped. A code span and
21
+ * a fence quote a keyword rather than use it, and GitHub ignores both.
22
+ */
23
+ export function readClosingReferences(body: string): readonly number[] {
24
+ const numbers = new Set<number>()
25
+
26
+ for (const line of linesOutsideFences(body)) {
27
+ for (const match of maskCodeSpans(line).matchAll(CLOSING)) {
28
+ numbers.add(Number(match[1]))
29
+ }
30
+ }
31
+
32
+ return [...numbers]
33
+ }
34
+
35
+ /** Compares the issues a body closes to the issues it should. */
36
+ export function checkClosing(
37
+ body: string,
38
+ expected: readonly number[],
39
+ ): ClosingCheck {
40
+ const found = readClosingReferences(body)
41
+
42
+ return {
43
+ missing: expected.filter((number) => !found.includes(number)),
44
+ stray: found.filter((number) => !expected.includes(number)),
45
+ }
46
+ }