@erclx/canon 4.92.0 → 4.94.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 (39) hide show
  1. package/README.md +5 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  4. package/claude/skills/test-craft/REQUIREMENT.md +54 -0
  5. package/claude/skills/test-craft/SKILL.md +77 -0
  6. package/claude/skills/test-craft/references/adopted.md +44 -0
  7. package/claude/skills/test-craft/references/component.md +42 -0
  8. package/claude/skills/test-craft/references/e2e.md +44 -0
  9. package/claude/skills/test-craft/references/unit.md +49 -0
  10. package/claude/skills/test-first/REQUIREMENT.md +2 -0
  11. package/claude/skills/test-first/SKILL.md +4 -2
  12. package/docs/agents/capture.md +12 -5
  13. package/docs/agents/commands.md +3 -3
  14. package/docs/agents/rule-citations.md +1 -1
  15. package/docs/agents/teach.md +1 -1
  16. package/docs/workflow/ai-workflow.md +9 -8
  17. package/governance/rules/core/010-testing.md +4 -2
  18. package/governance/rules/lib/300-testing-ts.md +4 -0
  19. package/governance/rules/lib/305-e2e-reliability.md +3 -2
  20. package/governance/rules/lib/306-test-scope.md +1 -2
  21. package/governance/rules/lib/330-testing-py.md +4 -0
  22. package/governance/stacks/astro.toml +1 -1
  23. package/package.json +3 -2
  24. package/scripts/core/regen-hero.sh +4 -3
  25. package/src/capture/render.ts +15 -1
  26. package/src/claude/cases/misc.ts +5 -0
  27. package/src/cli.ts +1 -1
  28. package/src/commands/capture.ts +25 -1
  29. package/src/commands/serve.ts +11 -1
  30. package/src/design/components.ts +40 -26
  31. package/src/gate/measures.ts +5 -4
  32. package/src/gate/stages.ts +1 -1
  33. package/src/serve/static.ts +90 -6
  34. package/src/teach/fonts.ts +9 -19
  35. package/src/teach/render.ts +2 -2
  36. package/tooling/astro/configs/playwright.config.ts +2 -0
  37. package/tooling/nextjs/configs/playwright.config.ts +2 -0
  38. package/tooling/vite-react/configs/playwright.config.ts +2 -0
  39. package/src/teach/render-fixture.tsx +0 -95
@@ -1,8 +1,7 @@
1
1
  ---
2
2
  description: Enforce settled waits and falsifiable guards in end-to-end tests
3
3
  paths:
4
- - 'e2e/*.ts'
5
- - 'e2e/**/*.ts'
4
+ - '**/e2e/**/*.ts'
6
5
  ---
7
6
 
8
7
  # End-to-end reliability standards
@@ -14,6 +13,8 @@ paths:
14
13
  - Bound every settle with an explicit timeout.
15
14
  - Do not raise a timeout to clear a failure that reproduces under load. Replace the wait with a settle.
16
15
  - Do not read a value once after a pause. Poll it.
16
+ - Settle a smooth scroll on the `scrollend` event. Fall back to a stillness window only when the target was already in view.
17
+ - Run with motion reduced unless the test asserts motion, and opt back in per test.
17
18
 
18
19
  ## Falsifiable guards
19
20
 
@@ -1,8 +1,7 @@
1
1
  ---
2
2
  description: Enforce which specs and which engines an end-to-end run covers at each point in the loop
3
3
  paths:
4
- - 'e2e/*.ts'
5
- - 'e2e/**/*.ts'
4
+ - '**/e2e/**/*.ts'
6
5
  ---
7
6
 
8
7
  # Test scope standards
@@ -8,6 +8,10 @@ paths:
8
8
 
9
9
  # Python testing tooling
10
10
 
11
+ ## Layer
12
+
13
+ - Load the `canon:test-craft` skill to pick the layer a test belongs at before choosing the tooling below, and report it rather than proceeding silently when the skill does not resolve.
14
+
11
15
  ## Framework
12
16
 
13
17
  - Use `pytest` for all tests. Do not use `unittest.TestCase`.
@@ -1,2 +1,2 @@
1
1
  extends = "node"
2
- rules = ["210-astro", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "470-motion"]
2
+ rules = ["210-astro", "300-testing-ts", "305-e2e-reliability", "306-test-scope", "350-security-web", "400-ui", "410-a11y", "420-forms", "430-ux-completeness", "440-surface-capture", "450-link-behavior", "460-design-taste", "470-motion"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.92.0",
4
+ "version": "4.94.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "tsconfig.json",
20
20
  "!scripts/sandbox",
21
21
  "!scripts/eval",
22
- "!**/*.test.ts"
22
+ "!**/*.test.ts",
23
+ "!**/*.test.tsx"
23
24
  ],
