@erclx/canon 5.53.0 → 5.54.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 (94) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +10 -9
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -5
  5. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  6. package/claude/skills/design-identity/SKILL.md +6 -6
  7. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  8. package/claude/skills/design-record/SKILL.md +4 -4
  9. package/claude/skills/design-record/references/folder.md +1 -1
  10. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  11. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  12. package/claude/skills/draw-figure/SKILL.md +1 -1
  13. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  14. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  15. package/claude/skills/pick-by-eye/references/board.md +42 -0
  16. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  17. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  18. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/teach-craft/SKILL.md +32 -1
  20. package/claude/skills/teach-craft/references/adopted.md +113 -0
  21. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  22. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  23. package/docs/agents/install-and-sync.md +0 -8
  24. package/docs/target/migrations.md +8 -3
  25. package/docs/target/projects.md +1 -1
  26. package/docs/workflow/skill-map.md +0 -1
  27. package/docs/workflow/visual-design-workflow.md +1 -4
  28. package/governance/detect.toml +1 -6
  29. package/governance/renames.toml +0 -1
  30. package/package.json +2 -5
  31. package/src/claude/cases/misc.ts +0 -5
  32. package/src/claude/skills-audit.ts +1 -0
  33. package/src/cli/help.ts +0 -3
  34. package/src/cli.ts +0 -2
  35. package/src/commands/design.ts +16 -315
  36. package/src/commands/sync.ts +4 -5
  37. package/src/design/regen.ts +5 -32
  38. package/src/design/render.ts +1 -1
  39. package/src/design/tokens.ts +3 -2
  40. package/src/gate/stages.ts +4 -10
  41. package/src/gov/audit.ts +1 -6
  42. package/src/markdown/scope.ts +2 -4
  43. package/src/shipped/references.ts +1 -2
  44. package/src/sync/check.ts +4 -29
  45. package/src/sync/stamp.ts +6 -6
  46. package/src/sync/target.ts +1 -4
  47. package/tooling/claude/reference.md +1 -1
  48. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  49. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  50. package/claude/skills/canvas-board/SKILL.md +0 -143
  51. package/claude/skills/canvas-board/references/canvas.md +0 -170
  52. package/governance/rules/ui/canvas.md +0 -13
  53. package/src/canvas/address.ts +0 -227
  54. package/src/canvas/arrange.ts +0 -66
  55. package/src/canvas/capture.ts +0 -391
  56. package/src/canvas/client/app.tsx +0 -188
  57. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  58. package/src/canvas/client/inspector/color-field.tsx +0 -398
  59. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  60. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  61. package/src/canvas/client/inspector/color-space.ts +0 -240
  62. package/src/canvas/client/inspector/field.tsx +0 -263
  63. package/src/canvas/client/inspector/fields.css +0 -763
  64. package/src/canvas/client/inspector/section.tsx +0 -27
  65. package/src/canvas/client/inspector/size-field.tsx +0 -69
  66. package/src/canvas/client/inspector/token-select.tsx +0 -109
  67. package/src/canvas/client/inspector/values.ts +0 -297
  68. package/src/canvas/client/inspector.tsx +0 -692
  69. package/src/canvas/client/keys.ts +0 -71
  70. package/src/canvas/client/layers.tsx +0 -118
  71. package/src/canvas/client/main.tsx +0 -71
  72. package/src/canvas/client/pages-panel.tsx +0 -212
  73. package/src/canvas/client/panel-handle.tsx +0 -90
  74. package/src/canvas/client/selection.tsx +0 -199
  75. package/src/canvas/client/shell.css +0 -762
  76. package/src/canvas/client/state.ts +0 -792
  77. package/src/canvas/client/surface.tsx +0 -803
  78. package/src/canvas/client/theme-panel.tsx +0 -55
  79. package/src/canvas/client/tools.tsx +0 -153
  80. package/src/canvas/content.ts +0 -781
  81. package/src/canvas/edit.ts +0 -575
  82. package/src/canvas/editing.ts +0 -203
  83. package/src/canvas/fit.ts +0 -238
  84. package/src/canvas/global.d.ts +0 -15
  85. package/src/canvas/history.ts +0 -116
  86. package/src/canvas/index.html +0 -20
  87. package/src/canvas/server.ts +0 -766
  88. package/src/canvas/tokens.ts +0 -221
  89. package/src/commands/canvas.ts +0 -1097
  90. package/src/design/adapter.ts +0 -59
  91. package/src/design/base.css +0 -203
  92. package/src/design/brand-favicon.ts +0 -18
  93. package/src/design/neutral.ts +0 -127
  94. package/src/design/project-fonts.ts +0 -392
