@erclx/canon 5.61.0 → 5.63.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/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +2 -1
- package/claude/skills/create-skill/references/skill-practice.md +1 -0
- package/claude/skills/design-identity/references/card-route.md +3 -3
- package/claude/skills/design-record/references/design.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -1
- package/claude/skills/design-taste/SKILL.md +6 -5
- package/claude/skills/design-taste/references/adopted.md +136 -8
- package/claude/skills/design-taste/references/craft.md +1 -1
- package/claude/skills/design-taste/references/kinds.md +8 -2
- package/claude/skills/design-taste/references/preflight.md +3 -3
- package/claude/skills/design-taste/references/tells.md +26 -5
- package/claude/skills/plan-feature/SKILL.md +2 -2
- package/claude/skills/plan-feature/references/plan.md +24 -3
- package/claude/skills/review-craft/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/target-update/SKILL.md +9 -1
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ui-craft/REQUIREMENT.md +29 -0
- package/claude/skills/ui-craft/SKILL.md +44 -0
- package/{governance/rules/ui → claude/skills/ui-craft/references}/a11y.md +2 -10
- package/claude/skills/ui-craft/references/adopted.md +27 -0
- package/{governance/rules/ui/surface-capture.md → claude/skills/ui-craft/references/capture.md} +5 -15
- package/claude/skills/ui-craft/references/controls.md +18 -0
- package/claude/skills/ui-craft/references/copy.md +45 -0
- package/{governance/rules/ui → claude/skills/ui-craft/references}/forms.md +0 -9
- package/{governance/rules/ui → claude/skills/ui-craft/references}/keyboard.md +3 -16
- package/{governance/rules/ui → claude/skills/ui-craft/references}/motion.md +0 -10
- package/claude/skills/ui-craft/references/pointer.md +13 -0
- package/{governance/rules/ui/ux-completeness.md → claude/skills/ui-craft/references/states.md} +2 -20
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/write-human/references/adopted.md +4 -1
- package/docs/agents/install-and-sync.md +47 -13
- package/docs/index.md +1 -1
- package/docs/target/migrations.md +6 -8
- package/docs/target/sync.md +21 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/index.md +3 -3
- package/docs/workflow/skill-map.md +1 -0
- package/docs/workflow/visual-design-workflow.md +58 -32
- package/governance/detect.toml +1 -37
- package/governance/ledger/lib/test-scope.md +1 -1
- package/governance/ledger/{ui → skills/ui-craft}/a11y.md +11 -9
- package/governance/ledger/{ui/surface-capture.md → skills/ui-craft/capture.md} +15 -15
- package/governance/ledger/skills/ui-craft/controls.md +31 -0
- package/governance/ledger/skills/ui-craft/copy.md +102 -0
- package/governance/ledger/{ui → skills/ui-craft}/forms.md +3 -3
- package/governance/ledger/skills/ui-craft/keyboard.md +67 -0
- package/governance/ledger/{ui → skills/ui-craft}/motion.md +3 -3
- package/governance/ledger/skills/ui-craft/pointer.md +24 -0
- package/governance/ledger/{ui/ux-completeness.md → skills/ui-craft/states.md} +14 -26
- package/governance/ledger/ui/craft.md +19 -0
- package/governance/renames.toml +17 -3
- package/governance/rules/lib/test-scope.md +1 -1
- package/governance/rules/ui/craft.md +22 -0
- package/governance/splits.toml +13 -0
- package/package.json +1 -1
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +150 -12
- package/src/commands/claude.ts +5 -0
- package/src/commands/sync.ts +3 -1
- package/src/commands/tooling.ts +332 -10
- package/src/gate/measures.ts +4 -3
- package/src/gov/adapter.ts +53 -4
- package/src/gov/audit.ts +6 -1
- package/src/gov/splits.ts +36 -0
- package/src/inventory/subjects.ts +1 -1
- package/src/sync/check.ts +9 -1
- package/src/sync/stamp.ts +135 -11
- package/src/tooling/disposition.ts +167 -0
- package/src/tooling/inject.ts +17 -2
- package/src/tooling/package.ts +22 -13
- package/src/tooling/scan.ts +86 -7
- package/src/tooling/stamp.ts +51 -7
- package/tooling/web/reference.md +3 -1
- package/governance/ledger/ui/copy.md +0 -49
- package/governance/ledger/ui/design-taste.md +0 -19
- package/governance/ledger/ui/interface-casing.md +0 -47
- package/governance/ledger/ui/keyboard.md +0 -68
- package/governance/ledger/ui/link-behavior.md +0 -29
- package/governance/rules/ui/copy.md +0 -28
- package/governance/rules/ui/design-taste.md +0 -18
- package/governance/rules/ui/interface-casing.md +0 -29
- package/governance/rules/ui/link-behavior.md +0 -19
- /package/tooling/web/{configs → seeds}/e2e/home.spec.ts +0 -0
package/src/tooling/scan.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { resolveSurfacePath } from '@/roots/surface'
|
|
4
|
+
import {
|
|
5
|
+
type Disposition,
|
|
6
|
+
hashFile,
|
|
7
|
+
readStamp,
|
|
8
|
+
stampedDispositions,
|
|
9
|
+
stampedHashes,
|
|
10
|
+
} from '@/sync/stamp'
|
|
4
11
|
import { mergeSections } from '@/tooling/gitignore'
|
|
5
12
|
import {
|
|
6
13
|
ancestorsFirst,
|
|
@@ -19,10 +26,14 @@ import {
|
|
|
19
26
|
} from '@/tooling/package'
|
|
20
27
|
import { isSubfolderTarget, isWithheldInSubfolder } from '@/tooling/subfolder'
|
|
21
28
|
|
|
29
|
+
export type DriftCause = 'stale' | 'edited' | 'unattributed'
|
|
30
|
+
|
|
31
|
+
/** `cause` is set on a drifted config alone. */
|
|
22
32
|
export interface ConfigState {
|
|
23
33
|
readonly rel: string
|
|
24
34
|
readonly stack: string
|
|
25
35
|
readonly state: 'new' | 'drifted' | 'matching'
|
|
36
|
+
readonly cause?: DriftCause
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export interface SeedState {
|
|
@@ -41,9 +52,24 @@ export interface WithheldState {
|
|
|
41
52
|
readonly present: boolean
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A golden file the project declined or customized, which every write skips.
|
|
57
|
+
* `moved` is set on a customized config alone, true once the golden copy no
|
|
58
|
+
* longer hashes to the one the project took.
|
|
59
|
+
*/
|
|
60
|
+
export interface DispositionState {
|
|
61
|
+
readonly rel: string
|
|
62
|
+
readonly stack: string
|
|
63
|
+
readonly kind: 'config' | 'seed'
|
|
64
|
+
readonly disposition: Disposition
|
|
65
|
+
readonly present: boolean
|
|
66
|
+
readonly moved?: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
export interface ScanResult {
|
|
45
70
|
readonly configs: readonly ConfigState[]
|
|
46
71
|
readonly withheld: readonly WithheldState[]
|
|
72
|
+
readonly dispositions: readonly DispositionState[]
|
|
47
73
|
readonly seeds: readonly SeedState[]
|
|
48
74
|
readonly scripts: readonly ScriptState[]
|
|
49
75
|
readonly deps: readonly DepState[]
|
|
@@ -53,6 +79,17 @@ export interface ScanResult {
|
|
|
53
79
|
readonly totalChanges: number
|
|
54
80
|
}
|
|
55
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Why a drifted config differs, which decides whether a write may replace it.
|
|
84
|
+
* A copy still at the hash the last write recorded is `stale`, since canon
|
|
85
|
+
* moved and the project did not. A copy off it is `edited`, and a copy with no
|
|
86
|
+
* recorded hash is `unattributed`, since nothing says who changed it.
|
|
87
|
+
*/
|
|
88
|
+
function driftCause(recorded: string | undefined, dest: string): DriftCause {
|
|
89
|
+
if (recorded === undefined) return 'unattributed'
|
|
90
|
+
return hashFile(dest) === recorded ? 'stale' : 'edited'
|
|
91
|
+
}
|
|
92
|
+
|
|
56
93
|
function isIdentical(a: string, b: string): boolean {
|
|
57
94
|
if (!existsSync(b)) return false
|
|
58
95
|
return readFileSync(a).equals(readFileSync(b))
|
|
@@ -72,11 +109,19 @@ function isIdentical(a: string, b: string): boolean {
|
|
|
72
109
|
* GitHub reads only at the root moves to `withheld` and counts as no change.
|
|
73
110
|
* The scope is read here so every caller of `scan` agrees without passing a
|
|
74
111
|
* flag.
|
|
112
|
+
*
|
|
113
|
+
* A path the project declined or customized moves to `dispositions` ahead of
|
|
114
|
+
* the subfolder test, so it is listed once. The record is read here for the
|
|
115
|
+
* same reason as the scope.
|
|
75
116
|
*/
|
|
76
117
|
export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
77
118
|
const isSubfolder = isSubfolderTarget(target)
|
|
119
|
+
const stamp = readStamp(target)
|
|
120
|
+
const held = stampedDispositions(stamp, 'tooling')
|
|
121
|
+
const recorded = stampedHashes(stamp, 'tooling')
|
|
78
122
|
const configs: ConfigState[] = []
|
|
79
123
|
const withheld: WithheldState[] = []
|
|
124
|
+
const dispositions: DispositionState[] = []
|
|
80
125
|
const seenConfigs = new Set<string>()
|
|
81
126
|
|
|
82
127
|
for (const manifest of chain) {
|
|
@@ -87,18 +132,38 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
87
132
|
const source = join(manifest.configsDir, rel)
|
|
88
133
|
const dest = join(target, rel)
|
|
89
134
|
|
|
135
|
+
const entry = held[rel]
|
|
136
|
+
if (entry !== undefined) {
|
|
137
|
+
dispositions.push({
|
|
138
|
+
rel,
|
|
139
|
+
stack: manifest.name,
|
|
140
|
+
kind: 'config',
|
|
141
|
+
disposition: entry.disposition,
|
|
142
|
+
present: existsSync(dest),
|
|
143
|
+
...(entry.disposition === 'customized'
|
|
144
|
+
? { moved: hashFile(source) !== entry.base }
|
|
145
|
+
: {}),
|
|
146
|
+
})
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
if (isSubfolder && isWithheldInSubfolder(rel)) {
|
|
91
151
|
withheld.push({ rel, stack: manifest.name, present: existsSync(dest) })
|
|
92
152
|
continue
|
|
93
153
|
}
|
|
94
154
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
if (!existsSync(dest)) {
|
|
156
|
+
configs.push({ rel, stack: manifest.name, state: 'new' })
|
|
157
|
+
} else if (isIdentical(source, dest)) {
|
|
158
|
+
configs.push({ rel, stack: manifest.name, state: 'matching' })
|
|
159
|
+
} else {
|
|
160
|
+
configs.push({
|
|
161
|
+
rel,
|
|
162
|
+
stack: manifest.name,
|
|
163
|
+
state: 'drifted',
|
|
164
|
+
cause: driftCause(recorded[rel], dest),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
102
167
|
}
|
|
103
168
|
}
|
|
104
169
|
|
|
@@ -110,6 +175,18 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
110
175
|
if (seenSeeds.has(rel)) continue
|
|
111
176
|
seenSeeds.add(rel)
|
|
112
177
|
|
|
178
|
+
const entry = held[rel]
|
|
179
|
+
if (entry !== undefined) {
|
|
180
|
+
dispositions.push({
|
|
181
|
+
rel,
|
|
182
|
+
stack: manifest.name,
|
|
183
|
+
kind: 'seed',
|
|
184
|
+
disposition: entry.disposition,
|
|
185
|
+
present: existsSync(resolveSurfacePath(target, rel)),
|
|
186
|
+
})
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
113
190
|
if (isSubfolder && isWithheldInSubfolder(rel)) {
|
|
114
191
|
withheld.push({
|
|
115
192
|
rel,
|
|
@@ -144,6 +221,7 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
144
221
|
const totalChanges =
|
|
145
222
|
(newManifest ? 1 : 0) +
|
|
146
223
|
configs.filter((entry) => entry.state !== 'matching').length +
|
|
224
|
+
dispositions.filter((entry) => entry.moved === true).length +
|
|
147
225
|
seeds.filter((entry) => entry.state === 'missing').length +
|
|
148
226
|
scripts.filter((entry) => entry.state !== 'matching').length +
|
|
149
227
|
deps.filter((entry) => entry.state === 'missing').length +
|
|
@@ -152,6 +230,7 @@ export function scan(chain: readonly Manifest[], target: string): ScanResult {
|
|
|
152
230
|
return {
|
|
153
231
|
configs,
|
|
154
232
|
withheld,
|
|
233
|
+
dispositions,
|
|
155
234
|
seeds,
|
|
156
235
|
scripts,
|
|
157
236
|
deps,
|
package/src/tooling/stamp.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type DomainHashes,
|
|
5
|
+
hashFile,
|
|
6
|
+
readStamp,
|
|
7
|
+
stampedChain,
|
|
8
|
+
stampedHashes,
|
|
9
|
+
writeChainStamp,
|
|
10
|
+
} from '@/sync/stamp'
|
|
11
|
+
import {
|
|
12
|
+
configFiles,
|
|
13
|
+
isWholeStack,
|
|
14
|
+
loadManifest,
|
|
15
|
+
type Manifest,
|
|
16
|
+
} from '@/tooling/manifest'
|
|
5
17
|
import { readPackage } from '@/tooling/package'
|
|
6
18
|
|
|
7
19
|
/**
|
|
@@ -32,15 +44,47 @@ export async function recordToolingChain(
|
|
|
32
44
|
): Promise<string[] | undefined> {
|
|
33
45
|
if (isWorkspaceRoot(target)) return undefined
|
|
34
46
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
const stamp = readStamp(target)
|
|
48
|
+
const merged = mergeAddOns(toolkitRoot, stampedChain(stamp, 'tooling'), chain)
|
|
49
|
+
await writeChainStamp(
|
|
50
|
+
target,
|
|
51
|
+
{ domain: 'tooling', toolkitRoot },
|
|
52
|
+
merged,
|
|
53
|
+
now,
|
|
54
|
+
matchedHashes(chain, target, stampedHashes(stamp, 'tooling')),
|
|
39
55
|
)
|
|
40
|
-
await writeChainStamp(target, { domain: 'tooling', toolkitRoot }, merged, now)
|
|
41
56
|
return merged
|
|
42
57
|
}
|
|
43
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The golden hash of every config the target now matches, over the hashes an
|
|
61
|
+
* earlier run recorded. A config the target holds a different copy of keeps
|
|
62
|
+
* its previous hash, so a held edit still reads as edited on the next run
|
|
63
|
+
* rather than as a stale copy the write may replace.
|
|
64
|
+
*/
|
|
65
|
+
function matchedHashes(
|
|
66
|
+
chain: readonly Manifest[],
|
|
67
|
+
target: string,
|
|
68
|
+
previous: DomainHashes,
|
|
69
|
+
): DomainHashes {
|
|
70
|
+
const hashes: Record<string, string> = { ...previous }
|
|
71
|
+
const seen = new Set<string>()
|
|
72
|
+
|
|
73
|
+
for (const manifest of chain) {
|
|
74
|
+
for (const rel of configFiles(manifest)) {
|
|
75
|
+
if (seen.has(rel)) continue
|
|
76
|
+
seen.add(rel)
|
|
77
|
+
|
|
78
|
+
const dest = join(target, rel)
|
|
79
|
+
if (!existsSync(dest)) continue
|
|
80
|
+
const golden = hashFile(join(manifest.configsDir, rel))
|
|
81
|
+
if (hashFile(dest) === golden) hashes[rel] = golden
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return hashes
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* An add-on stands alone beside a core chain, so syncing one must not displace
|
|
46
90
|
* the chain it layers onto. Base extends nothing and still heads a core chain.
|
package/tooling/web/reference.md
CHANGED
|
@@ -45,6 +45,8 @@ The web layer covers the tooling shared across Vite + React, Astro, and Next.js.
|
|
|
45
45
|
|
|
46
46
|
## Screenshot cases
|
|
47
47
|
|
|
48
|
+
The starter `e2e/home.spec.ts` is seeded once. Sync copies it into a project that has none and leaves it alone after, so grow it freely.
|
|
49
|
+
|
|
48
50
|
- Edit the `CASES` in the seeded `e2e/capture/cases.ts`, one entry per output file naming a section, a theme, and a route, since sync overwrites the `e2e/screenshot.ts` harness
|
|
49
51
|
- Move cases from `e2e/screenshot.ts` to `e2e/capture/cases.ts` before a sync, and set `EVIDENCE_ROOT = 'evidence'` to keep an existing baseline
|
|
50
52
|
- Expect each case crossed with `VIEWPORTS`, written to `screenshots/<hostname>/<section>/<width>--<theme>.png`, so local and deployed runs stay apart
|
|
@@ -76,7 +78,7 @@ The web layer covers the tooling shared across Vite + React, Astro, and Next.js.
|
|
|
76
78
|
|
|
77
79
|
- Expect the sweep under `screenshots/` to be ignored and only a flagged case's `EVIDENCE_ROOT` output tracked, so a target's first capture is the baseline it commits
|
|
78
80
|
- Size a case's `sections` list against the frames each later change regenerates, since a case naming sections commits a folder per section
|
|
79
|
-
- Run the capture after a surface changes, component files included, per `
|
|
81
|
+
- Run the capture after a surface changes, component files included, per the `ui-craft` skill's capture reference, unless the production build strips the component out
|
|
80
82
|
|
|
81
83
|
## Screenshot scripts and workflows
|
|
82
84
|
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: UI copy rule ledger
|
|
3
|
-
description: Sources for governance/rules/ui/copy.md, one row per bullet
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# UI copy rule ledger
|
|
7
|
-
|
|
8
|
-
The field is content design, and the authority for researched interface copy is the GOV.UK Design System and its style guide.
|
|
9
|
-
|
|
10
|
-
## Punctuation
|
|
11
|
-
|
|
12
|
-
| Bullet | Source | Read |
|
|
13
|
-
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
14
|
-
| No em dash or semicolon | House choice: both read as generated copy in an interface, and a comma, a period, or a split sentence carries the same meaning in a shorter line | 2026-10-18 |
|
|
15
|
-
| No trailing label punctuation | [Button](https://design-system.service.gov.uk/components/button/): every example label, such as "Start now", "Sign in", and "Continue", carries no trailing punctuation | 2026-10-18 |
|
|
16
|
-
|
|
17
|
-
## Button copy
|
|
18
|
-
|
|
19
|
-
| Bullet | Source | Read |
|
|
20
|
-
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
21
|
-
| The action's verb | [Button](https://design-system.service.gov.uk/components/button/): "Write button text in sentence case, describing the action it performs" | 2026-10-18 |
|
|
22
|
-
|
|
23
|
-
## Error messages
|
|
24
|
-
|
|
25
|
-
| Bullet | Source | Read |
|
|
26
|
-
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
27
|
-
| Say how to fix it | [Error message](https://design-system.service.gov.uk/components/error-message/): "Describe what has happened and tell them how to fix it", and "'Enter your first name' is clearer, more direct and natural than 'First name must have an entry'" | 2026-10-18 |
|
|
28
|
-
| Short and plain | [Error message](https://design-system.service.gov.uk/components/error-message/): "The message must be in plain English, use positive language and get to the point", with no "technical jargon like 'form post error'" | 2026-10-18 |
|
|
29
|
-
|
|
30
|
-
## Placeholder text
|
|
31
|
-
|
|
32
|
-
| Bullet | Source | Read |
|
|
33
|
-
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
34
|
-
| No placeholder, a hint instead | [Text input](https://design-system.service.gov.uk/components/text-input/): "Do not use placeholder text in place of a label, or for hints or examples", since "it vanishes when the user starts typing" and "not all screen readers read it out" | 2026-10-18 |
|
|
35
|
-
|
|
36
|
-
## Declined
|
|
37
|
-
|
|
38
|
-
Every bullet the rewrite removed or reshaped, grouped by why.
|
|
39
|
-
|
|
40
|
-
### Contradicted by the source
|
|
41
|
-
|
|
42
|
-
- Replaced "show format examples over instructions in placeholder text" with the hint text bullet, since GOV.UK says not to use a placeholder "for hints or examples".
|
|
43
|
-
- Cut "reserve instructions for labels or helper text", which the new placeholder bullet now carries.
|
|
44
|
-
|
|
45
|
-
### Merged or reworded
|
|
46
|
-
|
|
47
|
-
- Merged the imperative verb and padded phrasing bullets. The `Save` over `Save changes` example went, since GOV.UK adds words "to better describe the action".
|
|
48
|
-
- Merged "keep messages short and actionable" and "one sentence maximum" into the plain language bullet.
|
|
49
|
-
- Reworded the error example to `Enter an email address`, the instruction form GOV.UK prefers over a description.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Design taste rule ledger
|
|
3
|
-
description: Sources for governance/rules/ui/design-taste.md, one row per bullet
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Design taste rule ledger
|
|
7
|
-
|
|
8
|
-
The rule is a pointer to the `design-taste` skill, which carries its own sources, so both rows are house choices naming the bug the pointer prevents.
|
|
9
|
-
|
|
10
|
-
## Authority
|
|
11
|
-
|
|
12
|
-
| Bullet | Source | Read |
|
|
13
|
-
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
14
|
-
| Load the skill | House choice: a style edit made without the layer order or the coherence locks reaches for a model's defaults, and a second copy of them in the rule drifts from the skill | 2026-10-18 |
|
|
15
|
-
| Report an unresolved skill | House choice: the rule ships with the CLI and the skill with the plugin, so a project holding governance alone would otherwise proceed silently without it | 2026-10-18 |
|
|
16
|
-
|
|
17
|
-
## Declined
|
|
18
|
-
|
|
19
|
-
The rewrite kept both bullets unchanged.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Interface casing rule ledger
|
|
3
|
-
description: Sources for governance/rules/ui/interface-casing.md, one row per bullet
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Interface casing rule ledger
|
|
7
|
-
|
|
8
|
-
The field is content design, and the authority is the GOV.UK style guide, which sets sentence case for every heading and title. The label count and the scope bullets are canon's own.
|
|
9
|
-
|
|
10
|
-
## Sentence case
|
|
11
|
-
|
|
12
|
-
| Bullet | Source | Read |
|
|
13
|
-
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
14
|
-
| Sentence case | [A to Z of GOV.UK style](https://www.gov.uk/guidance/style-guide/a-to-z-of-gov-uk-style): "Always use sentence case, even in page titles and service names" | 2026-10-18 |
|
|
15
|
-
| Proper nouns keep casing | [A to Z of GOV.UK style](https://www.gov.uk/guidance/style-guide/a-to-z-of-gov-uk-style): "The exceptions to this are proper nouns" | 2026-10-18 |
|
|
16
|
-
|
|
17
|
-
## Uppercase labels
|
|
18
|
-
|
|
19
|
-
| Bullet | Source | Read |
|
|
20
|
-
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
21
|
-
| Label count | House choice: an uppercase eyebrow over every section is the template tell `design-taste` reaches past, and the count makes the cap checkable against a page | 2026-10-18 |
|
|
22
|
-
|
|
23
|
-
## Where it applies
|
|
24
|
-
|
|
25
|
-
| Bullet | Source | Read |
|
|
26
|
-
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
27
|
-
| Copy outside the globs | House choice: a locale file holds the same rendered strings, so title case there ships with no rule firing on it | 2026-10-18 |
|
|
28
|
-
|
|
29
|
-
## Overriding
|
|
30
|
-
|
|
31
|
-
| Bullet | Source | Read |
|
|
32
|
-
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
33
|
-
| Depart where needed | House choice: an acronym or brand mark lowered to sentence case misnames it, the same reason GOV.UK keeps "proper nouns" as its one exception | 2026-10-18 |
|
|
34
|
-
|
|
35
|
-
## Declined
|
|
36
|
-
|
|
37
|
-
Every bullet the rewrite removed or reshaped, grouped by why.
|
|
38
|
-
|
|
39
|
-
### Cut without a check
|
|
40
|
-
|
|
41
|
-
- Cut "treat the count as a judgment, since a glob cannot check it", which tells a session nothing it can verify against a line.
|
|
42
|
-
|
|
43
|
-
### Merged or reworded
|
|
44
|
-
|
|
45
|
-
- Folded "treat title case on every heading as a template tell" into the sentence case bullet as its reason.
|
|
46
|
-
- Shortened the uppercase label bullet under 200 characters, keeping the `ceil(sections / 3)` count and the delete remedy.
|
|
47
|
-
- Reworded the scope bullet from a statement of what the rule covers to the directive a session can act on.
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Keyboard rule ledger
|
|
3
|
-
description: Sources for governance/rules/ui/keyboard.md, one row per bullet
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Keyboard rule ledger
|
|
7
|
-
|
|
8
|
-
The field is web accessibility, and the authorities are the W3C, through WCAG 2.2 and the ARIA Authoring Practices Guide, MDN for CSS behavior, and the platform guidelines from Apple and Android for native targets. Every bullet here was split from `a11y.md`.
|
|
9
|
-
|
|
10
|
-
## Focus styles
|
|
11
|
-
|
|
12
|
-
| Bullet | Source | Read |
|
|
13
|
-
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
14
|
-
| One project focus ring | House choice: per-component focus styles drift apart, and `canon inventory` reports the ring by computed style, so a one-off treatment shows as its own row | 2026-10-18 |
|
|
15
|
-
| No outline removal without a replacement | [outline](https://developer.mozilla.org/en-US/docs/Web/CSS/outline): "Assigning outline a value of 0 or none will remove the browser's default focus style. If an element can be interacted with it must have a visible focus indicator." | 2026-10-18 |
|
|
16
|
-
| 3:1 ring in every theme | [Understanding Non-text Contrast](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html): visual information needed to identify a state needs "a contrast ratio of at least 3:1 against adjacent color(s)" | 2026-10-18, WCAG 2.2 |
|
|
17
|
-
|
|
18
|
-
## Keyboard operation
|
|
19
|
-
|
|
20
|
-
| Bullet | Source | Read |
|
|
21
|
-
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- |
|
|
22
|
-
| Escape closes | [Dialog (modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): "Escape : Closes the dialog", and the [combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) gives Escape the same job for a popup | 2026-10-18, APG |
|
|
23
|
-
| No bare character shortcut | [Understanding Character Key Shortcuts](https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html): a character-only shortcut needs a way to "Turn off", to "Remap", or to be "Active only on focus" | 2026-10-18, WCAG 2.2 |
|
|
24
|
-
| Drag has a single-pointer path | [Understanding Dragging Movements](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html): "For any action that involves dragging, provide a simple pointer alternative" | 2026-10-18, WCAG 2.2 |
|
|
25
|
-
|
|
26
|
-
## Composite widgets
|
|
27
|
-
|
|
28
|
-
| Bullet | Source | Read |
|
|
29
|
-
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- |
|
|
30
|
-
| One item in the tab sequence | [Developing a keyboard interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/): "the tab sequence should include only one focusable element of a composite UI component", managed by "a roving tabindex" or "the aria-activedescendant property" | 2026-10-18, APG |
|
|
31
|
-
| Combobox keys | [Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/): "Down Arrow : If the popup is available, moves focus into the popup", Enter accepts the focused option, and "Escape : Dismisses the popup if it is visible" | 2026-10-18, APG |
|
|
32
|
-
|
|
33
|
-
## Focus management
|
|
34
|
-
|
|
35
|
-
| Bullet | Source | Read |
|
|
36
|
-
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
37
|
-
| Focus into a dialog and back | [Dialog (modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): "When a dialog opens, focus moves to an element inside the dialog", and "When a dialog closes, focus returns to the element that invoked the dialog" | 2026-10-18, APG |
|
|
38
|
-
| Focus never covered | [Understanding Focus Not Obscured (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html): "the component is not entirely hidden due to author-created content" | 2026-10-18, WCAG 2.2 |
|
|
39
|
-
|
|
40
|
-
## Pointer targets
|
|
41
|
-
|
|
42
|
-
| Bullet | Source | Read |
|
|
43
|
-
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
44
|
-
| 24 by 24 on the web | [Understanding Target Size (Minimum)](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html): "at least 24 by 24 CSS pixels", except where "a 24 CSS pixel diameter circle is centered on the bounding box of each" and "the circles do not intersect" | 2026-10-18, WCAG 2.2 |
|
|
45
|
-
| Native figures | [Apple accessibility guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility): a default "44x44 pt" control size on iOS and iPadOS, and [Android accessibility](https://developer.android.com/guide/topics/ui/accessibility/apps): "a focusable area, or touch target size , of at least 48dpx48dp" | 2026-10-18 |
|
|
46
|
-
|
|
47
|
-
## Declined
|
|
48
|
-
|
|
49
|
-
Every bullet the split removed or reshaped, grouped by why.
|
|
50
|
-
|
|
51
|
-
### Browser defaults
|
|
52
|
-
|
|
53
|
-
- Cut the Tab, Enter, and Space bullets of the old keyboard model. A native element does each by default, and `a11y.md` already sends a session to the native element.
|
|
54
|
-
|
|
55
|
-
### Rewritten to what the source says
|
|
56
|
-
|
|
57
|
-
- Replaced "do not introduce non-standard key bindings" with the character key bullet. No reviewer can check "non-standard", and the criterion names the binding that breaks.
|
|
58
|
-
- Narrowed "move focus into any overlay, popover, or dialog on open" to a dialog. The APG moves focus into a modal dialog, and a non-modal popover such as a tooltip leaves focus on its trigger.
|
|
59
|
-
|
|
60
|
-
### Cut without a source or a bug class
|
|
61
|
-
|
|
62
|
-
- Cut "never move focus on hover", a convention Claude already follows that no cited page takes a side on.
|
|
63
|
-
|
|
64
|
-
### Merged or reworded
|
|
65
|
-
|
|
66
|
-
- Merged "arrow keys navigate within composite widgets" into the roving `tabindex` bullet, which names the arrow keys.
|
|
67
|
-
- Merged "Escape cancels in-progress edits" into the Escape bullet, and took in the inline confirmation case from the `ux-completeness.md` rewrite so the Escape behavior has one copy.
|
|
68
|
-
- Reworded the combobox bullet to the pattern's own keys, dropping "typing filters", which the APG leaves to each autocomplete behavior.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Link behavior rule ledger
|
|
3
|
-
description: Sources for governance/rules/ui/link-behavior.md, one row per bullet
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Link behavior rule ledger
|
|
7
|
-
|
|
8
|
-
The field is interaction design, and the authority is the Nielsen Norman Group's research on new windows and tabs.
|
|
9
|
-
|
|
10
|
-
## Default target
|
|
11
|
-
|
|
12
|
-
| Bullet | Source | Read |
|
|
13
|
-
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
|
|
14
|
-
| Same tab, new tab by task | [Opening links in new browser windows and tabs](https://www.nngroup.com/articles/new-browser-windows-and-tabs/): "There is generally one good reason to open a page in a new window, and that is that the user will need to refer to that content in order to complete a task in another window" | 2026-10-18 |
|
|
15
|
-
| No blanket external new tab | [Opening links in new browser windows and tabs](https://www.nngroup.com/articles/new-browser-windows-and-tabs/): "New windows or tabs prevent the use of the Back button" and "can cause disorientation" | 2026-10-18 |
|
|
16
|
-
|
|
17
|
-
## mailto: links
|
|
18
|
-
|
|
19
|
-
| Bullet | Source | Read |
|
|
20
|
-
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
|
|
21
|
-
| `mailto:` same tab | House choice: the link opens the mail client and leaves the page in place, so `target="_blank"` only adds an empty tab the person must close | 2026-10-18 |
|
|
22
|
-
|
|
23
|
-
## Declined
|
|
24
|
-
|
|
25
|
-
Every bullet the rewrite removed or reshaped, grouped by why.
|
|
26
|
-
|
|
27
|
-
### Rewritten to what the source says
|
|
28
|
-
|
|
29
|
-
- Reworded the new tab exception from "a destination that would discard in-progress work" to a page the person must consult while finishing a task, the one reason NN/g names.
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Enforce UI copy and text standards for rendered strings in components
|
|
3
|
-
paths:
|
|
4
|
-
- '**/*.tsx'
|
|
5
|
-
- '**/*.jsx'
|
|
6
|
-
- '**/*.astro'
|
|
7
|
-
- '**/*.html'
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# UI copy standards
|
|
11
|
-
|
|
12
|
-
## Punctuation
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
## Button copy
|
|
18
|
-
|
|
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`.
|
|
20
|
-
|
|
21
|
-
## Error messages
|
|
22
|
-
|
|
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.
|
|
25
|
-
|
|
26
|
-
## Placeholder text
|
|
27
|
-
|
|
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,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Route edits that decide how a surface looks to the design-taste skill for the layer ordering, the coherence locks, and the defaults a model reaches for
|
|
3
|
-
paths:
|
|
4
|
-
- '**/*.tsx'
|
|
5
|
-
- '**/*.jsx'
|
|
6
|
-
- '**/*.astro'
|
|
7
|
-
- '**/*.html'
|
|
8
|
-
- '**/*.css'
|
|
9
|
-
- '**/*.scss'
|
|
10
|
-
- 'canon/DESIGN.md'
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
# Design taste standards
|
|
14
|
-
|
|
15
|
-
## Authority
|
|
16
|
-
|
|
17
|
-
- Load the `canon:design-taste` skill for which design decision settles first, what must stay constant across a surface, and the defaults to reach past. It is the single source for all three.
|
|
18
|
-
- Report it rather than proceeding silently when the skill does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Enforce casing for interface text in markup, MDX, and content modules, where copy lives in a content folder
|
|
3
|
-
paths:
|
|
4
|
-
- '**/*.tsx'
|
|
5
|
-
- '**/*.jsx'
|
|
6
|
-
- '**/*.astro'
|
|
7
|
-
- '**/*.html'
|
|
8
|
-
- '**/*.mdx'
|
|
9
|
-
- '**/content/**/*.ts'
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Interface casing standards
|
|
13
|
-
|
|
14
|
-
## Sentence case
|
|
15
|
-
|
|
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`.
|
|
18
|
-
|
|
19
|
-
## Uppercase labels
|
|
20
|
-
|
|
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.
|
|
22
|
-
|
|
23
|
-
## Where it applies
|
|
24
|
-
|
|
25
|
-
- Apply the same casing to interface copy kept outside the globs, such as a locale file, which this rule never fires on.
|
|
26
|
-
|
|
27
|
-
## Overriding
|
|
28
|
-
|
|
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.
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Enforce the same-tab default for an outbound link and for a mail link
|
|
3
|
-
paths:
|
|
4
|
-
- '**/*.tsx'
|
|
5
|
-
- '**/*.jsx'
|
|
6
|
-
- '**/*.astro'
|
|
7
|
-
- '**/*.html'
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# Link behavior standards
|
|
11
|
-
|
|
12
|
-
## Default target
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
## mailto: links
|
|
18
|
-
|
|
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.
|
|
File without changes
|