24
25
  "publishConfig": {
25
26
  "access": "public"
@@ -18,10 +18,11 @@
18
18
  # Only the HTML regenerates here. The PNG beside it is a chromium render whose
19
19
  # bytes move with the browser version, so asserting it in verify.sh would fail
20
20
  # on a machine whose chromium differs rather than on a stale count. Rebuild the
21
- # images with `canon capture assets/captures --selector .window --out assets`
21
+ # images with `canon capture assets/captures --selector .window --out assets/evidence`
22
22
  # after this script reports a change. The markup and the image sit in two
23
- # folders, so that run reads the sources here and sends every PNG and stamp back
24
- # up to assets/, where the documents point. The selector has no default, since
23
+ # folders, so that run reads the sources here and sends every PNG and stamp to
24
+ # assets/evidence/, where the documents point and where `canon pr evidence`
25
+ # compares a changed image. The selector has no default, since
25
26
  # the element a capture crops to belongs to the page rather than to the command,
26
27
  # and `.window` is the class this repository's own sources declare.
27
28
  # That capture also writes a .stamp beside each PNG, which records the digest of
@@ -25,6 +25,7 @@ import { formatStamp, hashSource, stampPath } from '@/capture/stamp'
25
25
  */
26
26
 
27
27
  const DEVICE_SCALE_FACTOR = 2
28
+ const DEFAULT_VIEWPORT_HEIGHT = 720
28
29
  const FONT_PROBE_SIZE = 72
29
30
  const FONT_PROBE_TEXT = 'canon capture 0123456789'
30
31
  const ABSENT_FAMILY = '__canon_absent_family__'
@@ -32,6 +33,7 @@ const ABSENT_FAMILY = '__canon_absent_family__'
32
33
  export interface CaptureOptions {
33
34
  selector: string
34
35
  outDir?: string
36
+ width?: number
35
37
  }
36
38
 
37
39
  export type CaptureResult =
@@ -54,7 +56,9 @@ export async function captureSources(
54
56
  const browser = await chromium.launch()
55
57
  try {
56
58
  return await Promise.all(
57
- sources.map((source) => captureOne(browser, source, options.selector)),
59
+ sources.map((source) =>
60
+ captureOne(browser, source, options.selector, options.width),
61
+ ),
58
62
  )
59
63
  } finally {
60
64
  await browser.close()
@@ -69,9 +73,19 @@ async function captureOne(
69
73
  browser: Browser,
70
74
  source: CaptureSource,
71
75
  selector: string,
76
+ width: number | undefined,
72
77
  ): Promise<CaptureResult> {
78
+ /**
79
+ * A viewport rather than a wrapper, since a media query answers to the
80
+ * viewport. Absent leaves the option out so the browser default stands and
81
+ * every committed capture renders as before. The height is Playwright's own
82
+ * default, which keeps `vh` units where they were.
83
+ */
73
84
  const page = await browser.newPage({
74
85
  deviceScaleFactor: DEVICE_SCALE_FACTOR,
86
+ ...(width === undefined
87
+ ? {}
88
+ : { viewport: { width, height: DEFAULT_VIEWPORT_HEIGHT } }),
75
89
  })
76
90
  try {
77
91
  await page.goto(
@@ -54,6 +54,11 @@ export const MISC_CASES: readonly SkillCase[] = [
54
54
  "I'm about to write the retry helper we settled on and nothing covers it yet, so get the failing case in first.",
55
55
  expect: 'test-first',
56
56
  },
57
+ {
58
+ prompt:
59
+ 'The loading state already shipped. Should its test be an e2e test or a component one, and is the one I wrote any good?',
60
+ expect: 'test-craft',
61
+ },
57
62
  {
58
63
  prompt: 'Does our github about text still match what the readme says?',
59
64
  expect: 'repo-metadata',
package/src/cli.ts CHANGED
@@ -121,7 +121,7 @@ function showHelp(): void {
121
121
  `${GREY}│${NC} canon design board`,
122
122
  `${GREY}│${NC} canon slides render`,
123
123
  `${GREY}│${NC} canon slides list --json`,
124
- `${GREY}│${NC} canon capture assets/captures/install.html --selector .window --out assets`,
124
+ `${GREY}│${NC} canon capture assets/captures/install.html --selector .window --out assets/evidence`,
125
125
  `${GREY}│${NC} canon serve .canon/teach`,
126
126
  `${GREY}│${NC} canon inventory focus --json`,
127
127
  `${GREY}│${NC} canon drive http://localhost:4173 run.json --json`,
@@ -54,10 +54,14 @@ export function register(program: Command): void {
54
54
  'Output directory for a file source, or the destination PNG for a URL source; defaults beside the source',
55
55
  )
56
56
  .option('-s, --selector <selector>', 'Element to capture')
57
+ .option(
58
+ '--width <px>',
59
+ 'Viewport width in pixels, so a media query resolves at that width',
60
+ )
57
61
  .action(
58
62
  async (
59
63
  source: string,
60
- opts: { out?: string; selector?: string },
64
+ opts: { out?: string; selector?: string; width?: string },
61
65
  ): Promise<void> => {
62
66
  /**
63
67
  * Refused rather than defaulted, and refused ahead of every other
@@ -75,6 +79,15 @@ export function register(program: Command): void {
75
79
  }
76
80
  const selector = opts.selector
77
81
 
82
+ const width = parseWidth(opts.width)
83
+ if (Number.isNaN(width)) {
84
+ frameError(
85
+ `--width takes a whole number of pixels of 1 or more, got ${opts.width}.`,
86
+ )
87
+ process.exitCode = 1
88
+ return
89
+ }
90
+
78
91
  const isUrl = isUrlSource(source)
79
92
  const sourcePath = isUrl ? source : resolve(process.cwd(), source)
80
93
  if (!isUrl && !existsSync(sourcePath)) {
@@ -89,6 +102,7 @@ export function register(program: Command): void {
89
102
  const renderer = await import('@/capture/render')
90
103
  results = await renderer.captureSources(sourcePath, {
91
104
  selector,
105
+ width,
92
106
  outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
93
107
  })
94
108
  } catch (error) {
@@ -122,6 +136,16 @@ export function register(program: Command): void {
122
136
  )
123
137
  }
124
138
 
139
+ /**
140
+ * Undefined for an absent flag and `NaN` for a value that is not a positive
141
+ * integer, so the caller can tell no width from a bad one.
142
+ */
143
+ function parseWidth(raw: string | undefined): number | undefined {
144
+ if (raw === undefined) return undefined
145
+ const value = Number(raw)
146
+ return Number.isInteger(value) && value >= 1 ? value : Number.NaN
147
+ }
148
+
125
149
  /**
126
150
  * Closes an open frame around a failure that stopped the whole run rather than
127
151
  * one source. Two of them are setup states rather than defects and each names
@@ -7,6 +7,7 @@ const DEFAULT_DIR = '.'
7
7
 
8
8
  interface ServeCommandOptions {
9
9
  readonly entry?: string
10
+ readonly index?: boolean
10
11
  readonly json?: boolean
11
12
  readonly port?: string
12
13
  }
@@ -19,6 +20,10 @@ export function register(program: Command): void {
19
20
  .helpOption('-h, --help', 'Show this help message')
20
21
  .option('--port <number>', `Port to try first, default ${DEFAULT_PORT}`)
21
22
  .option('--entry <path>', 'Page the printed link opens, default index.html')
23
+ .option(
24
+ '--index',
25
+ 'List a directory that has no index.html instead of answering 404',
26
+ )
22
27
  .option('--json', 'Emit a machine-readable record on stdout')
23
28
  .addHelpText(
24
29
  'after',
@@ -39,6 +44,7 @@ export function register(program: Command): void {
39
44
  ' canon serve .canon/teach',
40
45
  ' canon serve .canon/teach --entry 03-fde-system-design/index.html',
41
46
  ' canon serve dist --port 4000 --json',
47
+ ' canon serve .canon/groundwork --index',
42
48
  '',
43
49
  ].join('\n'),
44
50
  )
@@ -61,7 +67,11 @@ async function runServe(
61
67
  )
62
68
  }
63
69
 
64
- const outcome = startServer(dir, { port, entry: opts.entry })
70
+ const outcome = startServer(dir, {
71
+ port,
72
+ entry: opts.entry,
73
+ index: opts.index,
74
+ })
65
75
  const code = report(outcome, emitJson)
66
76
  if (!outcome.ok) return code
67
77
 
@@ -225,11 +225,16 @@ const TEACH_CHROME: Component = {
225
225
  '--teach-chrome',
226
226
  '--teach-shadow',
227
227
  '--color-teach-accent-bg',
228
+ '--type-body-family',
229
+ '--type-code-family',
230
+ '--t3',
231
+ '--t4',
232
+ '--t5',
228
233
  ],
229
234
  rules: `:root {
230
- --teach-sans: 'Nunito', ui-rounded, 'Segoe UI', system-ui, sans-serif;
231
- --teach-hand: 'Virgil', 'Nunito', cursive;
232
- --teach-mono: 'Cascadia Code', ui-monospace, monospace;
235
+ --teach-sans: var(--type-body-family);
236
+ --teach-hand: 'Virgil', 'Excalifont', cursive;
237
+ --teach-mono: var(--type-code-family);
233
238
  --teach-measure: 52rem;
234
239
  --teach-chrome: 52rem;
235
240
  --teach-mast-h: 4.4rem;
@@ -268,7 +273,7 @@ body {
268
273
  background: var(--color-background);
269
274
  color: var(--color-text-body);
270
275
  font-family: var(--teach-sans);
271
- font-size: 1.125rem;
276
+ font-size: var(--t3);
272
277
  line-height: 1.62;
273
278
  -webkit-font-smoothing: antialiased;
274
279
  }
@@ -297,7 +302,7 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
297
302
  justify-content: space-between;
298
303
  align-items: center;
299
304
  gap: 1rem;
300
- font-size: 0.8125rem;
305
+ font-size: var(--t5);
301
306
  color: var(--color-muted);
302
307
  }
303
308
 
@@ -332,7 +337,7 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
332
337
  border-radius: 8px;
333
338
  text-decoration: none;
334
339
  color: var(--color-text-body);
335
- font-size: 0.9375rem;
340
+ font-size: var(--t4);
336
341
  font-weight: 600;
337
342
  line-height: 1.3;
338
343
  box-shadow: var(--teach-shadow);
@@ -370,14 +375,14 @@ main { max-width: calc(var(--teach-measure) + 3rem); margin: 0 auto; padding: 2.
370
375
  value for a character even a decorative one. */
371
376
  .crumb-sep {
372
377
  color: var(--color-muted);
373
- font-size: 0.9375rem;
378
+ font-size: var(--t4);
374
379
  user-select: none;
375
380
  }
376
381
 
377
382
  /* One arrow, on the first segment, since that is the only one that goes up. */
378
383
  .crumb-back {
379
384
  margin-right: 0.3rem;
380
- font-size: 0.9375rem;
385
+ font-size: var(--t4);
381
386
  line-height: 1;
382
387
  }
383
388
 
@@ -612,6 +617,9 @@ const TEACH_ARTICLE: Component = {
612
617
  '--color-accent',
613
618
  '--teach-hand',
614
619
  '--teach-mono',
620
+ '--t4',
621
+ '--t5',
622
+ '--t6',
615
623
  ],
616
624
  rules: `/* ---- Type ---- */
617
625
 
@@ -664,7 +672,7 @@ em { font-style: italic; }
664
672
  font-weight: 700;
665
673
  letter-spacing: 0.06em;
666
674
  text-transform: uppercase;
667
- font-size: 0.6875rem;
675
+ font-size: var(--t6);
668
676
  margin-bottom: 0.2rem;
669
677
  }
670
678
 
@@ -680,7 +688,7 @@ code {
680
688
 
681
689
  pre {
682
690
  font-family: var(--teach-mono);
683
- font-size: 0.8125rem;
691
+ font-size: var(--t5);
684
692
  line-height: 1.7;
685
693
  background: var(--color-surface);
686
694
  padding: 1.15rem 1.3rem;
@@ -745,12 +753,12 @@ tbody tr.mark td { background: var(--color-teach-accent-bg); }
745
753
  }
746
754
 
747
755
  .toc a:hover { background: var(--color-surface); }
748
- .toc .num { font-family: var(--teach-mono); font-size: 0.8125rem; color: var(--color-accent); }
756
+ .toc .num { font-family: var(--teach-mono); font-size: var(--t5); color: var(--color-accent); }
749
757
  .toc b { font-size: 1.0625rem; font-weight: 700; letter-spacing: -0.012em; }
750
758
 
751
759
  .toc .blurb {
752
760
  grid-column: 2;
753
- font-size: 0.9375rem;
761
+ font-size: var(--t4);
754
762
  line-height: 1.5;
755
763
  color: var(--color-text-secondary);
756
764
  margin-top: 0.1rem;
@@ -799,7 +807,7 @@ tbody tr.mark td { background: var(--color-teach-accent-bg); }
799
807
  ol.succ {
800
808
  margin: 0 0 2.5rem;
801
809
  padding-left: 1.35rem;
802
- font-size: 0.9375rem;
810
+ font-size: var(--t4);
803
811
  line-height: 1.6;
804
812
  color: var(--color-text-secondary);
805
813
  }
@@ -846,13 +854,13 @@ ol.succ li { margin-bottom: 0.4rem; }
846
854
 
847
855
  .road-n {
848
856
  font-family: var(--teach-mono);
849
- font-size: 0.8125rem;
857
+ font-size: var(--t5);
850
858
  color: var(--color-accent);
851
859
  }
852
860
 
853
861
  .road-b { display: block; }
854
862
  .road-b b { display: block; margin-bottom: 0.2rem; }
855
- .road-b span { color: var(--color-text-secondary); font-size: 0.9375rem; line-height: 1.55; }
863
+ .road-b span { color: var(--color-text-secondary); font-size: var(--t4); line-height: 1.55; }
856
864
 
857
865
  .road-t {
858
866
  font-family: var(--teach-mono);
@@ -889,6 +897,8 @@ const TEACH_QUIZ: Component = {
889
897
  '--teach-mono',
890
898
  '--teach-shadow',
891
899
  '--color-teach-accent-bg',
900
+ '--t4',
901
+ '--t5',
892
902
  ],
893
903
  rules: `/* ---- Quiz ---- */
894
904
 
@@ -907,7 +917,7 @@ const TEACH_QUIZ: Component = {
907
917
  margin-bottom: 0.45rem;
908
918
  cursor: pointer;
909
919
  font-family: var(--teach-sans);
910
- font-size: 0.9375rem;
920
+ font-size: var(--t4);
911
921
  background: transparent;
912
922
  width: 100%;
913
923
  text-align: left;
@@ -967,7 +977,7 @@ const TEACH_QUIZ: Component = {
967
977
 
968
978
  .fb {
969
979
  display: none;
970
- font-size: 0.9375rem;
980
+ font-size: var(--t4);
971
981
  line-height: 1.55;
972
982
  color: var(--color-text-secondary);
973
983
  background: var(--color-surface);
@@ -983,7 +993,7 @@ footer {
983
993
  margin-top: 4rem;
984
994
  padding-top: 1.4rem;
985
995
  border-top: 1px solid var(--color-border);
986
- font-size: 0.8125rem;
996
+ font-size: var(--t5);
987
997
  line-height: 1.6;
988
998
  color: var(--color-muted);
989
999
  }`,
@@ -1004,6 +1014,8 @@ const TEACH_GLOSSARY: Component = {
1004
1014
  '--color-accent',
1005
1015
  '--teach-sans',
1006
1016
  '--teach-mono',
1017
+ '--t4',
1018
+ '--t5',
1007
1019
  ],
1008
1020
  rules: `/* ---- Glossary view, generated from the markdown source ---- */
1009
1021
 
@@ -1022,7 +1034,7 @@ h2 .count {
1022
1034
  .filter {
1023
1035
  width: 100%;
1024
1036
  font-family: var(--teach-sans);
1025
- font-size: 0.9375rem;
1037
+ font-size: var(--t4);
1026
1038
  color: var(--color-text-body);
1027
1039
  background: var(--color-background);
1028
1040
  border: 1px solid var(--color-border);
@@ -1064,7 +1076,7 @@ h2 .count {
1064
1076
  display: none;
1065
1077
  padding: 1.2rem 0;
1066
1078
  color: var(--color-muted);
1067
- font-size: 0.9375rem;
1079
+ font-size: var(--t4);
1068
1080
  }
1069
1081
 
1070
1082
  .gloss.none .empty { display: block; }
@@ -1072,14 +1084,14 @@ h2 .count {
1072
1084
  .gterm {
1073
1085
  padding: 0.7rem 0;
1074
1086
  border-bottom: 1px solid var(--color-border);
1075
- font-size: 0.9375rem;
1087
+ font-size: var(--t4);
1076
1088
  line-height: 1.55;
1077
1089
  }
1078
1090
 
1079
1091
  .gloss-group {
1080
1092
  margin: 1.1rem 0 0.4rem;
1081
1093
  font-family: var(--teach-sans);
1082
- font-size: 0.8125rem;
1094
+ font-size: var(--t5);
1083
1095
  font-weight: 600;
1084
1096
  color: var(--color-muted);
1085
1097
  text-transform: uppercase;
@@ -1101,7 +1113,7 @@ h2 .count {
1101
1113
  ol.succ {
1102
1114
  margin: 0 0 2.5rem;
1103
1115
  padding-left: 1.35rem;
1104
- font-size: 0.9375rem;
1116
+ font-size: var(--t4);
1105
1117
  line-height: 1.6;
1106
1118
  color: var(--color-text-secondary);
1107
1119
  }
@@ -1137,6 +1149,7 @@ const TEACH_OUTLINE: Component = {
1137
1149
  '--teach-sans',
1138
1150
  '--teach-measure',
1139
1151
  '--teach-mast-h',
1152
+ '--t5',
1140
1153
  ],
1141
1154
  rules: `/* ---- In-lesson outline, in the gutter the measure leaves over ---- */
1142
1155
 
@@ -1151,7 +1164,7 @@ const TEACH_OUTLINE: Component = {
1151
1164
  width: calc(13rem + 8px);
1152
1165
  max-height: calc(100vh - var(--teach-mast-h) - 4rem);
1153
1166
  overflow-y: auto;
1154
- font-size: 0.8125rem;
1167
+ font-size: var(--t5);
1155
1168
  line-height: 1.45;
1156
1169
  }
1157
1170
 
@@ -1218,12 +1231,13 @@ const TEACH_REFERENCES: Component = {
1218
1231
  '--color-accent',
1219
1232
  '--teach-mono',
1220
1233
  '--color-teach-accent-bg',
1234
+ '--t5',
1221
1235
  ],
1222
1236
  rules: `footer {
1223
1237
  margin-top: 4rem;
1224
1238
  padding-top: 1.4rem;
1225
1239
  border-top: 1px solid var(--color-border);
1226
- font-size: 0.8125rem;
1240
+ font-size: var(--t5);
1227
1241
  line-height: 1.6;
1228
1242
  color: var(--color-muted);
1229
1243
  }
@@ -1252,7 +1266,7 @@ ol.refs {
1252
1266
  counter-reset: r;
1253
1267
  padding: 0;
1254
1268
  margin: 0.9rem 0 1.1rem;
1255
- font-size: 0.8125rem;
1269
+ font-size: var(--t5);
1256
1270
  line-height: 1.55;
1257
1271
  }
1258
1272
 
@@ -142,7 +142,7 @@ export function auditsBaselineRel(root: string): string {
142
142
  }
143
143
 
144
144
  export const CAPTURE_STAMP_FAILURE =
145
- 'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets and commit each frame with its image and its stamp.'
145
+ 'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets/evidence and commit each frame with its image and its stamp.'
146
146
 
147
147
  function parseJson(payload: string): unknown {
148
148
  try {
@@ -1118,11 +1118,12 @@ async function collectPluginManifests(ctx: MeasureContext): Promise<string[]> {
1118
1118
  /**
1119
1119
  * A capture set spans two folders. The markup and the template beside it are
1120
1120
  * the authored half and sit under `assets/captures/`, while the image a
1121
- * document points at and the stamp answering for it stay in `assets/`, which is
1122
- * where `--out assets` sends them.
1121
+ * document points at and the stamp answering for it sit under
1122
+ * `assets/evidence/`, which is where `--out assets/evidence` sends them and where
1123
+ * `canon pr evidence` looks for a changed image.
1123
1124
  */
1124
1125
  const CAPTURE_MARKUP_DIR = 'assets/captures'
1125
- const CAPTURE_OUTPUT_DIR = 'assets'
1126
+ const CAPTURE_OUTPUT_DIR = 'assets/evidence'
1126
1127
 
1127
1128
  /**
1128
1129
  * Every capture under `assets/captures/`, named by the base its three files
@@ -201,7 +201,7 @@ export const STAGES: readonly Stage[] = [
201
201
  kind: 'drift',
202
202
  pathspec: 'assets/captures/*.html',
203
203
  failure:
204
- 'A generated frame drifted from the catalogs or the design source. Run bun run check, then canon capture assets/captures --selector .window --out assets, and commit each assets/captures/*.html with its .png and .stamp.',
204
+ 'A generated frame drifted from the catalogs or the design source. Run bun run check, then canon capture assets/captures --selector .window --out assets/evidence, and commit each assets/captures/*.html with its .png and .stamp.',
205
205
  },
206
206
  { kind: 'measure', measure: captureStamps },
207
207
  ],