@erclx/canon 5.14.1 → 5.16.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 (77) hide show
  1. package/README.md +3 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/draft-screencast/REQUIREMENT.md +2 -1
  4. package/claude/skills/draft-screencast/SKILL.md +14 -9
  5. package/claude/skills/draft-wiki/SKILL.md +1 -1
  6. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  7. package/claude/skills/read-frames/SKILL.md +3 -2
  8. package/claude/skills/record-screencast/REQUIREMENT.md +3 -0
  9. package/claude/skills/record-screencast/SKILL.md +28 -5
  10. package/claude/skills/search-craft/REQUIREMENT.md +57 -0
  11. package/claude/skills/search-craft/SKILL.md +83 -0
  12. package/docs/agents/canvas.md +36 -0
  13. package/docs/agents/commands.md +3 -0
  14. package/docs/agents/demo.md +34 -7
  15. package/docs/agents/index.md +1 -0
  16. package/docs/agents/migrate.md +1 -1
  17. package/docs/agents/sandbox.md +25 -0
  18. package/docs/target/migrations.md +1 -1
  19. package/docs/workflow/ai-workflow.md +2 -3
  20. package/docs/workflow/skill-map.md +2 -1
  21. package/governance/renames.toml +7 -5
  22. package/governance/rules/canon/630-requirements.md +1 -1
  23. package/package.json +5 -2
  24. package/scripts/lib/sandbox-git.sh +2 -2
  25. package/src/canvas/address.ts +184 -0
  26. package/src/canvas/capture.ts +172 -0
  27. package/src/canvas/client/app.tsx +87 -0
  28. package/src/canvas/client/inspector.tsx +121 -0
  29. package/src/canvas/client/layers.tsx +118 -0
  30. package/src/canvas/client/main.tsx +62 -0
  31. package/src/canvas/client/pages-panel.tsx +157 -0
  32. package/src/canvas/client/shell.css +449 -0
  33. package/src/canvas/client/state.ts +372 -0
  34. package/src/canvas/client/surface.tsx +465 -0
  35. package/src/canvas/content.ts +617 -0
  36. package/src/canvas/index.html +14 -0
  37. package/src/canvas/server.ts +471 -0
  38. package/src/canvas/tokens.ts +71 -0
  39. package/src/claude/cases/misc.ts +5 -0
  40. package/src/claude/seeds.ts +0 -1
  41. package/src/cli.ts +2 -0
  42. package/src/commands/canvas.ts +522 -0
  43. package/src/commands/claude.ts +9 -14
  44. package/src/commands/demo.ts +6 -6
  45. package/src/commands/migrate.ts +1 -1
  46. package/src/commands/sandbox.ts +108 -0
  47. package/src/context/classify/prompts.ts +1 -1
  48. package/src/context/folders.ts +0 -1
  49. package/src/demo/compile.ts +8 -4
  50. package/src/demo/container.ts +1 -1
  51. package/src/gov/renames.ts +3 -2
  52. package/src/help.ts +1 -0
  53. package/src/migrate/rename.ts +5 -4
  54. package/src/record-root.ts +1 -0
  55. package/src/sandbox/equivalence.ts +847 -0
  56. package/src/serve/static.ts +25 -11
  57. package/src/surface-root.ts +0 -1
  58. package/src/teach/browser/compile.ts +44 -0
  59. package/src/teach/browser/dismiss.ts +76 -0
  60. package/src/teach/browser/focus-line.ts +19 -0
  61. package/src/teach/browser/glossary-filter.ts +44 -0
  62. package/src/teach/browser/head.ts +30 -0
  63. package/src/teach/browser/quiz.ts +19 -0
  64. package/src/teach/browser/sidebar.ts +233 -0
  65. package/src/teach/browser/theme.ts +18 -0
  66. package/src/teach/nav.ts +19 -374
  67. package/standards/architecture.md +1 -1
  68. package/standards/context.md +1 -1
  69. package/standards/design.md +1 -1
  70. package/standards/index.md +0 -1
  71. package/standards/requirements.md +4 -6
  72. package/standards/wireframes.md +2 -2
  73. package/tooling/claude/reference.md +0 -1
  74. package/tooling/claude/seeds/canon/REQUIREMENTS.md +0 -2
  75. package/governance/rules/canon/645-decisions.md +0 -12
  76. package/standards/decisions.md +0 -105
  77. package/tooling/claude/seeds/canon/decisions/index.md +0 -8
