@erclx/canon 5.28.0 → 5.30.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 (81) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/review-ui/references/post.md +7 -5
  3. package/docs/agents/canvas.md +8 -0
  4. package/docs/agents/pr-evidence.md +7 -7
  5. package/package.json +1 -1
  6. package/src/canvas/client/app.tsx +105 -4
  7. package/src/canvas/client/inspector.tsx +13 -4
  8. package/src/canvas/client/keys.ts +23 -6
  9. package/src/canvas/client/main.tsx +9 -0
  10. package/src/canvas/client/pages-panel.tsx +5 -1
  11. package/src/canvas/client/panel-handle.tsx +90 -0
  12. package/src/canvas/client/shell.css +86 -2
  13. package/src/canvas/client/state.ts +159 -6
  14. package/src/canvas/client/surface.tsx +37 -1
  15. package/src/canvas/client/tools.tsx +91 -2
  16. package/src/canvas/content.ts +65 -0
  17. package/src/canvas/edit.ts +195 -23
  18. package/src/canvas/history.ts +116 -0
  19. package/src/canvas/server.ts +126 -13
  20. package/src/{exec.ts → cli/exec.ts} +1 -1
  21. package/src/{help.ts → cli/help.ts} +1 -1
  22. package/src/cli.ts +1 -1
  23. package/src/commands/audits.ts +9 -1
  24. package/src/commands/autoship.ts +1 -1
  25. package/src/commands/canvas.ts +1 -1
  26. package/src/commands/capture.ts +1 -1
  27. package/src/commands/census.ts +1 -1
  28. package/src/commands/claude.ts +2 -2
  29. package/src/commands/comments.ts +1 -1
  30. package/src/commands/context.ts +1 -1
  31. package/src/commands/demo.ts +9 -1
  32. package/src/commands/deps.ts +1 -1
  33. package/src/commands/design.ts +9 -1
  34. package/src/commands/docs.ts +1 -1
  35. package/src/commands/driver.ts +9 -1
  36. package/src/commands/feedback.ts +1 -1
  37. package/src/commands/gate.ts +1 -1
  38. package/src/commands/gov.ts +1 -1
  39. package/src/commands/indexes.ts +1 -1
  40. package/src/commands/init.ts +10 -2
  41. package/src/commands/intake.ts +1 -1
  42. package/src/commands/inventory.ts +9 -1
  43. package/src/commands/labels.ts +1 -1
  44. package/src/commands/markdown.ts +1 -1
  45. package/src/commands/migrate.ts +1 -1
  46. package/src/commands/pr.ts +141 -2
  47. package/src/commands/records.ts +1 -1
  48. package/src/commands/repo.ts +1 -1
  49. package/src/commands/sandbox.ts +1 -1
  50. package/src/commands/secrets.ts +9 -1
  51. package/src/commands/sessions.ts +1 -1
  52. package/src/commands/slides.ts +1 -1
  53. package/src/commands/standards.ts +1 -1
  54. package/src/commands/sync.ts +2 -2
  55. package/src/commands/targets.ts +1 -1
  56. package/src/commands/tasks.ts +1 -1
  57. package/src/commands/teach.ts +1 -1
  58. package/src/commands/tooling.ts +1 -1
  59. package/src/commands/transcripts.ts +1 -1
  60. package/src/commands/upgrade.ts +1 -1
  61. package/src/commands/wiki.ts +1 -1
  62. package/src/commands/worktrees.ts +1 -1
  63. package/src/design/brand-favicon.ts +18 -0
  64. package/src/gate/measures.ts +1 -1
  65. package/src/init/run.ts +1 -1
  66. package/src/migrate/rename.ts +1 -1
  67. package/src/pr/evidence.ts +41 -7
  68. package/src/roots/project.ts +1 -1
  69. package/src/sandbox/headless/run.ts +1 -1
  70. package/src/sandbox/provision.ts +1 -1
  71. package/src/sandbox/scenario.ts +1 -1
  72. package/src/serve/report.ts +1 -1
  73. package/src/sync/engine.ts +1 -1
  74. package/src/sync/git.ts +1 -1
  75. package/src/sync/workflow.ts +1 -1
  76. package/src/targets/validate.ts +1 -1
  77. package/src/teach/nav.ts +5 -12
  78. package/src/tooling/inject.ts +1 -1
  79. package/src/tooling/verify-stack.ts +1 -1
  80. /package/src/{cli-run.ts → cli/run.ts} +0 -0
  81. /package/src/{ui.ts → cli/ui.ts} +0 -0
@@ -57,6 +57,10 @@ export interface EvidenceItem {
57
57
  readonly stem: string
58
58
  /** True when `path` did not exist at the comparison's base commit. */
59
59
  readonly added: boolean
60
+ /** Pixel width of the head image, read from its PNG header. Absent when unread. */
61
+ readonly width?: number
62
+ /** Pixel width of the base image, absent for an added case or an unread header. */
63
+ readonly baseWidth?: number
60
64
  }
61
65
 