@@ -1,53 +1,30 @@
1
- import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
2
- import { basename, join, relative, resolve } from 'node:path'
1
+ import { existsSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import {
5
- createDesignAdapter,
6
- DESIGN_INSTALL_DIR,
7
- DESIGN_PROJECT_SUBDIR,
8
- } from '@/design/adapter'
9
4
  import { buildDesignCss } from '@/design/css'
10
5
  import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
11
- import { addProjectFace, listProjectFaces } from '@/design/project-fonts'
12
- import { renderDesignDoc } from '@/design/render'
13
- import { DESIGN_BASE_CSS, DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
6
+ import { DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
14
7
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
15
- import { creationRel, SCRATCH } from '@/roots/record'
16
- import { surfaceDir } from '@/roots/surface'
17
- import { recordStamp, runDomainSync } from '@/sync/engine'
18
- import { isDirectory, resolveTarget } from '@/targets/validate'
19
- import {
20
- intro,
21
- logAdd,
22
- logError,
23
- logInfo,
24
- logWarn,
25
- outro,
26
- palette,
27
- } from '@/cli/ui'
8
+ import { logWarn, palette } from '@/cli/ui'
28
9
 
29
10
  export function register(program: Command): void {
30
11
  const design = program
31
12
  .command('design')
32
13
  .helpOption('-h, --help', 'Show this help message')
33
- .description(
34
- 'Design system commands (regen, css, render, install, sync, fonts)',
35
- )
14
+ .description('Design system commands (regen, css)')
36
15
 
37
16
  design
38
17
  .command('regen')
39
18
  .helpOption('-h, --help', 'Show this help message')
40
- .description(
41
- 'Rewrite canon/DESIGN.md and the base stylesheet from src/design/tokens.ts',
42
- )
19
+ .description('Rewrite canon/DESIGN.md from src/design/tokens.ts')
43
20
  .addHelpText(
44
21
  'after',
45
22
  [
46
23
  '',
47
- 'Unlike install and sync, this runs against the toolkit checkout rather',
48
- 'than a target. It rewrites the record and the base stylesheet from the',
49
- 'token module and refuses where the record is absent, which is what the',
50
- 'CLI installed into a project as a dependency looks like.',
24
+ 'This runs against the toolkit checkout rather than a target. It',
25
+ 'rewrites the record from the token module and refuses where the record',
26
+ 'is absent, which is what the CLI installed into a project as a',
27
+ 'dependency looks like.',
51
28
  '',
52
29
  ].join('\n'),
53
30
  )
@@ -60,10 +37,10 @@ export function register(program: Command): void {
60
37
  // so each emits it after its own opener rather than ahead of the branch.
61
38
  const mismatch = checkoutMismatchWarning(process.cwd())
62
39
 
63
- // Both outputs resolve from `PROJECT_ROOT`, which is the installed
40
+ // The output resolves from `PROJECT_ROOT`, which is the installed
64
41
  // package directory when the CLI runs out of a target's `node_modules`.
65
- // The record is the one output that is already committed here and ships
66
- // with no package, so its absence is what separates the two.
42
+ // The record is already committed here and ships with no package, so
43
+ // its absence is what separates the two.
67
44
  if (!existsSync(join(PROJECT_ROOT, DESIGN_DOCUMENT))) {
68
45
  process.stderr.write(`${GREY}┌${NC}\n`)
69
46
  if (mismatch !== undefined) logWarn(mismatch)
@@ -79,11 +56,9 @@ export function register(program: Command): void {
79
56
  )
80
57
  if (mismatch !== undefined) logWarn(mismatch)
81
58
  const result = regenDesign(PROJECT_ROOT)
82
- for (const path of [result.documentPath, result.cssPath]) {
83
- process.stderr.write(
84
- `${GREY}│${NC} ${GREEN}✓${NC} ${relative(PROJECT_ROOT, path)}\n`,
85
- )
86
- }
59
+ process.stderr.write(
60
+ `${GREY}│${NC} ${GREEN}✓${NC} ${relative(PROJECT_ROOT, result.documentPath)}\n`,
61
+ )
87
62
  process.stderr.write(`${GREY}└${NC}\n`)
88
63
  })
89
64
 
@@ -107,278 +82,4 @@ export function register(program: Command): void {
107
82
  }),
108
83
  )
109
84
  })