@@ -13,6 +13,11 @@ import {
13
13
  coveragePercent,
14
14
  type CoverageReport,
15
15
  } from '@/sandbox/coverage'
16
+ import {
17
+ equivalenceExitCode,
18
+ runEquivalence as collectEquivalence,
19
+ type EquivalenceRecord,
20
+ } from '@/sandbox/equivalence'
16
21
  import {
17
22
  expectFilePath,
18
23
  parseTarget,
@@ -338,6 +343,71 @@ function runCoverage(options: CoverageOptions): void {
338
343
  process.exitCode = wrongExemption || rolloutIncomplete ? 1 : 0
339
344
  }
340
345
 
346
+ interface EquivalenceOptions {
347
+ readonly base: string
348
+ readonly includeAnchor?: boolean
349
+ readonly out?: string
350
+ readonly masks: boolean
351
+ readonly json?: boolean
352
+ }
353
+
354
+ /**
355
+ * Provisions every arm on a base ref and on this checkout, then names what
356
+ * differs. The record is the only thing on stdout, and a red arm that is red the
357
+ * same way on both sides is reported without failing the run, since it proves
358
+ * nothing either way.
359
+ */
360
+ async function runEquivalence(
361
+ targets: string[],
362
+ options: EquivalenceOptions,
363
+ ): Promise<void> {
364
+ if (reportAbsentScenarioTree()) return
365
+
366
+ intro('canon sandbox equivalence')
367
+
368
+ let record: EquivalenceRecord
369
+ try {
370
+ record = await collectEquivalence({
371
+ root: PROJECT_ROOT,
372
+ targets,
373
+ base: options.base,
374
+ includeAnchor: options.includeAnchor === true,
375
+ useMasks: options.masks,
376
+ out: options.out,
377
+ })
378
+ } catch (error) {
379
+ const reason = error instanceof Error ? error.message : String(error)
380
+ logError(`Cannot provision the baseline ${options.base}: ${reason}`)
381
+ outro()
382
+ process.exitCode = 1
383
+
384
+ return
385
+ }
386
+
387
+ logStep(`Base ${record.base} (${record.baseCommit.slice(0, 8)})`)
388
+ logInfo(`canon ${record.canonVersion}`)
389
+
390
+ for (const arm of record.arms) {
391
+ if (arm.state === 'identical' || arm.state === 'skipped-anchor') continue
392
+ const detail = arm.differs.length > 0 ? `: ${arm.differs.join(', ')}` : ''
393
+ if (arm.state === 'differs') logRemove(`${arm.arm} differs${detail}`)
394
+ else logWarn(`${arm.arm} is ${arm.state}`)
395
+ }
396
+ for (const line of record.masksApplied) logInfo(`masked ${line}`)
397
+ for (const error of record.errors) logError(error)
398
+
399
+ const { counts } = record
400
+ logStep('Equivalence')
401
+ const summary = `${counts.identical} identical, ${counts.differs} differs, ${counts['red-on-base']} red-on-base, ${counts['skipped-anchor']} skipped-anchor`
402
+ if (equivalenceExitCode(record) === 0) logInfo(summary)
403
+ else logWarn(summary)
404
+
405
+ if (options.json === true) process.stdout.write(`${JSON.stringify(record)}\n`)
406
+
407
+ outro()
408
+ process.exitCode = equivalenceExitCode(record)
409
+ }
410
+
341
411
  function runCheck(
342
412
  target: string,
343
413
  arm: string | undefined,
@@ -482,4 +552,42 @@ export function register(program: Command): void {
482
552
  .action((options: CoverageOptions) => {
483
553
  runCoverage(options)
484
554
  })
555
+
556
+ sandbox
557
+ .command('equivalence')
558
+ .description(
559
+ 'Provision every arm on a base ref and on this checkout, and diff the trees',
560
+ )
561
+ .argument(
562
+ '[target...]',
563
+ 'Category or <category>:<command>, defaulting to every category',
564
+ )
565
+ .helpOption('-h, --help', 'Show this help message')
566
+ .option('--base <ref>', 'Ref the baseline is provisioned from', 'main')
567
+ .option(
568
+ '--include-anchor',
569
+ 'Also run the arms that push to the shared GitHub anchor, one at a time (attended)',
570
+ )
571
+ .option('--out <dir>', 'Keep the provisioned trees and logs in this folder')
572
+ .option(
573
+ '--no-masks',
574
+ 'Compare every path, listing what each mask would hide',
575
+ )
576
+ .option('--json', 'Emit the record as JSON on stdout')
577
+ .addHelpText(
578
+ 'after',
579
+ [
580
+ '',
581
+ 'Examples:',
582
+ ' canon sandbox equivalence claude --json',
583
+ ' canon sandbox equivalence claude:plan-feature --base origin/main',
584
+ '',
585
+ 'Provisions offline only and never runs sandbox/run.sh or a claude binary.',
586
+ 'Exit codes: 0 when every compared arm is identical or red the same way',
587
+ 'on both sides, 1 on any difference, exit mismatch, or enumeration error.',
588
+ ].join('\n'),
589
+ )
590
+ .action((targets: string[], options: EquivalenceOptions) =>
591
+ runEquivalence(targets, options),
592
+ )
485
593
  }
@@ -20,7 +20,7 @@ File types and what they hold:
20
20
  Verdicts (pick exactly one):
21
21
  - KEEP: states current design, a rule, copy, a decision with its rejected alternative, or a live gotcha, on the right surface.
22
22
  - REPLACE: restates a fact or figure the existing section already carries ("moved again", "now", "the count moved", a newer number appended after an older one). The old statement should be rewritten in place, not appended to.
23
- - HISTORY: narrates how things got here: which branch/PR changed what, "closed on", "did not survive", review-pass stories, pick-by-pick rounds. The current state is what belongs; the trail goes to the PR or a decision log.
23
+ - HISTORY: narrates how things got here: which branch/PR changed what, "closed on", "did not survive", review-pass stories, pick-by-pick rounds. The current state is what belongs; the trail goes to the PR.
24
24
  - MOVE: correct content on the wrong surface (domain mechanism in ARCHITECTURE.md, implementation detail in a wireframe).
25
25
 
26
26
  Return only JSON: {"verdict": "...", "quote": "<the shortest added phrase that decided it>", "reason": "<one sentence>"}
@@ -19,7 +19,6 @@ import { SURFACE_ROOTS } from '@/surface-root'
19
19
  */
20
20
  export const DEFAULT_FOLDERS: readonly string[] = [
21
21
  'context',
22
- 'decisions',
23
22
  'diagrams',
24
23
  'wireframes',
25
24
  ]
@@ -134,8 +134,8 @@ export function compilePlan(draft: Draft, options: CompileOptions): DemoPlan {
134
134
  url: '',
135
135
  viewport: VIEWPORT,
136
136
  output: {
137
- video: `${options.outDir}/${options.slug}.webm`,
138
- still: `${options.outDir}/${options.slug}.png`,
137
+ video: `${options.outDir}/${options.slug}/take/${options.slug}.webm`,
138
+ still: `${options.outDir}/${options.slug}/take/${options.slug}.png`,
139
139
  },
140
140
  pointer: { travelMs: POINTER_TRAVEL_MS, typeDelayMs: TYPE_DELAY_MS },
141
141
  annotations: ANNOTATIONS,
@@ -252,8 +252,12 @@ export function parsePlan(text: string): PlanParse {
252
252
  },
253
253
  ...(colorScheme.value && { colorScheme: colorScheme.value }),
254
254
  output: {
255
- video: asText(output.video) || `demos/${slug || 'demo'}.webm`,
256
- still: asText(output.still) || `demos/${slug || 'demo'}.png`,
255
+ video:
256
+ asText(output.video) ||
257
+ `demos/${slug || 'demo'}/take/${slug || 'demo'}.webm`,
258
+ still:
259
+ asText(output.still) ||
260
+ `demos/${slug || 'demo'}/take/${slug || 'demo'}.png`,
257
261
  },
258
262
  pointer: {
259
263
  travelMs: asNumber(pointer.travelMs, POINTER_TRAVEL_MS),
@@ -135,7 +135,7 @@ export async function convertToGif(
135
135
 
136
136
  /**
137
137
  * Written beside the video by default, so frames fall under the same
138
- * `demos/*.png` gitignore entry the still already uses with no new rule
138
+ * `take` folder gitignore entry the still already uses with no new rule
139
139
  * needed. One frame a second by default, matched to the seconds-to-tens-of-
140
140
  * seconds length a tuned recording runs, without a smarter sampling strategy
141
141
  * nobody has asked for.
@@ -34,8 +34,9 @@ export function loadRenames(root: string): Renames {
34
34
 
35
35
  /**
36
36
  * Follows a declared rename to the name the toolkit ships the rule under now,
37
- * through every later rename of that name. The ledger is never pruned, so a
38
- * target several releases behind still reaches the current name.
37
+ * through every later rename of that name. The ledger is pruned only once a
38
+ * rule is retired, so a target several releases behind still reaches the
39
+ * current name of every rule that still ships.
39
40
  *
40
41
  * Returns nothing when the chain ends on a name with no source, or loops,
41
42
  * since installing a guess is worse than the retire the caller falls back to.
package/src/help.ts CHANGED
@@ -45,6 +45,7 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
45
45
  ['drive <url> <run>', 'Walk a page through interactions, measure each'],
46
46
  ['transcripts <url>', 'Fetch a YouTube transcript with frontmatter'],
47
47
  ['teach [cmd]', 'Learning workspaces (list, open, resource, glossary)'],
48
+ ['canvas [cmd]', 'Local canvas of pages and frames (serve, list)'],
48
49
  ],
49
50
  },
50
51
  {
@@ -168,10 +168,11 @@ const AITK_ARTICLE = /\b([Aa])n(\s+`?)(canon|CANON|Canon)(?![A-Za-z])/g
168
168
  /**
169
169
  * The `aitk` to `canon` rename.
170
170
  *
171
- * `aitk-sandbox` is a separate repository that is not being renamed. It has to
172
- * win against the bare token, and it also has to win against the owner-scoped
173
- * spelling, since `erclx/aitk-sandbox` would otherwise rewrite to
174
- * `erclx/canon-sandbox` and name a repository that does not exist.
171
+ * `aitk-sandbox` is the retired spelling of a repository that was later renamed
172
+ * `canon-sandbox`, and GitHub still resolves the old name through a redirect.
173
+ * It has to win against the bare token, and it also has to win against the
174
+ * owner-scoped spelling, so a consumer's text naming `erclx/aitk-sandbox` keeps
175
+ * the form that resolves rather than being rewritten.
175
176
  *
176
177
  * Case is carried in the map rather than derived, because the uppercase form
177
178
  * is an environment variable prefix and the title-case form is a heading word,
@@ -64,6 +64,7 @@ export const RECORD_ENTRIES: readonly string[] = [
64
64
  '.records.git',
65
65
  SCRATCH,
66
66
  'README.md',
67
+ 'canvas',
67
68
  'diagrams',
68
69
  'evidence',
69
70
  'feedback',