@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.
- package/CHANGELOG.md +10 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +0 -8
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -5
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli.ts +0 -2
- package/src/commands/design.ts +16 -315
- package/src/commands/sync.ts +4 -5
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/stages.ts +4 -10
- package/src/gov/audit.ts +1 -6
- package/src/markdown/scope.ts +2 -4
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/governance/rules/ui/canvas.md +0 -13
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -127
- package/src/design/project-fonts.ts +0 -392
package/src/commands/design.ts
CHANGED
|
@@ -1,53 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
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
|
-
//
|
|
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
|
|
66
|
-
//
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -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
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
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:',
|
package/src/design/regen.ts
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* The
|
|
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
|
}
|
package/src/design/render.ts
CHANGED
|
@@ -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>
|
|
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>
|
package/src/design/tokens.ts
CHANGED
|
@@ -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
|
|
88
|
-
* written by `
|
|
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
|
*/
|
package/src/gate/stages.ts
CHANGED
|
@@ -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`,
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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[] = [
|
package/src/markdown/scope.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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', '
|
|
23
|
+
export const STAMP_DOMAINS = ['governance', 'tooling'] as const
|
|
24
24
|
|
|
25
25
|
export type StampDomain = (typeof STAMP_DOMAINS)[number]
|
|
26
26
|
|