@erclx/canon 4.49.0 → 4.50.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "4.49.0",
4
+ "version": "4.50.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.49.0",
4
+ "version": "4.50.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -28,14 +28,22 @@
28
28
  --space-frame-top: 44px;
29
29
  --space-frame-inline: 52px;
30
30
  --space-frame-bottom: 38px;
31
+ --type-display-family: Noto Sans Mono, DejaVu Sans Mono, monospace;
31
32
  --type-display-size: 34px;
32
33
  --type-display-lh: 1.3;
34
+ --type-page-display-family: Noto Sans, DejaVu Sans, sans-serif;
35
+ --type-page-display-size: 52px;
36
+ --type-page-display-lh: 1.1;
37
+ --type-heading-family: Noto Sans Mono, DejaVu Sans Mono, monospace;
33
38
  --type-heading-size: 19px;
34
39
  --type-heading-lh: 1.3;
40
+ --type-body-family: Noto Sans Mono, DejaVu Sans Mono, monospace;
35
41
  --type-body-size: 16px;
36
42
  --type-body-lh: 1.65;
43
+ --type-label-family: Noto Sans Mono, DejaVu Sans Mono, monospace;
37
44
  --type-label-size: 12px;
38
45
  --type-label-lh: 1.45;
46
+ --type-code-family: Noto Sans Mono, DejaVu Sans Mono, monospace;
39
47
  --type-code-size: 14.5px;
40
48
  --type-code-lh: 1.3;
41
49
  --radius-frame: 12px;
package/src/design/css.ts CHANGED
@@ -76,6 +76,7 @@ function tokenProperties(tokens: DesignTokens): string[] {
76
76
  }
77
77
 
78
78
  for (const role of tokens.typography) {
79
+ lines.push(` --type-${slug(role.role)}-family: ${role.family};`)
79
80
  lines.push(` --type-${slug(role.role)}-size: ${role.size};`)
80
81
  lines.push(` --type-${slug(role.role)}-lh: ${role.lineHeight};`)
81
82
  }
@@ -68,9 +68,21 @@ export interface DesignTokens {
68
68
  readonly preamble: string
69
69
  }
70
70
 
71
- /** The one monospace stack every surface that renders text declares. */
71
+ /** The monospace stack every surface but the landing page hero declares. */
72
72
  const MONO = 'Noto Sans Mono, DejaVu Sans Mono, monospace'
73
73
 
74
+ /**
75
+ * The proportional stack the landing page hero declares and nothing else does.
76
+ * Noto Sans is the proportional sibling of the mono above rather than a face
77
+ * picked for taste, so the pairing is one superfamily rather than two systems.
78
+ *
79
+ * The stack stops at two names and a generic because `src/design/base.css` is
80
+ * written by `canon design regen` and formatted by prettier, and a declaration
81
+ * past 80 columns is wrapped by the second and flattened by the first. Every
82
+ * emitted line stays inside the width so the two writers never disagree.
83
+ */
84
+ const SANS = 'Noto Sans, DejaVu Sans, sans-serif'
85
+
74
86
  const DARK_GROUNDS = ['background', 'surface'] as const
75
87
  const LIGHT_GROUNDS = ['light-background', 'light-surface'] as const
76
88
 
