@erclx/canon 5.24.0 → 5.26.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 (185) hide show
  1. package/README.md +2 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  4. package/claude/skills/backlog-triage/SKILL.md +19 -6
  5. package/claude/skills/canvas/REQUIREMENT.md +49 -0
  6. package/claude/skills/canvas/SKILL.md +106 -0
  7. package/claude/skills/context-fold/REQUIREMENT.md +6 -2
  8. package/claude/skills/context-fold/SKILL.md +8 -6
  9. package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
  10. package/claude/skills/design-extract/REQUIREMENT.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/SKILL.md +4 -2
  13. package/claude/skills/design-taste/references/adopted.md +40 -0
  14. package/claude/skills/design-taste/references/kinds.md +1 -1
  15. package/claude/skills/design-taste/references/systems.md +1 -7
  16. package/claude/skills/design-taste/references/tells.md +2 -24
  17. package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
  18. package/claude/skills/document-health/REQUIREMENT.md +2 -0
  19. package/claude/skills/document-health/SKILL.md +21 -0
  20. package/claude/skills/document-health/references/axes.md +12 -0
  21. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  22. package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
  23. package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
  24. package/claude/skills/draft-figure/SKILL.md +24 -51
  25. package/claude/skills/draft-figure/references/architecture-set.md +35 -0
  26. package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
  27. package/claude/skills/draft-figure/references/mermaid.md +55 -0
  28. package/claude/skills/draft-identity/SKILL.md +1 -1
  29. package/claude/skills/draft-screencast/SKILL.md +9 -7
  30. package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
  31. package/claude/skills/draft-slides/SKILL.md +31 -20
  32. package/claude/skills/git-followup/SKILL.md +17 -6
  33. package/claude/skills/git-issue/references/issue.md +10 -0
  34. package/claude/skills/git-pr/references/evidence.md +12 -2
  35. package/claude/skills/git-pr/references/labels.md +12 -0
  36. package/claude/skills/git-split/SKILL.md +19 -8
  37. package/claude/skills/index-lookup/SKILL.md +2 -2
  38. package/claude/skills/markdown-propose/references/format.md +9 -0
  39. package/claude/skills/memory-review/references/later-phases.md +7 -0
  40. package/claude/skills/review-address/SKILL.md +6 -13
  41. package/claude/skills/review-address/references/rest-reads.md +43 -0
  42. package/claude/skills/review-craft/references/ui.md +7 -1
  43. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  44. package/claude/skills/review-pr/SKILL.md +9 -3
  45. package/claude/skills/review-pr/references/close-out.md +2 -2
  46. package/claude/skills/review-pr/references/post.md +17 -5
  47. package/claude/skills/review-pr/references/scope.md +10 -8
  48. package/claude/skills/review-ui/SKILL.md +12 -4
  49. package/claude/skills/review-ui/references/post.md +4 -2
  50. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +38 -1
  51. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
  52. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
  53. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
  54. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
  55. package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
  56. package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
  57. package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
  58. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  59. package/claude/skills/search-craft/SKILL.md +3 -1
  60. package/claude/skills/search-craft/references/adopted.md +30 -0
  61. package/claude/skills/target-setup/references/detect.md +8 -0
  62. package/claude/skills/target-setup/references/indexes.md +13 -0
  63. package/claude/skills/target-setup/references/verify.md +8 -0
  64. package/claude/skills/test-first/SKILL.md +2 -0
  65. package/claude/skills/test-first/references/adopted.md +36 -0
  66. package/claude/skills/write-human/SKILL.md +1 -1
  67. package/claude/skills/write-human/references/machine-tells.md +13 -0
  68. package/docs/agents/canvas.md +4 -2
  69. package/docs/agents/capture.md +1 -1
  70. package/docs/agents/commands.md +47 -47
  71. package/docs/agents/context-audit-checks.md +2 -2
  72. package/docs/agents/index.md +2 -3
  73. package/docs/agents/key-changes.md +3 -3
  74. package/docs/agents/pr-evidence.md +11 -2
  75. package/docs/agents/pr-reads.md +5 -4
  76. package/docs/agents/records-stale.md +22 -9
  77. package/docs/agents/records.md +1 -1
  78. package/docs/agents/sandbox.md +1 -1
  79. package/docs/agents/skills-audit.md +10 -3
  80. package/docs/agents/slides.md +15 -3
  81. package/docs/target/projects.md +4 -4
  82. package/docs/workflow/ai-workflow.md +1 -2
  83. package/docs/workflow/index.md +1 -0
  84. package/docs/workflow/parallel-features.md +1 -1
  85. package/docs/workflow/skill-map-on-request.md +42 -0
  86. package/docs/workflow/skill-map.md +3 -36
  87. package/docs/workflow/visual-design-workflow.md +1 -0
  88. package/governance/renames.toml +0 -4
  89. package/governance/rules/standards/markdown.md +1 -0
  90. package/package.json +1 -2
  91. package/scripts/core/install-check.sh +1 -1
  92. package/scripts/lib/sandbox-dispatch.sh +1 -1
  93. package/scripts/lib/sandbox-git.sh +1 -9
  94. package/scripts/lib/sandbox-path.sh +1 -1
  95. package/scripts/sandbox-hook.sh +99 -0
  96. package/src/audits/catalog.ts +2 -2
  97. package/src/canvas/capture.ts +1 -9
  98. package/src/canvas/client/inspector/color-field.tsx +368 -0
  99. package/src/canvas/client/inspector/field.tsx +200 -0
  100. package/src/canvas/client/inspector/fields.css +292 -0
  101. package/src/canvas/client/inspector/section.tsx +27 -0
  102. package/src/canvas/client/inspector/values.ts +183 -0
  103. package/src/canvas/client/inspector.tsx +176 -160
  104. package/src/canvas/client/shell.css +0 -101
  105. package/src/capture/render.ts +45 -7
  106. package/src/capture/sources.ts +43 -0
  107. package/src/census/count.ts +1 -1
  108. package/src/claude/cases/misc.ts +5 -0
  109. package/src/claude/cases/workflow.ts +6 -1
  110. package/src/claude/seeds.ts +1 -2
  111. package/src/claude/skills-audit.ts +68 -14
  112. package/src/client-commands.ts +1 -1
  113. package/src/commands/claude.ts +27 -2
  114. package/src/commands/design.ts +2 -64
  115. package/src/commands/feedback.ts +3 -3
  116. package/src/commands/pr.ts +219 -202
  117. package/src/commands/records.ts +88 -6
  118. package/src/commands/sandbox.ts +50 -10
  119. package/src/commands/slides.ts +31 -77
  120. package/src/context/audit.ts +8 -7
  121. package/src/design/tokens.ts +2 -18
  122. package/src/docs/read.ts +1 -1
  123. package/src/{github.ts → feedback/github.ts} +1 -1
  124. package/src/gate/measures.ts +6 -4
  125. package/src/gov/citations.ts +1 -1
  126. package/src/gov/payload.ts +1 -1
  127. package/src/gov/superseded.ts +2 -2
  128. package/src/help.ts +2 -2
  129. package/src/markdown/ceiling.ts +1 -1
  130. package/src/migrate/skill-names.ts +7 -1
  131. package/src/pr/evidence.ts +25 -5
  132. package/src/pr/rest.ts +137 -0
  133. package/src/record-root.ts +1 -0
  134. package/src/records/backup.ts +1 -0
  135. package/src/records/push-guard.ts +1 -1
  136. package/src/records/stale-canonical.ts +146 -0
  137. package/src/records/stale.ts +1 -1
  138. package/src/sandbox/hooks.ts +128 -0
  139. package/src/sandbox/provision.ts +596 -0
  140. package/src/sandbox/tree.ts +103 -4
  141. package/src/secrets/marker.ts +2 -2
  142. package/src/secrets/scan.ts +1 -1
  143. package/src/shipped/references.ts +2 -2
  144. package/src/slides/convert/export.ts +2 -16
  145. package/src/slides/decks.ts +110 -0
  146. package/src/standards/read.ts +1 -1
  147. package/src/sync/seeds-report.ts +1 -1
  148. package/src/tasks/answers.ts +1 -1
  149. package/src/tasks/archive.ts +1 -1
  150. package/src/teach/nav.ts +1 -1
  151. package/src/tooling/inject.ts +1 -1
  152. package/src/web/readme-citations.ts +1 -1
  153. package/standards/architecture.md +1 -0
  154. package/standards/canvas.md +112 -0
  155. package/standards/context.md +2 -1
  156. package/standards/figures.md +0 -1
  157. package/standards/index.md +1 -1
  158. package/standards/markdown.md +1 -0
  159. package/standards/mermaid.md +1 -7
  160. package/standards/requirements.md +2 -0
  161. package/standards/skill-practice.md +2 -1
  162. package/standards/skill.md +1 -0
  163. package/tooling/claude/reference.md +0 -5
  164. package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
  165. package/claude/skills/draft-diagram/SKILL.md +0 -177
  166. package/docs/agents/design-board.md +0 -33
  167. package/governance/rules/standards/diagrams.md +0 -14
  168. package/governance/rules/standards/mermaid.md +0 -15
  169. package/scripts/core/check-gallery-exclusion.sh +0 -50
  170. package/scripts/manage-sandbox.sh +0 -472
  171. package/src/design/board.ts +0 -576
  172. package/src/slides/layouts.ts +0 -478
  173. package/src/slides/parse.ts +0 -84
  174. package/src/slides/render.ts +0 -110
  175. package/src/slides/styles.ts +0 -96
  176. package/standards/diagrams.md +0 -132
  177. package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
  178. /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
  179. /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
  180. /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
  181. /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
  182. /package/src/{binary.ts → sweep/binary.ts} +0 -0
  183. /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
  184. /package/src/{paths.ts → tasks/paths.ts} +0 -0
  185. /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