110
-
111
- design
112
- .command('render')
113
- .helpOption('-h, --help', 'Show this help message')
114
- .description('Render DESIGN.md tokens to HTML and CSS preview')
115
- .option(
116
- '-s, --source <path>',
117
- 'Source DESIGN.md path',
118
- relative(process.cwd(), surfaceDir(process.cwd(), 'DESIGN.md')),
119
- )
120
- .option(
121
- '-o, --out <path>',
122
- 'Output directory',
123
- creationRel(process.cwd(), SCRATCH, 'render', 'design'),
124
- )
125
- .option(
126
- '--embed-fonts',
127
- 'Declare each vendored face the typography table names, and set the page body in the body stack',
128
- )
129
- .action((opts: { source: string; out: string; embedFonts?: boolean }) => {
130
- const sourcePath = resolve(process.cwd(), opts.source)
131
- const outDir = resolve(process.cwd(), opts.out)
132
- const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
133
- if (!existsSync(sourcePath)) {
134
- process.stderr.write(
135
- `${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${opts.source} not found\n${GREY}└${NC}\n`,
136
- )
137
- process.exitCode = 1
138
- return
139
- }
140
- process.stderr.write(
141
- `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Render design tokens${NC}\n`,
142
- )
143
- const result = renderDesignDoc(sourcePath, outDir, {
144
- embedFonts: opts.embedFonts,
145
- })
146
- process.stderr.write(
147
- `${GREY}│${NC} ${GREEN}✓${NC} ${result.htmlPath}\n${GREY}│${NC} ${GREEN}✓${NC} ${result.cssPath}\n${GREY}└${NC}\n`,
148
- )
149
- })
150
-
151
- design
152
- .command('install')
153
- .description('Install the base stylesheet into a project')
154
- .argument('[target]', 'Target directory', '.')
155
- .helpOption('-h, --help', 'Show this help message')
156
- .addHelpText(
157
- 'after',
158
- [
159
- '',
160
- 'Installs one toolkit-owned file and creates no override. A project',
161
- `overrides a value by writing ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/`,
162
- 'itself, which sync never touches. Nothing arrives on a project that',
163
- 'has not run this, so a design value reaches a target on an install',
164
- 'rather than on the next sync. A base stylesheet already installed is',
165
- 'replaced.',
166
- '',
167
- 'Exit codes:',
168
- ' 0 the stylesheet was installed',
169
- ' 1 the target is missing or is the toolkit root, or the toolkit',
170
- ' source is absent',
171
- '',
172
- ].join('\n'),
173
- )
174
- .action(async (target: string) => {
175
- process.exitCode = await runInstall(target)
176
- })
177
-
178
- design
179
- .command('sync')
180
- .description(
181
- 'Update the base stylesheet already installed under .claude/design/',
182
- )
183
- .argument('[target]', 'Target directory', '.')
184
- .helpOption('-h, --help', 'Show this help message')
185
- .addHelpText(
186
- 'after',
187
- [
188
- '',
189
- 'Replaces the shipped design files under .claude/design/ with the',
190
- 'toolkit copy, edited or not, and leaves',
191
- `${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/ alone. It deletes nothing it finds no`,
192
- 'source for.',
193
- '',
194
- 'Exit codes:',
195
- ' 0 synced, already up to date, or the prompt was cancelled',
196
- ' 1 the target is missing or is the toolkit root',
197
- '',
198
- ].join('\n'),
199
- )
200
- .action(async (target: string) => {
201
- process.exitCode = await runDomainSync(
202
- createDesignAdapter(PROJECT_ROOT),
203
- target,
204
- { protectedRoot: PROJECT_ROOT },
205
- )
206
- })
207
-
208
- registerFonts(design)
209
- }
210
-
211
- const FONTS_DIR = join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, 'fonts')
212
-
213
- interface FontsRefused {
214
- readonly ok: false
215
- readonly reason: string
216
- readonly detail: string
217
- }
218
-
219
- function writeJson(record: unknown): void {
220
- process.stdout.write(`${JSON.stringify(record)}\n`)
221
- }
222
-
223
- function refuseFonts(
224
- title: string,
225
- refusal: FontsRefused,
226
- emitJson: boolean,
227
- ): number {
228
- if (emitJson) writeJson(refusal)
229
- intro(title)
230
- logError(refusal.detail)
231
- outro()
232
- return 1
233
- }
234
-
235
- /** A target that is a directory, or the refusal naming the path given. */
236
- function fontsTarget(target: string): string | FontsRefused {
237
- const resolved = resolve(target)
238
- if (!isDirectory(resolved)) {
239
- return {
240
- ok: false,
241
- reason: 'no-target',
242
- detail: `Target directory not found: ${target}`,
243
- }
244
- }
245
- return resolved
246
- }
247
-
248
- function registerFonts(design: Command): void {
249
- const fonts = design
250
- .command('fonts')
251
- .helpOption('-h, --help', 'Show this help message')
252
- .description(`Read and add the project's own faces under ${FONTS_DIR}/`)
253
- .addHelpText(
254
- 'after',
255
- [
256
- '',
257
- `Faces are declared as @font-face rules in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/fonts.css,`,
258
- 'which the canvas embeds into every frame. Sync never writes the folder.',
259
- '',
260
- ].join('\n'),
261
- )
262
-
263
- fonts
264
- .command('list')
265
- .helpOption('-h, --help', 'Show this help message')
266
- .description('List each face the project override sheets declare')
267
- .argument('[target]', 'Target directory', '.')
268
- .option('--json', 'Emit the record on stdout')
269
- .action((target: string, opts: { json?: boolean }) => {
270
- const title = 'canon design fonts list'
271
- const resolved = fontsTarget(target)
272
- if (typeof resolved !== 'string') {
273
- process.exitCode = refuseFonts(title, resolved, Boolean(opts.json))
274
- return
275
- }
276
- const faces = listProjectFaces(resolved)
277
- if (opts.json) writeJson({ ok: true, faces })
278
- intro(title)
279
- if (faces.length === 0) logInfo(`No faces declared under ${FONTS_DIR}/`)
280
- for (const face of faces) {
281
- const line = `${face.family} ${face.weight} ${face.style}: ${face.file} in ${face.sheet}`
282
- if (face.present) logInfo(line)
283
- else logWarn(`${line} (${face.problem ?? 'missing'})`)
284
- }
285
- outro()
286
- })
287
-
288
- fonts
289
- .command('add')
290
- .helpOption('-h, --help', 'Show this help message')
291
- .description(
292
- `Copy a woff2, woff, ttf, or otf face into ${FONTS_DIR}/ and declare it`,
293
- )
294
- .argument('<file>', 'Font file to copy')
295
- .argument('[target]', 'Target directory', '.')
296
- .requiredOption('--family <name>', 'Family name the face declares')
297
- .option('--weight <n>', "Weight, or a range such as '100 900'", '400')
298
- .option('--style <style>', 'normal or italic', 'normal')
299
- .option('--json', 'Emit the record on stdout')
300
- .action(
301
- (
302
- file: string,
303
- target: string,
304
- opts: { family: string; weight: string; style: string; json?: boolean },
305
- ) => {
306
- const title = 'canon design fonts add'
307
- const emitJson = Boolean(opts.json)
308
- const resolved = fontsTarget(target)
309
- if (typeof resolved !== 'string') {
310
- process.exitCode = refuseFonts(title, resolved, emitJson)
311
- return
312
- }
313
- if (resolved === PROJECT_ROOT) {
314
- process.exitCode = refuseFonts(
315
- title,
316
- {
317
- ok: false,
318
- reason: 'toolkit-root',
319
- detail:
320
- 'Cannot run against toolkit root. Files here are the source of truth.',
321
- },
322
- emitJson,
323
- )
324
- return
325
- }
326
- const outcome = addProjectFace(resolved, resolve(file), {
327
- family: opts.family,
328
- weight: opts.weight,
329
- style: opts.style,
330
- })
331
- if (!outcome.ok) {
332
- process.exitCode = refuseFonts(title, outcome, emitJson)
333
- return
334
- }
335
- if (emitJson) writeJson(outcome)
336
- intro(title)
337
- logAdd(join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, outcome.file))
338
- logInfo(
339
- `Declared ${outcome.family} ${outcome.weight} ${outcome.style} in ${outcome.sheet}`,
340
- )
341
- outro()
342
- },
343
- )
344
- }
345
-
346
- /**
347
- * Copies the base and stops. The override folder is deliberately not created:
348
- * an empty override is a file the project did not ask for and did not write,
349
- * the three-way merge already handles a missing side, and an empty file invites
350
- * a target to fill it before it has an opinion.
351
- */
352
- async function runInstall(target: string): Promise<number> {
353
- intro('canon design install')
354
-
355
- const resolved = resolveTarget(target, PROJECT_ROOT)
356
- if (typeof resolved === 'number') {
357
- outro()
358
- return resolved
359
- }
360
-
361
- const source = join(PROJECT_ROOT, DESIGN_BASE_CSS)
362
- if (!existsSync(source)) {
363
- logError(
364
- `No base stylesheet at ${DESIGN_BASE_CSS}. Run canon design regen.`,
365
- )
366
- outro()
367
- return 1
368
- }
369
-
370
- const dir = join(resolved, DESIGN_INSTALL_DIR)
371
- mkdirSync(dir, { recursive: true })
372
-
373
- const dest = join(dir, basename(source))
374
- copyFileSync(source, dest)
375
- logAdd(relative(resolved, dest))
376
-
377
- await recordStamp(createDesignAdapter(PROJECT_ROOT), resolved, new Date())
378
-
379
- logInfo(
380
- `Override a value in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/, which sync leaves alone.`,
381
- )
382
- outro()
383
- return 0
384
85
  }