62
66
  export interface EvidenceState {
@@ -140,12 +144,33 @@ function rawUrl(repo: string, sha: string, path: string): string {
140
144
  }
141
145
 
142
146
  /**
143
- * The pixel width every screenshot asks for. GitHub sizes a comment table to
144
- * its content and clamps an image to its cell, so a width past any comment
145
- * fills the cell, where `width="100%"` leaves a narrow screenshot narrow.
147
+ * The most pixels a screenshot asks for. GitHub sizes a comment table to its
148
+ * content and clamps an image to its cell, so a width past any comment fills
149
+ * the cell, where `width="100%"` leaves a narrow screenshot narrow. An image
150
+ * asks for the smaller of its own width and this ceiling, so a small crop
151
+ * shows at its true size and is never scaled up. An image whose width was not
152
+ * read asks for the ceiling.
146
153
  */
147
154
  const IMAGE_WIDTH = 1000
148
155
 
156
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
157
+
158
+ /** Bytes of a PNG that reach the end of IHDR's width field. */
159
+ export const PNG_HEADER_BYTES = 24
160
+
161
+ /**
162
+ * The pixel width in a PNG's IHDR chunk, or undefined for bytes that are not
163
+ * a PNG with a header or that carry a zero width.
164
+ */
165
+ export function readPngWidth(bytes: Uint8Array): number | undefined {
166
+ if (bytes.length < PNG_HEADER_BYTES) return undefined
167
+ if (PNG_SIGNATURE.some((byte, index) => bytes[index] !== byte)) {
168
+ return undefined
169
+ }
170
+ const width = new DataView(bytes.buffer, bytes.byteOffset).getUint32(16)
171
+ return width > 0 ? width : undefined
172
+ }
173
+
149
174
  /** A table cell and an attribute value share one escape, since a stem is a path segment placed in both. */
150
175
  function escapeCell(text: string): string {
151
176
  return text
@@ -156,8 +181,9 @@ function escapeCell(text: string): string {
156
181
  .replaceAll('|', '&#124;')
157
182
  }
158
183
 
159
- function evidenceImage(url: string, alt: string): string {
160
- return `<img src="${url}" width="${IMAGE_WIDTH}" alt="${alt}">`
184
+ function evidenceImage(url: string, alt: string, width?: number): string {
185
+ const shown = Math.min(width ?? IMAGE_WIDTH, IMAGE_WIDTH)
186
+ return `<img src="${url}" width="${shown}" alt="${alt}">`
161
187
  }
162
188
 
163
189
  const EVIDENCE_IMAGE = String.raw`(?:!\[\]\([^)]*\)|<img [^>]*>)`
@@ -232,8 +258,16 @@ export function renderEvidenceBody(
232
258
  const stem = escapeCell(item.stem)
233
259
  const before = item.added
234
260
  ? '*(new)*'
235
- : evidenceImage(rawUrl(repo, base, item.path), `${stem} base`)
236
- const after = evidenceImage(rawUrl(repo, head, item.path), `${stem} head`)
261
+ : evidenceImage(
262
+ rawUrl(repo, base, item.path),
263
+ `${stem} base`,
264
+ item.baseWidth,
265
+ )
266
+ const after = evidenceImage(
267
+ rawUrl(repo, head, item.path),
268
+ `${stem} head`,
269
+ item.width,
270
+ )
237
271
  return hasBase
238
272
  ? `| ${stem} | ${before} | ${after} |`
239
273
  : `| ${stem} | ${after} |`
@@ -72,7 +72,7 @@ export function findCheckoutMismatch(startDir: string): string | undefined {
72
72
  * The warning line every verb that answers from `PROJECT_ROOT` emits when the
73
73
  * caller's cwd sits inside a second checkout, or `undefined` when it does not.
74
74
  *
75
- * It returns the string rather than logging it so this module keeps no `@/ui`
75
+ * It returns the string rather than logging it so this module keeps no `@/cli/ui`
76
76
  * dependency and the wording is unit-testable on its own. The wording names
77
77
  * both roots and no subcommand: the four chokepoints that carry it stand under
78
78
  * eight-plus verbs each, so a per-verb suffix would cost a parameter at every
@@ -37,7 +37,7 @@ import {
37
37
  logStep,
38
38
  logWarn,
39
39
  outro,
40
- } from '@/ui'
40
+ } from '@/cli/ui'
41
41
 
42
42
  /**
43
43
  * The flags and budget every run takes, each overridable through its
@@ -30,7 +30,7 @@ import {
30
30
  outro,
31
31
  palette,
32
32
  select,
33
- } from '@/ui'
33
+ } from '@/cli/ui'
34
34
 
35
35
  /**
36
36
  * Provisions one scenario into the sandbox tree, and resets or cleans that tree.
@@ -11,7 +11,7 @@ import {
11
11
  } from 'node:fs'
12
12
  import { createRequire } from 'node:module'
13
13
  import { dirname, join, relative } from 'node:path'
14
- import { logError, logInfo, logStep, logWarn, palette, select } from '@/ui'
14
+ import { logError, logInfo, logStep, logWarn, palette, select } from '@/cli/ui'
15
15
 
16
16
  /**
17
17
  * The TypeScript form of a sandbox scenario: `sandbox/<category>/<command>.ts`,
@@ -4,7 +4,7 @@ import {
4
4
  type ServeOutcome,
5
5
  type ServeStarted,
6
6
  } from '@/serve/static'
7
- import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
7
+ import { intro, logError, logInfo, logStep, logWarn, outro } from '@/cli/ui'
8
8
 
9
9
  /**
10
10
  * The relative path where it stays inside the working directory, and the
@@ -28,7 +28,7 @@ import {
28
28
  outro,
29
29
  palette,
30
30
  select,
31
- } from '@/ui'
31
+ } from '@/cli/ui'
32
32
  import { compareVersions, parseVersion } from '@/version/compare'
33
33
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
34
34
 
package/src/sync/git.ts CHANGED
@@ -2,7 +2,7 @@ import { basename } from 'node:path'
2
2
  import { execa } from 'execa'
3
3
  import { gitEnv } from '@/git/env'
4
4
  import type { SyncDomain } from '@/sync/target'
5
- import { pipeOutput } from '@/ui'
5
+ import { pipeOutput } from '@/cli/ui'
6
6
 
7
7
  const FILE_LIST_MAX = 3
8
8
 
@@ -22,7 +22,7 @@ import {
22
22
  palette,
23
23
  pipeOutput,
24
24
  select,
25
- } from '@/ui'
25
+ } from '@/cli/ui'
26
26
 
27
27
  const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
28
28
 
@@ -1,7 +1,7 @@
1
1
  import { statSync } from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
3
  import { checkoutMismatchWarning } from '@/roots/project'
4
- import { logError, logWarn, outro } from '@/ui'
4
+ import { logError, logWarn, outro } from '@/cli/ui'
5
5
 
6
6
  export function isDirectory(path: string): boolean {
7
7
  try {
package/src/teach/nav.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs'
1
+ import { existsSync } from 'node:fs'
2
2
  import { readdir, readFile, rm } from 'node:fs/promises'
3
3
  import { join, posix, relative } from 'node:path'
4
4
  import {
@@ -6,10 +6,10 @@ import {
6
6
  TEACH_STYLESHEET_COMPONENTS,
7
7
  } from '@/design/components'
8
8
  import { buildDesignCss } from '@/design/css'
9
- import { FAVICON_COLORS, faviconLink, renderFavicon } from '@/design/favicon'
9
+ import { brandFavicon } from '@/design/brand-favicon'
10
+ import { faviconLink } from '@/design/favicon'
10
11
  import { stripFrontmatter } from '@/markdown/frontmatter'
11
12
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
12
- import { PROJECT_ROOT } from '@/roots/project'
13
13
  import { compileScript } from '@/teach/browser/compile'
14
14
  import { dismiss } from '@/teach/browser/dismiss'
15
15
  import { focusLine } from '@/teach/browser/focus-line'
@@ -39,8 +39,6 @@ import {
39
39
  } from '@/teach/workspace'
40
40
  import { writeIfChanged } from '@/teach/write-if-changed'
41
41
 
42
- const BRAND_MARK = 'assets/brand/mark.svg'
43
-
44
42
  /**
45
43
  * The link `teach-workspace` once had a session write by hand into every page.
46
44
  * A lesson keeps whatever sits outside its marked regions, so the old icon
@@ -68,14 +66,9 @@ function dropHandWrittenHead(html: string): string {
68
66
  )
69
67
  }
70
68
 
71
- /**
72
- * Built from the brand mark and the favicon's own pair, the same two inputs
73
- * `web/public/favicon.svg` is generated from, so every teach page carries the
74
- * icon the landing page does rather than a copy of its own.
75
- */
69
+ /** Every teach page carries the icon the landing page does rather than a copy of its own. */
76
70
  function teachFavicon(): string {
77
- const mark = readFileSync(join(PROJECT_ROOT, BRAND_MARK), 'utf8')
78
- return faviconLink(renderFavicon(mark, FAVICON_COLORS))
71
+ return faviconLink(brandFavicon())
79
72
  }
80
73
 
81
74
  /**
@@ -27,7 +27,7 @@ import {
27
27
  SUBFOLDER_SPELL_CONFIG,
28
28
  subfolderSpellConfig,
29
29
  } from '@/tooling/subfolder'
30
- import { logAdd, logRemove, logStep, logWarn } from '@/ui'
30
+ import { logAdd, logRemove, logStep, logWarn } from '@/cli/ui'
31
31
 
32
32
  export async function injectConfigs(
33
33
  chain: readonly Manifest[],
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { execa, type Options } from 'execa'
4
4
  import { isStackExcluded, loadManifest } from '@/tooling/manifest'
5
- import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
5
+ import { intro, logError, logInfo, logStep, logWarn, outro } from '@/cli/ui'
6
6
 
7
7
  const SCREENSHOT_TIMEOUT_MS = 60_000
8
8
  const TAIL_LINES = 20
File without changes
File without changes