@@ -30,6 +30,11 @@ import {
30
30
  type StaleReport,
31
31
  staleMemory,
32
32
  } from '@/records/stale'
33
+ import {
34
+ type CanonicalDoc,
35
+ type CanonicalReport,
36
+ staleCanonical,
37
+ } from '@/records/stale-canonical'
33
38
  import {
34
39
  type Finding,
35
40
  type FindingRemedy,
@@ -85,8 +90,11 @@ interface StaleCommandOptions extends ValidateCommandOptions {
85
90
  readonly days?: string
86
91
  }
87
92
 
88
- /** The one kind `stale` reads, since only a memory entry carries a review date. */
89
- const STALE_KINDS = ['memory'] as const
93
+ /**
94
+ * The kinds `stale` reads. A memory entry ages by days in the shared pen, and
95
+ * a canonical doc ages by the releases shipped since its review.
96
+ */
97
+ const STALE_KINDS = ['memory', 'canonical'] as const
90
98
 
91
99
  interface OrdinalCommandOptions extends ValidateCommandOptions {
92
100
  readonly claim?: boolean
@@ -270,14 +278,17 @@ export function register(program: Command): void {
270
278
  .option('--json', 'Add a machine-readable record on stdout')
271
279
  .option(
272
280
  '--days <n>',
273
- `Days after a review before an entry is due again (default ${DEFAULT_REVIEW_DAYS})`,
281
+ `Days after a review before a memory entry is due again (default ${DEFAULT_REVIEW_DAYS})`,
282
+ )
283
+ .option(
284
+ '--root <path>',
285
+ 'Project root, defaulting to the main worktree except on canonical',
274
286
  )
275
- .option('--root <path>', 'Project root, defaulting to the main worktree')
276
287
  .addHelpText(
277
288
  'after',
278
289
  [
279
290
  '',
280
- 'Reads the top-level entries of the memory folder, never review/, archive/,',
291
+ 'memory: reads the top-level entries of the memory folder, never review/, archive/,',
281
292
  'or index.md. An entry is due when it carries no reviewed date, a reviewed',
282
293
  'value that is not a date, or one older than --days. A path is a backticked',
283
294
  'token holding a / and a file extension, resolved against the project root',
@@ -288,17 +299,24 @@ export function register(program: Command): void {
288
299
  'Entries are ordered due first, then those citing a missing path, then the',
289
300
  'longest unreviewed, then by name, so a review takes the first N as a batch.',
290
301
  '',
302
+ 'canonical: reads REQUIREMENTS.md and ARCHITECTURE.md at whichever surface',
303
+ 'root carries each, in the current worktree since both are tracked. Each',
304
+ 'reports its reviewed date and the release tags merged into HEAD after that',
305
+ 'day, or never when it carries no date. --days does not apply.',
306
+ '',
291
307
  'Exit codes:',
292
308
  ' 0 the reading completed, whatever it found',
293
309
  ' 1 refused, with the reason on stderr or in the JSON record',
294
310
  '',
295
311
  'It gates nothing and writes nothing. A due entry is a queue position rather',
296
- 'than a failure, and a missing path is a prompt for the review to judge.',
312
+ 'than a failure, and a missing path or a release count is a prompt for the',
313
+ 'review to judge.',
297
314
  '',
298
315
  'Examples:',
299
316
  ' canon records stale memory',
300
317
  ' canon records stale memory --json',
301
318
  ' canon records stale memory --days 60 --json',
319
+ ' canon records stale canonical --json',
302
320
  '',
303
321
  ].join('\n'),
304
322
  )
@@ -560,6 +578,8 @@ async function runStale(
560
578
  )
561
579
  }
562
580
 
581
+ if (kind === 'canonical') return runStaleCanonical(opts, emitJson)
582
+
563
583
  const days = Number(opts.days ?? DEFAULT_REVIEW_DAYS)
564
584
 
565
585
  if (!Number.isInteger(days) || days <= 0) {
@@ -588,6 +608,68 @@ async function runStale(
588
608
  return 0
589
609
  }
590
610
 
611
+ /**
612
+ * Both docs are tracked, so the root is the checkout the caller stands in. The
613
+ * main worktree would answer from its own copy and miss a stamp this branch
614
+ * just wrote.
615
+ */
616
+ async function runStaleCanonical(
617
+ opts: StaleCommandOptions,
618
+ emitJson: boolean,
619
+ ): Promise<number> {
620
+ if (opts.days !== undefined) {
621
+ return reportRefusal(
622
+ 'canon records stale',
623
+ {
624
+ reason: 'bad-days',
625
+ message:
626
+ '--days does not apply to canonical, which counts releases rather than days.',
627
+ },
628
+ emitJson,
629
+ )
630
+ }
631
+
632
+ const root = opts.root ?? (await currentWorktreeRoot())
633
+ const outcome = await staleCanonical(root)
634
+
635
+ if (!outcome.ok)
636
+ return reportRefusal('canon records stale', outcome, emitJson)
637
+
638
+ if (emitJson) {
639
+ process.stdout.write(`${JSON.stringify(outcome)}\n`)
640
+ return 0
641
+ }
642
+
643
+ intro('canon records stale')
644
+ logStep('Canonical')
645
+
646
+ if (outcome.docs.length === 0) {
647
+ logInfo('neither REQUIREMENTS.md nor ARCHITECTURE.md exists')
648
+ } else {
649
+ for (const doc of outcome.docs) logInfo(describeCanonical(doc, outcome))
650
+ }
651
+
652
+ if (!outcome.tagged) logInfo('no release tags merged into HEAD')
653
+ outro()
654
+ return 0
655
+ }
656
+
657
+ function describeCanonical(
658
+ doc: CanonicalDoc,
659
+ outcome: CanonicalReport,
660
+ ): string {
661
+ if (doc.reviewed === null) {
662
+ return doc.invalidReviewed === undefined
663
+ ? `${doc.path}: never reviewed`
664
+ : `${doc.path}: reviewed value is not a date: ${doc.invalidReviewed}`
665
+ }
666
+
667
+ const since = outcome.tagged
668
+ ? `${plural(doc.releasesSince ?? 0, 'release')} since`
669
+ : 'no releases to count'
670
+ return `${doc.path}: reviewed ${doc.reviewed}, ${since}`
671
+ }
672
+
591
673
  function staleRow(entry: StaleEntry): string[] {
592
674
  return [
593
675
  entry.name,
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import { execScript } from '@/exec'
5
4
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
6
5
  import {
7
6
  assertedPercent,
@@ -26,6 +25,7 @@ import {
26
25
  type RunEnvelope,
27
26
  type Verdict,
28
27
  } from '@/sandbox/expect'
28
+ import { runSandbox } from '@/sandbox/provision'
29
29
  import { sandboxTree } from '@/sandbox/tree'
30
30
  import {
31
31
  frameError,
@@ -36,6 +36,7 @@ import {
36
36
  logStep,
37
37
  logWarn,
38
38
  outro,
39
+ palette,
39
40
  select,
40
41
  } from '@/ui'
41
42
 
@@ -69,11 +70,39 @@ function reportAbsentScenarioTree(): boolean {
69
70
  return true
70
71
  }
71
72
 
73
+ /** Holds fixture content for scenarios rather than scenarios of its own. */
74
+ const FIXTURES_DIR = 'fixtures'
75
+
72
76
  /**
73
- * Holds fixture content for scenarios rather than scenarios of its own.
74
- * Twin of the `-not -name fixtures` filter in `scripts/manage-sandbox.sh`.
77
+ * The provision verb takes positional words rather than options, since `reset`
78
+ * and `clean` share its slot with a target, so its usage is written out here
79
+ * rather than generated by commander.
75
80
  */
76
- const FIXTURES_DIR = 'fixtures'
81
+ function showProvisionHelp(): void {
82
+ const { GREY, NC, WHITE } = palette(process.stdout)
83
+ const lines = [
84
+ `${GREY}┌${NC}`,
85
+ `${GREY}├${NC} ${WHITE}Usage:${NC} canon sandbox [cat:cmd]`,
86
+ `${GREY}│${NC}`,
87
+ `${GREY}│${NC} ${WHITE}Arguments:${NC}`,
88
+ `${GREY}│${NC} cat:cmd ${GREY}# Scenario to provision (e.g. git:commit)${NC}`,
89
+ `${GREY}│${NC}`,
90
+ `${GREY}│${NC} ${WHITE}Commands:${NC}`,
91
+ `${GREY}│${NC} reset ${GREY}# Reset sandbox to baseline${NC}`,
92
+ `${GREY}│${NC} clean ${GREY}# Wipe the sandbox${NC}`,
93
+ `${GREY}│${NC}`,
94
+ `${GREY}│${NC} ${WHITE}Options:${NC}`,
95
+ `${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}`,
96
+ `${GREY}│${NC}`,
97
+ `${GREY}│${NC} ${WHITE}Examples:${NC}`,
98
+ `${GREY}│${NC} canon sandbox`,
99
+ `${GREY}│${NC} canon sandbox git:commit`,
100
+ `${GREY}│${NC} canon sandbox reset`,
101
+ `${GREY}│${NC} canon sandbox clean`,
102
+ `${GREY}└${NC}`,
103
+ ]
104
+ process.stdout.write(`${lines.join('\n')}\n`)
105
+ }
77
106
 
78
107
  /** A run that reported nothing counts as clean, so only real signals fail. */
79
108
  const CLEAN_ENVELOPE: RunEnvelope = { isError: false, turns: 0, denials: 0 }
@@ -477,14 +506,25 @@ export function register(program: Command): void {
477
506
  .action(async (_opts: unknown, cmd: Command) => {
478
507
  if (reportAbsentScenarioTree()) return
479
508
 
480
- const args = cmd.args
509
+ const mismatch = checkoutMismatchWarning(process.cwd())
510
+ if (mismatch !== undefined) logWarn(mismatch)
511
+
512
+ // `--no-header` is the frame opt-out for a caller that already opened
513
+ // one, which `sandbox/run.sh` and the picker below both do.
514
+ const isHeader = cmd.args[0] !== '--no-header'
515
+ const args = isHeader ? cmd.args : cmd.args.slice(1)
481
516
 
482
- if (args.length === 0) {
483
- const resolved = await interactivePicker()
484
- await execScript('manage-sandbox.sh', ['--no-header', resolved])
485
- } else {
486
- await execScript('manage-sandbox.sh', args)
517
+ if (args[0] === '-h' || args[0] === '--help') {
518
+ showProvisionHelp()
519
+ return
487
520
  }
521
+
522
+ const target = args[0] ?? (await interactivePicker())
523
+ process.exitCode = await runSandbox(PROJECT_ROOT, {
524
+ isHeader: isHeader && args[0] !== undefined,
525
+ target,
526
+ arm: args[1] === '' ? undefined : args[1],
527
+ })
488
528
  })
489
529
 
490
530
  sandbox
@@ -1,14 +1,13 @@
1
- import { existsSync, statSync } from 'node:fs'
2
1
  import { resolve } from 'node:path'
3
2
  import type { Command } from 'commander'
4
3
  import { INSTALL_BROWSER } from '@/browser/engine'
5
4
  import { creationRel, SCRATCH } from '@/record-root'
6
- import { LAYOUTS } from '@/slides/layouts'
5
+ import { listDecks, resolveDeck, SLIDES_FOLDER } from '@/slides/decks'
7
6
  import { openDeck } from '@/slides/open'
8
- import { renderSlidesDoc } from '@/slides/render'
9
- import type { Variant } from '@/slides/styles'
10
7
  import { intro, outro, palette } from '@/ui'
11
8
 
9
+ const SLIDES_AT = creationRel(process.cwd(), SLIDES_FOLDER)
10
+
12
11
  export function register(program: Command): void {
13
12
  const slides = program
14
13
  .command('slides')
@@ -19,77 +18,34 @@ export function register(program: Command): void {
19
18
  .command('render')
20
19
  .helpOption('-h, --help', 'Show this help message')
21
20
  .description(
22
- 'Render a SLIDES.md source, or a folder of HTML slides, into a PowerPoint deck',
21
+ `Render a deck under ${SLIDES_AT}/, or a folder of HTML slides, into a PowerPoint deck`,
23
22
  )
24
- .option(
25
- '-s, --source <path>',
26
- 'Source SLIDES.md file, or a folder of .html slides read in filename order',
27
- '.claude/SLIDES.md',
23
+ .argument(
24
+ '[deck]',
25
+ 'Deck name or folder, defaulting to the only deck when there is one',
28
26
  )
29
27
  .option(
30
28
  '-o, --out <path>',
31
29
  'Output directory',
32
30
  creationRel(process.cwd(), SCRATCH, 'render', 'slides'),
33
31
  )
34
- .option('-v, --variant <variant>', 'Override variant (light or dark)')
35
32
  .option(
36
33
  '-m, --mirror <path>',
37
34
  'Copy the deck to this directory after writing',
38
35
  )
39
36
  .option('--open', 'Open the deck after writing')
40
37
  .action(
41
- async (opts: {
42
- source: string
43
- out: string
44
- variant?: string
45
- mirror?: string
46
- open?: boolean
47
- }) => {
38
+ async (
39
+ deck: string | undefined,
40
+ opts: { out: string; mirror?: string; open?: boolean },
41
+ ) => {
48
42
  try {
49
- const sourcePath = resolve(process.cwd(), opts.source)
50
- const outDir = resolve(process.cwd(), opts.out)
51
- if (!existsSync(sourcePath)) {
52
- fail(`${opts.source} not found`)
53
- }
43
+ const root = process.cwd()
44
+ const resolved = resolveDeck(root, deck)
45
+ if (resolved.status === 'refused') fail(resolved.message)
46
+ const outDir = resolve(root, opts.out)
54
47
  const mirror = resolveMirror(opts.mirror)
55
- if (statSync(sourcePath).isDirectory()) {
56
- if (opts.variant !== undefined) {
57
- fail('--variant applies to a SLIDES.md source, not a folder')
58
- }
59
- await renderHtmlFolder(sourcePath, outDir, mirror, opts.open)
60
- return
61
- }
62
- const variant = parseVariant(opts.variant)
63
- const { GREEN, GREY, NC, RED } = palette(process.stderr)
64
- intro('Render slides')
65
- const result = await renderSlidesDoc(sourcePath, outDir, {
66
- variant,
67
- mirror,
68
- })
69
- process.stderr.write(
70
- `${GREY}│${NC} ${GREEN}✓${NC} ${result.slideCount} slides\n${GREY}│${NC} ${GREEN}✓${NC} ${result.pptxPath}\n`,
71
- )
72
- const validNames = LAYOUTS.map((layout) => layout.name).join(', ')
73
- for (const { value, slideNumbers } of result.unrecognizedLayouts) {
74
- const noun = slideNumbers.length === 1 ? 'slide' : 'slides'
75
- process.stderr.write(
76
- `${GREY}│${NC} ${RED}✗${NC} unrecognized layout "${value}" on ${noun} ${slideNumbers.join(', ')}. Valid layouts: ${validNames}\n`,
77
- )
78
- }
79
- if (result.mirrorPath) {
80
- process.stderr.write(
81
- `${GREY}│${NC} ${GREEN}✓${NC} mirrored to ${result.mirrorPath}\n`,
82
- )
83
- }
84
- if (opts.open) {
85
- const target = result.mirrorPath ?? result.pptxPath
86
- const opened = await openDeck(target)
87
- const mark = opened ? `${GREEN}✓${NC}` : `${RED}✗${NC}`
88
- process.stderr.write(
89
- `${GREY}│${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
90
- )
91
- }
92
- outro()
48
+ await renderHtmlFolder(resolved.dir, outDir, mirror, opts.open)
93
49
  } catch (error) {
94
50
  reportFailure(error)
95
51
  }
@@ -99,18 +55,23 @@ export function register(program: Command): void {
99
55
  slides
100
56
  .command('list')
101
57
  .helpOption('-h, --help', 'Show this help message')
102
- .description('List the available slide layouts')
103
- .option('--json', 'Output the layout catalog as JSON')
58
+ .description(`List the decks under ${SLIDES_AT}/`)
59
+ .option('--json', 'Output the decks as JSON')
104
60
  .action((opts: { json?: boolean }) => {
61
+ const decks = listDecks(process.cwd())
105
62
  if (opts.json) {
106
- process.stdout.write(`${JSON.stringify(LAYOUTS)}\n`)
63
+ process.stdout.write(`${JSON.stringify(decks)}\n`)
107
64
  return
108
65
  }
109
66
  const { GREEN, GREY, NC } = palette(process.stderr)
110
- intro('Slide layouts')
111
- for (const layout of LAYOUTS) {
67
+ intro('Slide decks')
68
+ if (decks.length === 0) {
69
+ process.stderr.write(`${GREY}│${NC} No deck under ${SLIDES_AT}/\n`)
70
+ }
71
+ for (const { name, title, slides: count, path } of decks) {
72
+ const noun = count === 1 ? 'slide' : 'slides'
112
73
  process.stderr.write(
113
- `${GREY}│${NC} ${GREEN}✓${NC} ${layout.name} ${GREY}${layout.description}${NC}\n`,
74
+ `${GREY}│${NC} ${GREEN}✓${NC} ${name} ${GREY}${title}, ${count} ${noun}, ${path}${NC}\n`,
114
75
  )
115
76
  }
116
77
  outro()
@@ -121,8 +82,7 @@ export function register(program: Command): void {
121
82
  * The converter loads on demand, so no other command resolves pptxgenjs,
122
83
  * jszip, or the token module at startup. A fallback, a refused link, chart, or
123
84
  * effect, or a token the master fell back on reports one line each and keeps
124
- * the exit at 0, as an unrecognized layout does on the markdown path, since the
125
- * deck is written either way. A malformed `deck.json` refuses before any slide is laid
85
+ * the exit at 0, since the deck is written either way. A malformed `deck.json` refuses before any slide is laid
126
86
  * out and exits 1.
127
87
  */
128
88
  async function renderHtmlFolder(
@@ -192,12 +152,6 @@ async function renderHtmlFolder(
192
152
  outro()
193
153
  }
194
154
 
195
- function parseVariant(value: string | undefined): Variant | undefined {
196
- if (value === undefined) return undefined
197
- if (value === 'light' || value === 'dark') return value
198
- fail(`Invalid variant "${value}". Use light or dark.`)
199
- }
200
-
201
155
  function resolveMirror(value: string | undefined): string | undefined {
202
156
  const mirror = value ?? process.env.CANON_SLIDES_MIRROR
203
157
  return mirror ? resolve(process.cwd(), mirror) : undefined
@@ -205,9 +159,9 @@ function resolveMirror(value: string | undefined): string | undefined {
205
159
 
206
160
  /**
207
161
  * Carries a fail-fast message from a validation helper to the action that
208
- * called it. `fail` has to keep its `never` return, since that is what makes
209
- * `parseVariant` exhaustive to the compiler, and a helper deep in the call
210
- * stack cannot set `process.exitCode` and unwind on its own.
162
+ * called it. `fail` keeps its `never` return so the compiler narrows past it,
163
+ * and a helper deep in the call stack cannot set `process.exitCode` and unwind
164
+ * on its own.
211
165
  */
212
166
  class SlidesError extends Error {}
213
167
 
@@ -4,7 +4,7 @@ import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
4
4
  import type { AuditedFolder } from '@/context/folders'
5
5
  import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
6
6
  import { documentHeight } from '@/markdown/structure'
7
- import { isStubSeed } from '@/seed-marker'
7
+ import { isStubSeed } from '@/tooling/seed-marker'
8
8
 
9
9
  /**
10
10
  * Checkpoint quoted from the standard stating it. It is not a cap.
@@ -18,9 +18,9 @@ import { isStubSeed } from '@/seed-marker'
18
18
  * Every reason `canon context audit` refuses for.
19
19
  *
20
20
  * `no-folders` is the one ordinary absence: a project that never adopted
21
- * `canon/context/`, `.canon/diagrams/`, or `canon/wireframes/` names no
22
- * corpus this audit can measure, the same state `no-skills` reads for the
23
- * skill corpora. The other four are a malformed invocation or a checkout git
21
+ * `canon/context/` or `canon/wireframes/`, and holds no `.canon/diagrams/`
22
+ * folder an older install left, names no corpus this audit can measure, the
23
+ * same state `no-skills` reads for the skill corpora. The other four are a malformed invocation or a checkout git
24
24
  * cannot read, which stay a break rather than an absence.
25
25
  */
26
26
  export type ContextAuditRefusal =
@@ -157,9 +157,10 @@ const SENTENCE_END = /[.!?:;]\s(?=[^.!?:;]*$)/
157
157
  * JSON record publishes as `checkpoints.provenanceFolder` breaks every consumer
158
158
  * reading that field for a gain of one word.
159
159
  *
160
- * `standards/context.md` opens its scope by handing diagrams and wireframes to
161
- * `diagrams.md` and `wireframes.md`, so a marker reported in either would cite
162
- * a rule that entry's own standard routes elsewhere. The length and table
160
+ * `standards/context.md` opens its scope by handing figures to the
161
+ * `draft-figure` skill and wireframes to `wireframes.md`, so a marker reported
162
+ * in a diagrams or wireframes entry would cite a rule that entry's own
163
+ * standard routes elsewhere. The length and table
163
164
  * checkpoints are quoted from the same standard and keep reaching every audited
164
165
  * folder, because a threshold on how far a reader travels generalizes across
165
166
  * entry types while a rule about what an entry may say does not.
@@ -2,11 +2,8 @@
2
2
  * The design system's one source of values.
3
3
  *
4
4
  * `canon/DESIGN.md` is rendered from this module rather than read by it, so
5
- * the document a person opens is a view and this file is the fact. Every
6
- * rendering surface reads from here in the form it can take: a CSS surface
7
- * takes custom properties through `@/design/css`, and the slide renderer takes
8
- * bare hex through `bareHex` below, because PowerPoint has no concept of a
9
- * custom property.
5
+ * the document a person opens is a view and this file is the fact. A CSS
6
+ * surface reads it as custom properties through `@/design/css`.
10
7
  *
11
8
  * The two artifacts can disagree, which is what the `design` gate stage exists
12
9
  * to catch. Nothing else compares them.
@@ -371,16 +368,3 @@ export function colorValue(
371
368
  ): string | undefined {
372
369
  return tokens.color.find((token) => token.role === role)?.value
373
370
  }
374
-
375
- /**
376
- * The adapter the slide renderer takes. `PptxGenJS` receives color as
377
- * `{ color: theme.background }` and wants six hex digits with no leading `#`,
378
- * so the shared thing is the value and this is the per-consumer form.
379
- *
380
- * The hex is raised to upper case because that is the spelling
381
- * `src/slides/styles.ts` has always written, and the only one a diff of a
382
- * rendered deck reads cleanly against.
383
- */
384
- export function bareHex(value: string): string {
385
- return value.replace(/^#/, '').toUpperCase()
386
- }
package/src/docs/read.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
2
  import { basename, join } from 'node:path'
3
- import { stripFrontmatter } from '@/frontmatter'
3
+ import { stripFrontmatter } from '@/markdown/frontmatter'
4
4
  import { SURFACE_ROOTS } from '@/surface-root'
5
5
 
6
6
  const INDEX_TOPIC = 'index'
@@ -6,7 +6,7 @@ import {
6
6
  failureDetail,
7
7
  type CreateIssueOptions,
8
8
  type CreateIssueResult,
9
- } from '@/github-format'
9
+ } from '@/feedback/github-format'
10
10
 
11
11
  const GH_TIMEOUT_MS = 30_000
12
12
 
@@ -18,6 +18,7 @@ import {
18
18
  import { isOverWordCap, measureRequirements } from '@/context/requirements'
19
19
  import {
20
20
  auditSkills,
21
+ LEDGER_SKILLS,
21
22
  PRACTICE_LEDGER,
22
23
  PRACTICE_SKILLS,
23
24
  SHIPPED_CORPUS,
@@ -411,8 +412,9 @@ export const skillProvenance: Measure = async (ctx) => {
411
412
  }
412
413
 
413
414
  /**
414
- * Every listed practice skill against the closing sections and the ledger
415
- * `standards/skill-practice.md` requires of one.
415
+ * Every listed practice skill against the closing sections, and every skill on
416
+ * the wider ledger list against the ledger, both of which
417
+ * `standards/skill-practice.md` requires.
416
418
  *
417
419
  * Read in-process for the reason `skillProvenance` is, so the verb keeps its
418
420
  * missing-requirement-only exit. The list names shipped folders alone, so a
@@ -434,14 +436,14 @@ export const skillPracticeShape: Measure = async (ctx) => {
434
436
  .join(', ')
435
437
  return {
436
438
  emissions: [],
437
- failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named, or the ledger at ${PRACTICE_LEDGER}.`,
439
+ failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named to a practice skill, or the ledger at ${PRACTICE_LEDGER} to any skill on the ledger list.`,
438
440
  }
439
441
  }
440
442
 
441
443
  return {
442
444
  emissions: [
443
445
  info(
444
- `${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section and a ledger`,
446
+ `${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section, and ${plural(LEDGER_SKILLS.length, 'listed skill')} carry a ledger`,
445
447
  ),
446
448
  ],
447
449
  }
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { dirname, join, resolve } from 'node:path'
4
4
  import { $ } from 'bun'
5
- import { isMarked } from '@/exempt-marker'
5
+ import { isMarked } from '@/sweep/exempt-marker'
6
6
  import { gitEnv } from '@/git-env'
7
7
  import { listRuleFiles } from '@/gov/payload'
8
8
  import { parseFrontmatter } from '@/indexes/frontmatter'
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { basename, resolve } from 'node:path'
3
- import { stripFrontmatter } from '@/frontmatter'
3
+ import { stripFrontmatter } from '@/markdown/frontmatter'
4
4
 
5
5
  /**
6
6
  * Lists installed rules in the order the payload concatenates them. Sorting by
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
- import { isBinary } from '@/binary'
4
- import { isMarked } from '@/exempt-marker'
3
+ import { isBinary } from '@/sweep/binary'
4
+ import { isMarked } from '@/sweep/exempt-marker'
5
5
  import { listRepositoryFiles } from '@/git-files'
6
6
 
7
7
  /**
package/src/help.ts CHANGED
@@ -36,7 +36,7 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
36
36
  {
37
37
  title: 'Author and render',
38
38
  rows: [
39
- ['design [cmd]', 'Design system (render, board)'],
39
+ ['design [cmd]', 'Design system (render, regen)'],
40
40
  ['slides [cmd]', 'Slide decks (render, list)'],
41
41
  ['capture [source]', 'Render HTML capture sources to PNG'],
42
42
  ['serve [dir]', 'Serve a directory on localhost, print the link'],
@@ -102,7 +102,7 @@ const EXAMPLES = [
102
102
  'canon docs list --json',
103
103
  'canon docs agents',
104
104
  'canon design render',
105
- 'canon design board',
105
+ 'canon canvas list --json',
106
106
  'canon slides render',
107
107
  'canon slides list --json',
108
108
  'canon capture assets/captures/install.html --selector .window --out assets/evidence',
@@ -8,7 +8,7 @@ import { CHECKPOINTS, documentHeight } from '@/markdown/structure'
8
8
  *
9
9
  * The whole line rather than anywhere on it, so a standard quoting the marker
10
10
  * inside a code span to document it does not exempt itself. A marker with no
11
- * reason does not count, for the reason `isMarked` in `src/exempt-marker.ts`
11
+ * reason does not count, for the reason `isMarked` in `src/sweep/exempt-marker.ts`
12
12
  * gives: a bare token reads as a line that meant to say something and did not.
13
13
  */
14
14
  const EXEMPT_MARKER = /^<!--\s*canon-length-exempt:[ \t]*(\S.*?)\s*-->$/
@@ -40,6 +40,11 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
40
40
  * from where the document lands. Every retired name lands on the one skill, so
41
41
  * a target citing any of them resolves after a single sweep.
42
42
  *
43
+ * `draft-diagram` merged into `draft-figure` the same way, its architecture
44
+ * views becoming one reference the figure skill reads. `claude-diagram` is
45
+ * retargeted onto `draft-figure` rather than left on the retired name, for the
46
+ * same reason `claude-ui-test` was.
47
+ *
43
48
  * Every name takes two words. Ten of these would have landed as a bare single
44
49
  * word under a plain strip, and a bare word such as `review` or `docs` is a
45
50
  * substring of ordinary prose with no token left for a later sweep to find.
@@ -58,7 +63,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
58
63
  'claude-address-review': 'review-address',
59
64
  'claude-autoship': 'auto-ship',
60
65
  'claude-design-extract': 'design-extract',
61
- 'claude-diagram': 'draft-diagram',
66
+ 'claude-diagram': 'draft-figure',
62
67
  'claude-docs': 'context-fold',
63
68
  'docs-fold': 'context-fold',
64
69
  'claude-feature': 'plan-feature',
@@ -91,6 +96,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
91
96
  'draft-wireframes': 'draft-doc',
92
97
  'draft-readme': 'draft-doc',
93
98
  'draft-wiki': 'draft-doc',
99
+ 'draft-diagram': 'draft-figure',
94
100
  }
95
101
 
96
102
  /**