@@ -87,6 +99,8 @@ export const TOKENS: DesignTokens = {
87
99
 
88
100
  personality: [
89
101
  'Warm neutrals carry the frame under a single rust accent, rendered in the same monospace the terminal uses. The subject picks the register rather than taste: a toolkit whose primary surface is a shell has no proportional voice available, so the rendered surfaces match the terminal instead of the reverse. One accent carries every count, link, and primary action. Promoting a second and third into structural roles is what reads as a generated interface, so the palette stays at one.',
102
+ '',
103
+ 'One surface is carved out of the sentence above, and it is the landing page hero. A rendered surface here shows a reader what the terminal did, so matching the terminal is what makes it legible. A public page addresses somebody who has never opened the terminal at all, and the shell has no voice available for that, which is the reverse of the case the rule was written for. The `page-display` role below is the whole of the carve-out. Every other role on that page stays monospace, including its body, its controls, and every frame it embeds, so the page reads as two families rather than as a second design system.',
90
104
  ].join('\n'),
91
105
 
92
106
  colorNote: [
@@ -193,7 +207,9 @@ export const TOKENS: DesignTokens = {
193
207
  ],
194
208
 
195
209
  typographyNote: [
196
- 'One family covers every role. The size scale runs from 11.5 to 34 pixels across ten values, and five of them map onto a role. The other five are adjustments inside a single component and get no role here, since a scale with five invented steps reads as a system the surfaces do not implement. They are 11.5, 12.5, 13, 14, and 15 pixels.',
210
+ 'One family covers every role but `page-display`, which is the landing page hero and takes the proportional sibling of the same superfamily. The size scale runs from 11.5 to 52 pixels, and six values map onto a role. Five further values are adjustments inside a single component and get no role here, since a scale with five invented steps reads as a system the surfaces do not implement. They are 11.5, 12.5, 13, 14, and 15 pixels.',
211
+ '',
212
+ 'The 52 pixel step sits above the 34 the rest of the scale tops out at, and it is the one size no other surface reaches. A hero headline set at the display cap reads as a section heading rather than an opening, which four rendered arms measured on 2026-09-04 before the step was added.',
197
213
  '',
198
214
  'A tagged cell is one no rendering surface exercises yet, which is a declaration the system has not tested rather than one it has.',
199
215
  '',
@@ -208,6 +224,14 @@ export const TOKENS: DesignTokens = {
208
224
  size: '34px',
209
225
  lineHeight: '1.3',
210
226
  },
227
+ {
228
+ role: 'page-display',
229
+ family: SANS,
230
+ weight: '700',
231
+ size: '52px',
232
+ lineHeight: '1.1',
233
+ verify: ['family', 'size', 'lineHeight'],
234
+ },
211
235
  {
212
236
  role: 'heading',
213
237
  family: MONO,
@@ -400,7 +400,8 @@ export async function planCitations(
400
400
 
401
401
  const stems = await listTaskStems(dir)
402
402
  if (!stems.includes(stem)) {
403
- return refuse('no-match', `No task named ${stem} on the board.`, stems)
403
+ const unmatched = describeUnmatchedStem(stems, stem)
404
+ return refuse(unmatched.reason, unmatched.message, unmatched.detail)
404
405
  }
405
406
 
406
407
  const target = readPlanTarget(await readFile(join(dir, `${stem}.md`), 'utf8'))
@@ -488,6 +489,45 @@ function refuse(
488
489
  return { ok: false, reason, message, detail }
489
490
  }
490
491
 
492
+ export interface UnmatchedStem {
493
+ readonly reason: 'no-match' | 'ambiguous'
494
+ readonly message: string
495
+ readonly detail: readonly string[]
496
+ }
497
+
498
+ /**
499
+ * The three-way refusal a bare stem earns once it fails an exact match:
500
+ * exactly one task starts with it, several do, or none does. `archive.ts`'s
501
+ * own `resolveStem`, `planCitations`, and `record.ts`'s `resolveStem` all
502
+ * refuse a stem this way, so the wording lives once rather than three times.
503
+ */
504
+ export function describeUnmatchedStem(
505
+ stems: readonly string[],
506
+ name: string,
507
+ ): UnmatchedStem {
508
+ const prefixed = stems.filter((stem) => stem.startsWith(name))
509
+ if (prefixed.length === 1) {
510
+ const [match] = prefixed
511
+ return {
512
+ reason: 'no-match',
513
+ message: `${name} does not name a task by itself. One task starts with it: ${match}. Pass the full name instead.`,
514
+ detail: [match],
515
+ }
516
+ }
517
+ if (prefixed.length > 1) {
518
+ return {
519
+ reason: 'ambiguous',
520
+ message: `${name} does not name a task by itself. ${prefixed.length} tasks start with it. Pass the full name instead.`,
521
+ detail: prefixed,
522
+ }
523
+ }
524
+ return {
525
+ reason: 'no-match',
526
+ message: `No task named ${name} on the board.`,
527
+ detail: stems,
528
+ }
529
+ }
530
+
491
531
  async function resolveStem(
492
532
  dir: string,
493
533
  selector: TaskSelector,
@@ -496,27 +536,11 @@ async function resolveStem(
496
536
 
497
537
  if (selector.kind === 'stem') {
498
538
  if (!stems.includes(selector.stem)) {
499
- const prefixed = stems.filter((stem) => stem.startsWith(selector.stem))
500
- if (prefixed.length === 1) {
501
- const [match] = prefixed
502
- return refuse(
503
- 'no-match',
504
- `${selector.stem} does not name a task by itself. One task starts with it: ${match}. Pass the full name to archive it.`,
505
- [match],
506
- )
507
- }
508
- if (prefixed.length > 1) {
509
- return refuse(
510
- 'ambiguous',
511
- `${selector.stem} does not name a task by itself. ${prefixed.length} tasks start with it. Pass the full name to archive one.`,
512
- prefixed,
513
- )
514
- }
515
- return refuse(
516
- 'no-match',
517
- `No task named ${selector.stem} on the board.`,
539
+ const { reason, message, detail } = describeUnmatchedStem(
518
540
  stems,
541
+ selector.stem,
519
542
  )
543
+ return refuse(reason, message, detail)
520
544
  }
521
545
  return selector.stem
522
546
  }
@@ -3,6 +3,7 @@ import { readFile, writeFile } from 'node:fs/promises'
3
3
  import { basename, join, relative } from 'node:path'
4
4
  import { planCandidates } from '@/tasks/answers'
5
5
  import {
6
+ describeUnmatchedStem,
6
7
  fenceMask,
7
8
  linkTo,
8
9
  listTaskStems,
@@ -241,11 +242,11 @@ async function resolveStem(
241
242
 
242
243
  if (selector.kind === 'stem') {
243
244
  if (!stems.includes(selector.stem)) {
244
- return refuse(
245
- 'no-match',
246
- `No task named ${selector.stem} on the board.`,
245
+ const { reason, message, detail } = describeUnmatchedStem(
247
246
  stems,
247
+ selector.stem,
248
248
  )
249
+ return refuse(reason, message, detail)
249
250
  }
250
251
  return selector.stem
251
252
  }
@@ -22,5 +22,6 @@ export default defineConfig({
22
22
  command: 'bun run build && bun run preview',
23
23
  url: baseURL,
24
24
  reuseExistingServer: false,
25
+ env: { ASTRO_PREVIEW_BACKGROUND: '0' },
25
26
  },
26
27
  })