@@ -35,7 +35,6 @@ import { describeSkew } from '@/version/skew'
35
35
 
36
36
  const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
37
37
  governance: ['gov', 'sync'],
38
- design: ['design', 'sync'],
39
38
  claude: ['claude', 'sync'],
40
39
  }
41
40
 
@@ -58,10 +57,10 @@ export function register(program: Command): void {
58
57
  'after',
59
58
  [
60
59
  '',
61
- 'Runs `canon gov sync`, `canon design sync`, and `canon claude sync`',
62
- 'on each domain installed, so each carries its own replace behavior:',
63
- 'governance replaces an edited rule and deletes a retired one, design',
64
- 'replaces its shipped files, and claude writes .gitignore alone.',
60
+ 'Runs `canon gov sync` and `canon claude sync` on each domain',
61
+ 'installed, so each carries its own replace behavior: governance',
62
+ 'replaces an edited rule and deletes a retired one, and claude writes',
63
+ '.gitignore alone.',
65
64
  'Tooling is not synced. Refuses a dirty working tree.',
66
65
  '',
67
66
  'Exit codes:',
@@ -1,42 +1,20 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
- import { buildDesignCss } from '@/design/css'
4
3
  import { renderDesignDocument } from '@/design/document'
5
- import { NEUTRAL_TOKENS } from '@/design/neutral'
6
4
 
7
5
  /**
8
- * The two artifacts the token sources produce, written by `canon design regen`
9
- * and asserted for drift by the `design` stage of `bun run check`. The
10
- * document renders from `TOKENS` and the stylesheet from `NEUTRAL_TOKENS`.
11
- *
12
- * The document is what a person reads and the stylesheet is what a target
13
- * installs, and neither is edited by hand. That is the cost the operator took
14
- * on when the tokens moved into a module: two artifacts where the record used
15
- * to be one, and a regen step that has to run or both go stale silently. The
16
- * gate stage is what keeps silently out of it.
6
+ * The record the token module renders, written by `canon design regen` and
7
+ * asserted for drift by the `design` stage of `bun run check`. It is never
8
+ * edited by hand. That is the cost the operator took on when the tokens moved
9
+ * into a module: a regen step that has to run or the record goes stale
10
+ * silently. The gate stage is what keeps silently out of it.
17
11
  */
18
12
 
19
13
  /** The rendered record, at the path `claude/skills/design-record/references/design.md` fixes for it. */
20
14
  export const DESIGN_DOCUMENT = join('canon', 'DESIGN.md')
21
15
 
22
- /**
23
- * The base stylesheet a target installs. It sits inside `src/` because that is
24
- * the folder the published package ships, and beside the module that writes it
25
- * because a generated file kept away from its generator is one nobody thinks to
26
- * regenerate.
27
- */
28
- export const DESIGN_BASE_CSS = join('src', 'design', 'base.css')
29
-
30
- const BASE_BANNER = [
31
- 'Generated by `canon design regen` from src/design/neutral.ts. Do not edit.',
32
- 'This is the base half of a design install. A target overrides a value in',
33
- '.claude/design/project/, which the three-way merge leaves alone, so this',
34
- 'file changes freely and nothing a project wrote lives in it.',
35
- ].join('\n ')
36
-
37
16
  export interface RegenResult {
38
17
  readonly documentPath: string
39
- readonly cssPath: string
40
18
  }
41
19
 
42
20
  function write(root: string, rel: string, body: string): string {
@@ -49,10 +27,5 @@ function write(root: string, rel: string, body: string): string {
49
27
  export function regenDesign(root: string): RegenResult {
50
28
  return {
51
29
  documentPath: write(root, DESIGN_DOCUMENT, renderDesignDocument()),
52
- cssPath: write(
53
- root,
54
- DESIGN_BASE_CSS,
55
- buildDesignCss(NEUTRAL_TOKENS, { banner: BASE_BANNER }),
56
- ),
57
30
  }
58
31
  }
@@ -259,7 +259,7 @@ ${previewChrome()}
259
259
  </head>
260
260
  <body>
261
261
  <h1>Design tokens</h1>
262
- <p class="note">Generated from <code>canon/DESIGN.md</code> by <code>canon design render</code>. Token preview only, not a screen mock.</p>${summary}
262
+ <p class="note">Generated from <code>canon/DESIGN.md</code> by <code>bun run web:previews</code>. Token preview only, not a screen mock.</p>${summary}
263
263
  ${sections.join('\n')}
264
264
  </body>
265
265
  </html>
@@ -84,8 +84,9 @@ const MONO = 'Noto Sans Mono, DejaVu Sans Mono, monospace'
84
84
  * The proportional stack every role but `code` declares. Geist is the face the
85
85
  * visual-direction track set throughout, embedded through `FONT_FACES`.
86
86
  *
87
- * The stack stops at two names and a generic because `src/design/base.css` is
88
- * written by `canon design regen` and formatted by prettier, and a declaration
87
+ * The stack stops at two names and a generic because
88
+ * `web/src/styles/tokens.css` is written by `bun run web:tokens` and formatted
89
+ * by oxfmt, and a declaration
89
90
  * past 80 columns is wrapped by the second and flattened by the first. Every
90
91
  * emitted line stays inside the width so the two writers never disagree.
91
92
  */
@@ -212,10 +212,10 @@ export const STAGES: readonly Stage[] = [
212
212
  },
213
213
  {
214
214
  // `canon/DESIGN.md`, the web stylesheet, and the tab icon are written
215
- // from `src/design/tokens.ts`, the base stylesheet from
216
- // `src/design/neutral.ts`, and none is edited by hand. Four artifacts
217
- // from two token sources is the cost of the token move, and a render step that has to run is only safe while something fails
218
- // when it did not, which is this. The record and the worked example are
215
+ // from `src/design/tokens.ts`, and none is edited by hand. Three
216
+ // artifacts from one token source is the cost of the token move, and a
217
+ // render step that has to run is only safe while something fails when it
218
+ // did not, which is this. The record and the worked example are
219
219
  // then counted against the prose budget, which the drift asserts cannot
220
220
  // see since a regenerated paragraph agrees with its source.
221
221
  id: 'design',
@@ -232,12 +232,6 @@ export const STAGES: readonly Stage[] = [
232
232
  failure:
233
233
  'The design record drifted from the token source. Run bun run check and commit canon/DESIGN.md.',
234
234
  },
235
- {
236
- kind: 'drift',
237
- pathspec: 'src/design/base.css',
238
- failure:
239
- 'The base stylesheet drifted from the token source. Run bun run check and commit src/design/base.css.',
240
- },
241
235
  {
242
236
  kind: 'command',
243
237
  argv: ['bun', 'run', 'web:tokens'],
package/src/gov/audit.ts CHANGED
@@ -45,13 +45,8 @@ export const RULE_GATED_FOLDERS: readonly string[] = [
45
45
  /**
46
46
  * Rule files in a gated folder whose findings `--gate` still passes, one path
47
47
  * each and never a folder, so a new rule beside one is gated the day it lands.
48
- *
49
- * `ui/canvas.md` carries a shape finding and no ledger, and v149.2 removes the
50
- * rule, so this entry goes with it.
51
48
  */
52
- export const RULE_GATE_EXEMPT: readonly string[] = [
53
- join('governance', 'rules', 'ui', 'canvas.md'),
54
- ]
49
+ export const RULE_GATE_EXEMPT: readonly string[] = []
55
50
 
56
51
  /** The folders whose rules claim outside practice, so each carries a ledger. */
57
52
  export const PRACTICE_FOLDERS: readonly string[] = [
@@ -136,9 +136,6 @@ export const GATED_PATHS: readonly string[] = [
136
136
  'claude/skills/canon-feedback/REQUIREMENT.md',
137
137
  'claude/skills/canon-feedback/SKILL.md',
138
138
  'claude/skills/canon-feedback/references/template.md',
139
- 'claude/skills/canvas-board/REQUIREMENT.md',
140
- 'claude/skills/canvas-board/SKILL.md',
141
- 'claude/skills/canvas-board/references/canvas.md',
142
139
  'claude/skills/ci-workflow/REQUIREMENT.md',
143
140
  'claude/skills/ci-workflow/SKILL.md',
144
141
  'claude/skills/ci-workflow/references/workflows.md',
@@ -203,6 +200,7 @@ export const GATED_PATHS: readonly string[] = [
203
200
  'claude/skills/document-repair/references/batches.md',
204
201
  'claude/skills/pick-by-eye/REQUIREMENT.md',
205
202
  'claude/skills/pick-by-eye/SKILL.md',
203
+ 'claude/skills/pick-by-eye/references/board.md',
206
204
  'claude/skills/pick-by-eye/references/live-arms.md',
207
205
  'claude/skills/write-doc/REQUIREMENT.md',
208
206
  'claude/skills/write-doc/SKILL.md',
@@ -375,6 +373,7 @@ export const GATED_PATHS: readonly string[] = [
375
373
  'claude/skills/task-complete/references/unusable-baseline.md',
376
374
  'claude/skills/teach-craft/REQUIREMENT.md',
377
375
  'claude/skills/teach-craft/SKILL.md',
376
+ 'claude/skills/teach-craft/references/adopted.md',
378
377
  'claude/skills/teach-craft/references/lesson-craft.md',
379
378
  'claude/skills/teach-craft/references/pedagogy.md',
380
379
  'claude/skills/test-craft/REQUIREMENT.md',
@@ -399,7 +398,6 @@ export const GATED_PATHS: readonly string[] = [
399
398
  'claude/skills/write-human/references/machine-tells.md',
400
399
  'claude/skills/youtube-transcripts/REQUIREMENT.md',
401
400
  'claude/skills/youtube-transcripts/SKILL.md',
402
- '.claude/skills/internal-canvas/REQUIREMENT.md',
403
401
  '.claude/skills/internal-claude/REQUIREMENT.md',
404
402
  '.claude/skills/internal-docs/REQUIREMENT.md',
405
403
  '.claude/skills/internal-governance/REQUIREMENT.md',
@@ -15,8 +15,7 @@ import { isMarked } from '@/sweep/exempt-marker'
15
15
  * number resolves.
16
16
  *
17
17
  * That is also what keeps the check a prose pattern rather than a parser.
18
- * `src/design/base.css` and `src/design/tokens.ts` carry sixteen values shaped
19
- * `#191512`, which no width or boundary rule separates from a pull request
18
+ * `src/design/tokens.ts` carries twenty values shaped `#191512`, which no width or boundary rule separates from a pull request
20
19
  * number, so telling them apart would need the line's syntactic position.
21
20
  */
22
21
  export const SHIPPED_CORPORA = [
package/src/sync/check.ts CHANGED
@@ -2,7 +2,6 @@ import { existsSync } from 'node:fs'
2
2
  import { basename, join, sep } from 'node:path'
3
3
  import { execa } from 'execa'
4
4
  import { gitEnv } from '@/git/env'
5
- import { createDesignAdapter, DESIGN_INSTALL_DIR } from '@/design/adapter'
6
5
  import { createGovAdapter } from '@/gov/adapter'
7
6
  import { successorResolver } from '@/gov/renames'
8
7
  import { lookupRules } from '@/gov/install'
@@ -46,17 +45,11 @@ import { readSkew, type SkewReport } from '@/version/skew'
46
45
  * being one of these, because `src/tooling/` never calls `planSync`, so the
47
46
  * three lookups below have no entry to offer it. Standards and snippets left
48
47
  * the list with their install channels: nothing writes either corpus into a
49
- * target, so there is no installed copy to attribute.
50
- *
51
- * Design joins as a scanned domain rather than a stamp-only one because its
52
- * base file is attributed the same way a rule is, and its install marker is
53
- * what keeps it off a target that never asked for it: a project with no
54
- * `.claude/design/` is never scanned, so design values arrive on an install
55
- * rather than on the next sync.
48
+ * target, so there is no installed copy to attribute. Design left with its
49
+ * install verb for the same reason.
56
50
  */
57
51
  export const SCANNED_DOMAINS = [
58
52
  'governance',
59
- 'design',
60
53
  ] as const satisfies readonly StampDomain[]
61
54
 
62
55
  export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
@@ -68,17 +61,14 @@ export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
68
61
  */
69
62
  const SYNCED_SOURCES: Record<ScannedDomain, string> = {
70
63
  governance: 'governance/rules/',
71
- design: 'src/design/',
72
64
  }
73
65
 
74
66
  const ADAPTERS: Record<ScannedDomain, (root: string) => SyncAdapter> = {
75
67
  governance: createGovAdapter,
76
- design: createDesignAdapter,
77
68
  }
78
69
 
79
70
  const INSTALL_MARKERS: Record<ScannedDomain, readonly string[]> = {
80
71
  governance: ['.claude', 'rules', 'canon'],
81
- design: DESIGN_INSTALL_DIR.split(sep),
82
72
  }
83
73
 
84
74
  export interface StateCounts {
@@ -215,33 +205,18 @@ export interface CheckReport {
215
205
  readonly skew: SkewReport
216
206
  }
217
207
 
218
- /**
219
- * Scanned domains a target takes deliberately rather than by being managed.
220
- *
221
- * An absent one is a choice, so it is never reported as unstamped. Governance
222
- * wants the opposite reading, since a managed target without it has yet to
223
- * install what every project is expected to carry, and naming it is the only
224
- * place that shows up.
225
- */
226
- export const OPT_IN_DOMAINS: readonly ScannedDomain[] = ['design']
227
-
228
208
  /**
229
209
  * Scanned domains this target should have stamped and has not, which is the
230
210
  * one line a domain nobody installed ever appears on.
231
211
  *
232
212
  * An unmigrated domain is excluded because the relocation is its remedy rather
233
- * than a sync, and an opt-in domain the target does not hold is excluded
234
- * because there is nothing there to stamp and the sync it would name refuses.
213
+ * than a sync.
235
214
  */
236
215
  export function uncoveredDomains(report: CheckReport): ScannedDomain[] {
237
216
  const unmigrated = new Set(report.unmigrated.map((entry) => entry.domain))
238
- const installed = new Set(report.domains.map((entry) => entry.domain))
239
217
 
240
218
  return SCANNED_DOMAINS.filter(
241
- (domain) =>
242
- !report.covers.includes(domain) &&
243
- !unmigrated.has(domain) &&
244
- (!OPT_IN_DOMAINS.includes(domain) || installed.has(domain)),
219
+ (domain) => !report.covers.includes(domain) && !unmigrated.has(domain),
245
220
  )
246
221
  }
247
222
 
package/src/sync/stamp.ts CHANGED
@@ -10,17 +10,17 @@ import { compareVersions, parseVersion } from '@/version/compare'
10
10
  import { readInstalled } from '@/version/installed'
11
11
 
12
12
  /**
13
- * Domains the stamp can record. Governance and design both attribute file by
14
- * file through the sync engine. Tooling runs its own inject and manifest
15
- * machinery, so it records the stack chain it resolved instead and carries no
16
- * file hashes.
13
+ * Domains the stamp can record. Governance attributes file by file through
14
+ * the sync engine. Tooling runs its own inject and manifest machinery, so it
15
+ * records the stack chain it resolved instead and carries no file hashes.
17
16
  *
18
17
  * A stamp written before the standards or snippets install channel closed
19
- * still carries a `standards` or `snippets` record. `isStamp` ignores the key
18
+ * still carries a `standards` or `snippets` record, and one written before the
19
+ * design install retired carries a `design` record. `isStamp` ignores the key
20
20
  * and `sortDomains` drops it on the next write, so the target loses a domain
21
21
  * nothing can refresh rather than losing the whole file.
22
22
  */
23
- export const STAMP_DOMAINS = ['governance', 'design', 'tooling'] as const
23
+ export const STAMP_DOMAINS = ['governance', 'tooling'] as const
24
24
 
25
25
  export type StampDomain = (typeof STAMP_DOMAINS)[number]
26
26