@erclx/canon 5.62.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 +10 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-skill/references/skill-practice.md +1 -0
- 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/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 +0 -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} +0 -10
- package/{governance/rules/ui/ux-controls.md → claude/skills/ui-craft/references/controls.md} +3 -10
- 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 +0 -10
- package/{governance/rules/ui → claude/skills/ui-craft/references}/motion.md +0 -10
- package/{governance/rules/ui → claude/skills/ui-craft/references}/pointer.md +0 -10
- package/{governance/rules/ui/ux-completeness.md → claude/skills/ui-craft/references/states.md} +0 -10
- 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 +2 -2
- package/docs/target/migrations.md +3 -3
- package/docs/target/sync.md +3 -2
- package/docs/workflow/skill-map.md +1 -0
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +1 -45
- package/governance/ledger/lib/test-scope.md +1 -1
- package/governance/ledger/{ui → skills/ui-craft}/a11y.md +3 -3
- package/governance/ledger/{ui/surface-capture.md → skills/ui-craft/capture.md} +3 -3
- package/governance/ledger/{ui/ux-controls.md → skills/ui-craft/controls.md} +12 -7
- package/governance/ledger/skills/ui-craft/copy.md +102 -0
- package/governance/ledger/{ui → skills/ui-craft}/forms.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/keyboard.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/motion.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/pointer.md +3 -3
- package/governance/ledger/{ui/ux-completeness.md → skills/ui-craft/states.md} +3 -3
- 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 +0 -2
- 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/tooling.ts +4 -1
- package/src/gate/measures.ts +4 -3
- package/src/gov/audit.ts +6 -1
- package/src/inventory/subjects.ts +1 -1
- package/src/tooling/package.ts +22 -13
- 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/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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Motion
|
|
3
|
-
description: Sources for
|
|
2
|
+
title: Motion reference ledger
|
|
3
|
+
description: Sources for claude/skills/ui-craft/references/motion.md, one row per bullet
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Motion
|
|
6
|
+
# Motion reference ledger
|
|
7
7
|
|
|
8
8
|
The field is web accessibility, and the authorities are the W3C, through WCAG 2.2 and the ARIA Authoring Practices Guide, and MDN for the media feature.
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Pointer
|
|
3
|
-
description: Sources for
|
|
2
|
+
title: Pointer reference ledger
|
|
3
|
+
description: Sources for claude/skills/ui-craft/references/pointer.md, one row per bullet
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Pointer
|
|
6
|
+
# Pointer reference ledger
|
|
7
7
|
|
|
8
8
|
The field is web accessibility, and the authorities are the W3C, through WCAG 2.2 and the ARIA Authoring Practices Guide, MDN for popover behavior, and the platform guidelines from Apple and Android for native targets. These bullets were split from `keyboard.md`.
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: UX completeness
|
|
3
|
-
description: Sources for
|
|
2
|
+
title: UX completeness reference ledger
|
|
3
|
+
description: Sources for claude/skills/ui-craft/references/states.md, one row per bullet
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# UX completeness
|
|
6
|
+
# UX completeness reference ledger
|
|
7
7
|
|
|
8
8
|
The field is interaction design, and the authorities are the Nielsen Norman Group's research articles, WCAG 2.2 for timing, MDN for the `title` attribute and CSS truncation, and web.dev for layout shift.
|
|
9
9
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: UI craft rule ledger
|
|
3
|
+
description: Sources for governance/rules/ui/craft.md, one row per bullet
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# UI craft rule ledger
|
|
7
|
+
|
|
8
|
+
The rule is a pointer to the `ui-craft` and `design-taste` skills, which carry their 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 skills | House choice: a rendered edit made without the behavior floor or the layer order reaches for a model's defaults, and a second copy of either in the rule drifts from the skill that owns it | 2026-10-24 |
|
|
15
|
+
| Report an unresolved skill | House choice: the rule ships with the CLI and the skills with the plugin, so a project holding governance alone would otherwise proceed silently without them | 2026-10-18 |
|
|
16
|
+
|
|
17
|
+
## Declined
|
|
18
|
+
|
|
19
|
+
The fold kept both bullets and widened the first to name `ui-craft` beside `design-taste`, since the practice rules it replaced now live in that skill.
|
package/governance/renames.toml
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
# so `canon gov sync` replaces a held old file with the rule under its new name
|
|
4
4
|
# rather than only deleting it.
|
|
5
5
|
#
|
|
6
|
-
# An entry declares one-to-one identity
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# An entry declares one-to-one identity, or a fold onto a pointer rule that
|
|
7
|
+
# now routes to the skill carrying the folded rules, so a target holding any
|
|
8
|
+
# of them lands on the pointer rather than losing the practice with nothing in
|
|
9
|
+
# its place. Any other fold of several rules into one, or a split of one into
|
|
10
|
+
# several, is a retirement plus an addition and takes no entry here. Add the entry in the same change as the
|
|
9
11
|
# rename or renumber. Prune an entry only once its target rule is retired and
|
|
10
12
|
# the retire path covers it, since a chain ending on a rule nothing ships
|
|
11
13
|
# resolves to nothing and a sync deletes the held file instead. Otherwise leave
|
|
@@ -73,3 +75,15 @@
|
|
|
73
75
|
"460-design-taste" = "design-taste"
|
|
74
76
|
"465-interface-casing" = "interface-casing"
|
|
75
77
|
"470-motion" = "motion"
|
|
78
|
+
"design-taste" = "craft"
|
|
79
|
+
"a11y" = "craft"
|
|
80
|
+
"keyboard" = "craft"
|
|
81
|
+
"pointer" = "craft"
|
|
82
|
+
"forms" = "craft"
|
|
83
|
+
"motion" = "craft"
|
|
84
|
+
"ux-controls" = "craft"
|
|
85
|
+
"ux-completeness" = "craft"
|
|
86
|
+
"surface-capture" = "craft"
|
|
87
|
+
"copy" = "craft"
|
|
88
|
+
"interface-casing" = "craft"
|
|
89
|
+
"link-behavior" = "craft"
|
|
@@ -20,5 +20,5 @@ paths:
|
|
|
20
20
|
|
|
21
21
|
- Answer a question about the running page with a script against the dev server rather than with the suite.
|
|
22
22
|
- Do not enable `fullyParallel` in the Playwright config.
|
|
23
|
-
- Follow
|
|
23
|
+
- Follow the `canon:ui-craft` skill's capture reference for capture scope, reporting it when the skill does not resolve.
|
|
24
24
|
- Follow `.claude/rules/canon/lib/e2e-reliability.md` for waits and guards.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route edits to a rendered surface to the ui-craft skill for how it behaves and to the design-taste skill for how it looks
|
|
3
|
+
paths:
|
|
4
|
+
- '**/*.tsx'
|
|
5
|
+
- '**/*.jsx'
|
|
6
|
+
- '**/*.vue'
|
|
7
|
+
- '**/*.svelte'
|
|
8
|
+
- '**/*.astro'
|
|
9
|
+
- '**/*.html'
|
|
10
|
+
- '**/*.mdx'
|
|
11
|
+
- '**/*.css'
|
|
12
|
+
- '**/*.scss'
|
|
13
|
+
- '**/content/**/*.ts'
|
|
14
|
+
- 'canon/DESIGN.md'
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# UI craft standards
|
|
18
|
+
|
|
19
|
+
## Authority
|
|
20
|
+
|
|
21
|
+
- Load the `canon:ui-craft` skill for how a surface behaves and the `canon:design-taste` skill for how it looks. Each is the single source for its half.
|
|
22
|
+
- Report it rather than proceeding silently when either skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so governance alone carries neither.
|
package/governance/splits.toml
CHANGED
package/package.json
CHANGED
package/src/claude/cases/misc.ts
CHANGED
|
@@ -17,6 +17,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
17
17
|
'Which layer of this landing page should I settle first, and what has to stay constant across it?',
|
|
18
18
|
expect: 'design-taste',
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
prompt:
|
|
22
|
+
'This settings form needs validation and an empty state, so what does it owe for keyboard users and error messages before I build it?',
|
|
23
|
+
expect: 'ui-craft',
|
|
24
|
+
},
|
|
20
25
|
{
|
|
21
26
|
prompt:
|
|
22
27
|
'This project has no logo yet. Draft one and give me a social card to go with it.',
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
3
|
import { isFamilyKey } from '@/claude/skills-families'
|
|
4
|
+
import { plural } from '@/cli/ui'
|
|
5
|
+
import {
|
|
6
|
+
compareLedger,
|
|
7
|
+
countLedgerRows,
|
|
8
|
+
type LedgerMismatch,
|
|
9
|
+
measureRule,
|
|
10
|
+
} from '@/gov/audit'
|
|
4
11
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
5
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
type BodyLine,
|
|
14
|
+
bodyLines,
|
|
15
|
+
maskCodeSpans,
|
|
16
|
+
maskDisplayed,
|
|
17
|
+
} from '@/markdown/scan'
|
|
6
18
|
|
|
7
19
|
/** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
|
|
8
20
|
export const EXIT_MISSING_REQUIREMENT = 2
|
|
@@ -69,8 +81,22 @@ export const LEDGER_SKILLS: readonly string[] = [
|
|
|
69
81
|
join(SHIPPED_CORPUS, 'design-taste'),
|
|
70
82
|
join(SHIPPED_CORPUS, 'write-human'),
|
|
71
83
|
join(SHIPPED_CORPUS, 'test-first'),
|
|
84
|
+
join(SHIPPED_CORPUS, 'ui-craft'),
|
|
72
85
|
]
|
|
73
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The ledger-listed skills whose every reference but the source record carries
|
|
89
|
+
* a ledger of its own, one table row per bullet under each H2, the way a rule
|
|
90
|
+
* in a practice folder does. The ledgers sit outside the plugin as rule
|
|
91
|
+
* ledgers do, so a target pays nothing for them.
|
|
92
|
+
*/
|
|
93
|
+
export const BULLET_LEDGER_SKILLS: readonly string[] = [
|
|
94
|
+
join(SHIPPED_CORPUS, 'ui-craft'),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
/** Where a per-bullet ledger lives, as `<skill>/<reference>.md` below it. */
|
|
98
|
+
export const SKILL_LEDGERS = join('governance', 'ledger', 'skills')
|
|
99
|
+
|
|
74
100
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
75
101
|
export const PRACTICE_SECTIONS: readonly string[] = [
|
|
76
102
|
'Excuses and rebuttals',
|
|
@@ -81,6 +107,19 @@ export const PRACTICE_SECTIONS: readonly string[] = [
|
|
|
81
107
|
/** The source ledger every practice skill carries beside its body. */
|
|
82
108
|
export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
83
109
|
|
|
110
|
+
/**
|
|
111
|
+
* A ledger cites a source by linking a page outside the project, as an inline
|
|
112
|
+
* link or an autolink. A relative link points back into the skill and cites
|
|
113
|
+
* nothing a reader could check the rule against.
|
|
114
|
+
*/
|
|
115
|
+
const EXTERNAL_LINK = /\]\(\s*https?:\/\/|<https?:\/\//
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A table cell opening on the marker the governance ledgers already use for a
|
|
119
|
+
* rule the toolkit chose, so one check reads every ledger alike.
|
|
120
|
+
*/
|
|
121
|
+
const HOUSE_CHOICE = /\|\s*House choice\b/
|
|
122
|
+
|
|
84
123
|
/**
|
|
85
124
|
* The line count past which a reference opens with a contents list, from
|
|
86
125
|
* Anthropic's skill authoring guidance. Counted over the whole file the way
|
|
@@ -166,13 +205,23 @@ interface SkillSource {
|
|
|
166
205
|
readonly requirement: string | undefined
|
|
167
206
|
/** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
|
|
168
207
|
readonly sections: readonly string[]
|
|
169
|
-
|
|
208
|
+
/** Undefined when the folder carries no ledger at all. */
|
|
209
|
+
readonly ledger: string | undefined
|
|
170
210
|
/**
|
|
171
211
|
* `SKILL.md` and every reference below the folder, keyed by the path a
|
|
172
212
|
* finding names. `REQUIREMENT.md` and `EVAL.md` are left out, since the
|
|
173
213
|
* requirement is where the standard sends a date.
|
|
174
214
|
*/
|
|
175
215
|
readonly bodies: readonly SkillText[]
|
|
216
|
+
/** One entry per reference of a skill on the per-bullet ledger list. */
|
|
217
|
+
readonly referenceLedgers: readonly ReferenceLedger[]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface ReferenceLedger {
|
|
221
|
+
readonly rel: string
|
|
222
|
+
readonly ledgerRel: string
|
|
223
|
+
/** Undefined when the ledger file is absent. */
|
|
224
|
+
readonly mismatches: readonly LedgerMismatch[] | undefined
|
|
176
225
|
}
|
|
177
226
|
|
|
178
227
|
interface SkillText {
|
|
@@ -195,7 +244,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
|
|
|
195
244
|
)
|
|
196
245
|
|
|
197
246
|
const perCorpus = await Promise.all(
|
|
198
|
-
present.map((corpus) => readCorpus(corpus.rel, corpus.dir)),
|
|
247
|
+
present.map((corpus) => readCorpus(root, corpus.rel, corpus.dir)),
|
|
199
248
|
)
|
|
200
249
|
const sources = perCorpus.flat()
|
|
201
250
|
|
|
@@ -247,21 +296,29 @@ export function auditExitCode(report: SkillsAudit): number {
|
|
|
247
296
|
return report.missingRequirement.length > 0 ? EXIT_MISSING_REQUIREMENT : 0
|
|
248
297
|
}
|
|
249
298
|
|
|
250
|
-
async function readCorpus(
|
|
299
|
+
async function readCorpus(
|
|
300
|
+
root: string,
|
|
301
|
+
rel: string,
|
|
302
|
+
dir: string,
|
|
303
|
+
): Promise<SkillSource[]> {
|
|
251
304
|
const paths = [
|
|
252
305
|
...new Bun.Glob('*/SKILL.md').scanSync({ cwd: dir, onlyFiles: true }),
|
|
253
306
|
].sort()
|
|
254
307
|
|
|
255
|
-
return Promise.all(
|
|
308
|
+
return Promise.all(
|
|
309
|
+
paths.map((path) => readSkill(root, rel, dir, dirname(path))),
|
|
310
|
+
)
|
|
256
311
|
}
|
|
257
312
|
|
|
258
313
|
async function readSkill(
|
|
314
|
+
root: string,
|
|
259
315
|
rel: string,
|
|
260
316
|
dir: string,
|
|
261
317
|
folder: string,
|
|
262
318
|
): Promise<SkillSource> {
|
|
263
319
|
const skillDir = join(dir, folder)
|
|
264
320
|
const requirementPath = join(skillDir, 'REQUIREMENT.md')
|
|
321
|
+
const ledgerPath = join(skillDir, PRACTICE_LEDGER)
|
|
265
322
|
|
|
266
323
|
const references = [
|
|
267
324
|
...new Bun.Glob('references/**/*.md').scanSync({
|
|
@@ -270,11 +327,14 @@ async function readSkill(
|
|
|
270
327
|
}),
|
|
271
328
|
].sort()
|
|
272
329
|
|
|
273
|
-
const [body, requirement, referenceTexts] = await Promise.all([
|
|
330
|
+
const [body, requirement, ledger, referenceTexts] = await Promise.all([
|
|
274
331
|
Bun.file(join(skillDir, 'SKILL.md')).text(),
|
|
275
332
|
existsSync(requirementPath)
|
|
276
333
|
? Bun.file(requirementPath).text()
|
|
277
334
|
: Promise.resolve(undefined),
|
|
335
|
+
existsSync(ledgerPath)
|
|
336
|
+
? Bun.file(ledgerPath).text()
|
|
337
|
+
: Promise.resolve(undefined),
|
|
278
338
|
Promise.all(
|
|
279
339
|
references.map((path) => Bun.file(join(skillDir, path)).text()),
|
|
280
340
|
),
|
|
@@ -282,6 +342,15 @@ async function readSkill(
|
|
|
282
342
|
|
|
283
343
|
const fields = parseFrontmatter(body)
|
|
284
344
|
const skillRel = join(rel, folder)
|
|
345
|
+
const referenceLedgers = BULLET_LEDGER_SKILLS.includes(skillRel)
|
|
346
|
+
? await readReferenceLedgers(
|
|
347
|
+
root,
|
|
348
|
+
skillRel,
|
|
349
|
+
folder,
|
|
350
|
+
references,
|
|
351
|
+
referenceTexts,
|
|
352
|
+
)
|
|
353
|
+
: []
|
|
285
354
|
|
|
286
355
|
return {
|
|
287
356
|
rel: skillRel,
|
|
@@ -294,7 +363,7 @@ async function readSkill(
|
|
|
294
363
|
requirement === undefined ? undefined : headings(requirement),
|
|
295
364
|
requirement,
|
|
296
365
|
sections: sectionsOf(body),
|
|
297
|
-
|
|
366
|
+
ledger,
|
|
298
367
|
bodies: [
|
|
299
368
|
{ rel: join(skillRel, 'SKILL.md'), text: body, isReference: false },
|
|
300
369
|
...references.map((path, index) => ({
|
|
@@ -303,9 +372,45 @@ async function readSkill(
|
|
|
303
372
|
isReference: true,
|
|
304
373
|
})),
|
|
305
374
|
],
|
|
375
|
+
referenceLedgers,
|
|
306
376
|
}
|
|
307
377
|
}
|
|
308
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Pairs each reference's bullets with its ledger's rows through the rule
|
|
381
|
+
* audit's own comparison, so a reference and a rule are held to one count.
|
|
382
|
+
* The source record names authorities rather than stating bullets, so it is
|
|
383
|
+
* the one reference asked for no ledger.
|
|
384
|
+
*/
|
|
385
|
+
async function readReferenceLedgers(
|
|
386
|
+
root: string,
|
|
387
|
+
skillRel: string,
|
|
388
|
+
folder: string,
|
|
389
|
+
references: readonly string[],
|
|
390
|
+
texts: readonly string[],
|
|
391
|
+
): Promise<ReferenceLedger[]> {
|
|
392
|
+
return Promise.all(
|
|
393
|
+
references
|
|
394
|
+
.map((path, index) => ({ path, text: texts[index] ?? '' }))
|
|
395
|
+
.filter(({ path }) => path !== PRACTICE_LEDGER)
|
|
396
|
+
.map(async ({ path, text }) => {
|
|
397
|
+
const ledgerRel = join(
|
|
398
|
+
SKILL_LEDGERS,
|
|
399
|
+
folder,
|
|
400
|
+
relative('references', path),
|
|
401
|
+
)
|
|
402
|
+
const ledgerPath = join(root, ledgerRel)
|
|
403
|
+
const mismatches = existsSync(ledgerPath)
|
|
404
|
+
? compareLedger(
|
|
405
|
+
measureRule(text).groups,
|
|
406
|
+
countLedgerRows(await Bun.file(ledgerPath).text()),
|
|
407
|
+
)
|
|
408
|
+
: undefined
|
|
409
|
+
return { rel: join(skillRel, path), ledgerRel, mismatches }
|
|
410
|
+
}),
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
309
414
|
/**
|
|
310
415
|
* A key carrying an empty value declares nothing, so blank reads as absent
|
|
311
416
|
* rather than as a name that disagrees with every folder.
|
|
@@ -493,11 +598,44 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
|
493
598
|
rel: source.rel,
|
|
494
599
|
detail: `missing section: ${section}`,
|
|
495
600
|
}))
|
|
496
|
-
const ledger = source.hasLedger
|
|
497
|
-
? []
|
|
498
|
-
: [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
499
601
|
|
|
500
|
-
return [
|
|
602
|
+
return [
|
|
603
|
+
...sections,
|
|
604
|
+
...ledgerFindings(source),
|
|
605
|
+
...source.referenceLedgers.flatMap(referenceLedgerFindings),
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function referenceLedgerFindings(entry: ReferenceLedger): SkillFinding[] {
|
|
610
|
+
if (entry.mismatches === undefined) {
|
|
611
|
+
return [{ rel: entry.rel, detail: `missing ledger: ${entry.ledgerRel}` }]
|
|
612
|
+
}
|
|
613
|
+
return entry.mismatches.map((mismatch) => ({
|
|
614
|
+
rel: entry.rel,
|
|
615
|
+
detail: `${mismatch.heading === null ? 'above the first H2' : `## ${mismatch.heading}`}: ${plural(mismatch.bullets, 'bullet')}, ${plural(mismatch.rows, 'ledger row')}`,
|
|
616
|
+
}))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* A ledger present but citing nothing passed for as long as the check read the
|
|
621
|
+
* file's existence alone, so the contents are read too. A sample block shows a
|
|
622
|
+
* reader the shape of a row and cites nothing, so fences and code spans are
|
|
623
|
+
* left out.
|
|
624
|
+
*/
|
|
625
|
+
function ledgerFindings(source: SkillSource): SkillFinding[] {
|
|
626
|
+
if (source.ledger === undefined) {
|
|
627
|
+
return [{ rel: source.rel, detail: `missing ledger: ${PRACTICE_LEDGER}` }]
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const cites = bodyLines(source.ledger)
|
|
631
|
+
.filter((line) => !line.fenced)
|
|
632
|
+
.map((line) => maskCodeSpans(line.text))
|
|
633
|
+
.some((text) => EXTERNAL_LINK.test(text) || HOUSE_CHOICE.test(text))
|
|
634
|
+
if (cites) return []
|
|
635
|
+
|
|
636
|
+
return [
|
|
637
|
+
{ rel: source.rel, detail: `ledger cites nothing: ${PRACTICE_LEDGER}` },
|
|
638
|
+
]
|
|
501
639
|
}
|
|
502
640
|
|
|
503
641
|
/**
|
package/src/commands/claude.ts
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
DESCRIPTION_LIMIT,
|
|
31
31
|
LEDGER_SKILLS,
|
|
32
32
|
PRACTICE_LEDGER,
|
|
33
|
+
BULLET_LEDGER_SKILLS,
|
|
34
|
+
SKILL_LEDGERS,
|
|
33
35
|
PRACTICE_SECTIONS,
|
|
34
36
|
PRACTICE_SKILLS,
|
|
35
37
|
REQUIREMENT_LIMITS,
|
|
@@ -1768,6 +1770,9 @@ function reportPracticeShape(report: SkillsAudit): void {
|
|
|
1768
1770
|
logInfo(
|
|
1769
1771
|
`A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
|
|
1770
1772
|
)
|
|
1773
|
+
logInfo(
|
|
1774
|
+
`A skill on the per-bullet list, ${plural(BULLET_LEDGER_SKILLS.length, 'skill')} here, also matches each other reference to a ledger under ${SKILL_LEDGERS}, one row per bullet under each H2.`,
|
|
1775
|
+
)
|
|
1771
1776
|
logInfo(
|
|
1772
1777
|
`Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
|
|
1773
1778
|
)
|
package/src/commands/tooling.ts
CHANGED
|
@@ -258,7 +258,7 @@ export function register(program: Command): void {
|
|
|
258
258
|
'never overwrites one.',
|
|
259
259
|
'',
|
|
260
260
|
'Examples:',
|
|
261
|
-
' canon tooling customize e2e/
|
|
261
|
+
' canon tooling customize e2e/screenshot.ts',
|
|
262
262
|
'',
|
|
263
263
|
].join('\n'),
|
|
264
264
|
)
|
|
@@ -1014,6 +1014,9 @@ function reportPackage(result: ScanResult): void {
|
|
|
1014
1014
|
logStep('Scanning dependencies')
|
|
1015
1015
|
for (const entry of result.deps) {
|
|
1016
1016
|
if (entry.state === 'present') logInfo(entry.name)
|
|
1017
|
+
if (entry.state === 'ahead') {
|
|
1018
|
+
logInfo(`${entry.name} ${entry.installed} (ahead of ${entry.spec})`)
|
|
1019
|
+
}
|
|
1017
1020
|
}
|
|
1018
1021
|
for (const entry of result.deps) {
|
|
1019
1022
|
if (entry.state === 'missing') logAdd(entry.spec)
|
package/src/gate/measures.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
auditSkills,
|
|
13
13
|
LEDGER_SKILLS,
|
|
14
14
|
PRACTICE_LEDGER,
|
|
15
|
+
SKILL_LEDGERS,
|
|
15
16
|
PRACTICE_SKILLS,
|
|
16
17
|
SHIPPED_CORPUS,
|
|
17
18
|
} from '@/claude/skills-audit'
|
|
@@ -384,7 +385,7 @@ export const skillProvenance: Measure = async (ctx) => {
|
|
|
384
385
|
|
|
385
386
|
/**
|
|
386
387
|
* Every listed practice skill against the closing sections, and every skill on
|
|
387
|
-
* the wider ledger list against
|
|
388
|
+
* the wider ledger list against a ledger that cites something, both of which
|
|
388
389
|
* `claude/skills/create-skill/references/skill-practice.md` requires.
|
|
389
390
|
*
|
|
390
391
|
* Read in-process for the reason `skillProvenance` is, so the verb keeps its
|
|
@@ -407,14 +408,14 @@ export const skillPracticeShape: Measure = async (ctx) => {
|
|
|
407
408
|
.join(', ')
|
|
408
409
|
return {
|
|
409
410
|
emissions: [],
|
|
410
|
-
failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named to a practice skill, or the ledger at ${PRACTICE_LEDGER} to any skill on the ledger list.`,
|
|
411
|
+
failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named to a practice skill, or the ledger at ${PRACTICE_LEDGER} to any skill on the ledger list, citing one linked source or one House choice row. A reference on the per-bullet list takes one ledger row per bullet under the same H2 at ${SKILL_LEDGERS}.`,
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
return {
|
|
415
416
|
emissions: [
|
|
416
417
|
info(
|
|
417
|
-
`${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section, and ${plural(LEDGER_SKILLS.length, 'listed skill')} carry a ledger`,
|
|
418
|
+
`${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section, and ${plural(LEDGER_SKILLS.length, 'listed skill')} carry a ledger citing a source`,
|
|
418
419
|
),
|
|
419
420
|
],
|
|
420
421
|
}
|
package/src/gov/audit.ts
CHANGED
|
@@ -294,7 +294,12 @@ export function countLedgerRows(text: string): HeadingCount[] {
|
|
|
294
294
|
return [...counts].map(([key, count]) => ({ heading: key, count }))
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Pairs bullets and ledger rows under each heading, naming every heading whose
|
|
299
|
+
* counts differ. The skill audit reads a practice skill's references through
|
|
300
|
+
* it, so a rule and a reference are held to one comparison.
|
|
301
|
+
*/
|
|
302
|
+
export function compareLedger(
|
|
298
303
|
groups: readonly HeadingCount[],
|
|
299
304
|
rows: readonly HeadingCount[],
|
|
300
305
|
): LedgerMismatch[] {
|
|
@@ -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 `
|
|
30
|
+
* the reading the `ui-craft` keyboard reference 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
|
package/src/tooling/package.ts
CHANGED
|
@@ -17,7 +17,9 @@ export interface ScriptState {
|
|
|
17
17
|
export interface DepState {
|
|
18
18
|
readonly spec: string
|
|
19
19
|
readonly name: string
|
|
20
|
-
readonly state: 'missing' | 'present'
|
|
20
|
+
readonly state: 'missing' | 'present' | 'ahead'
|
|
21
|
+
/** The spec the target records, set only when it is `ahead` of the pin. */
|
|
22
|
+
readonly installed?: string
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function readPackage(path: string): PackageJson | undefined {
|
|
@@ -89,18 +91,24 @@ function firstInteger(value: string): string | undefined {
|
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
93
|
* Compares a manifest pin against what package.json records. A spec counts as
|
|
92
|
-
* missing when absent
|
|
93
|
-
*
|
|
94
|
+
* missing when absent or on an earlier major than the pin, and ahead when it
|
|
95
|
+
* sits on a later one, which a write leaves alone. A spec with no integer to
|
|
96
|
+
* compare, such as `latest` or `workspace:*`, counts as present.
|
|
94
97
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
function compareSpec(
|
|
99
|
+
installed: string | undefined,
|
|
100
|
+
pin: string,
|
|
101
|
+
): DepState['state'] {
|
|
102
|
+
if (installed === undefined) return 'missing'
|
|
103
|
+
if (pin === '') return 'present'
|
|
98
104
|
|
|
99
105
|
const pinMajor = firstInteger(pin)
|
|
100
106
|
const installedMajor = firstInteger(installed)
|
|
101
|
-
if (pinMajor === undefined) return
|
|
107
|
+
if (pinMajor === undefined || installedMajor === undefined) return 'present'
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
const delta = Number(installedMajor) - Number(pinMajor)
|
|
110
|
+
if (delta === 0) return 'present'
|
|
111
|
+
return delta > 0 ? 'ahead' : 'missing'
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
/**
|
|
@@ -121,11 +129,12 @@ export function collectDeps(
|
|
|
121
129
|
if (name === '' || seen.has(name)) continue
|
|
122
130
|
seen.add(name)
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
const state = compareSpec(installed[name], pin)
|
|
133
|
+
states.push(
|
|
134
|
+
state === 'ahead'
|
|
135
|
+
? { spec, name, state, installed: installed[name] }
|
|
136
|
+
: { spec, name, state },
|
|
137
|
+
)
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
|
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.
|