@erclx/canon 5.32.0 → 5.34.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 (250) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/REQUIREMENT.md +1 -2
  3. package/claude/skills/api-design/SKILL.md +2 -3
  4. package/claude/skills/api-design/references/adopted.md +1 -1
  5. package/claude/skills/auto-ship/SKILL.md +1 -1
  6. package/claude/skills/auto-ship/references/worktree-name.md +1 -1
  7. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  8. package/claude/skills/backlog-triage/SKILL.md +1 -1
  9. package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
  10. package/claude/skills/canon-cli/SKILL.md +75 -32
  11. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
  12. package/claude/skills/canon-feedback/SKILL.md +11 -29
  13. package/claude/skills/canon-feedback/references/template.md +53 -0
  14. package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
  15. package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
  16. package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
  17. package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
  18. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/code-craft/SKILL.md +0 -1
  20. package/claude/skills/context-fold/REQUIREMENT.md +25 -33
  21. package/claude/skills/context-fold/SKILL.md +25 -70
  22. package/claude/skills/context-fold/references/classify.md +3 -3
  23. package/claude/skills/context-fold/references/completion-report.md +4 -4
  24. package/claude/skills/context-fold/references/context-refresh.md +5 -5
  25. package/{standards → claude/skills/context-fold/references}/context.md +16 -5
  26. package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
  27. package/claude/skills/create-rule/SKILL.md +3 -3
  28. package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
  29. package/claude/skills/create-skill/SKILL.md +2 -2
  30. package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
  31. package/claude/skills/design-taste/REQUIREMENT.md +3 -3
  32. package/claude/skills/design-taste/SKILL.md +1 -1
  33. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  34. package/claude/skills/docs-sync/SKILL.md +3 -3
  35. package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
  36. package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
  37. package/claude/skills/draft-and-pick/SKILL.md +5 -5
  38. package/claude/skills/draft-doc/SKILL.md +6 -6
  39. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  40. package/claude/skills/draft-figure/SKILL.md +2 -2
  41. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  42. package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
  43. package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
  44. package/claude/skills/draft-identity/SKILL.md +2 -2
  45. package/claude/skills/draft-ready/SKILL.md +1 -1
  46. package/claude/skills/git-branch/SKILL.md +1 -1
  47. package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
  48. package/claude/skills/git-commit/SKILL.md +2 -2
  49. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  50. package/claude/skills/git-followup/SKILL.md +1 -1
  51. package/claude/skills/git-issue/SKILL.md +1 -1
  52. package/claude/skills/git-issue/references/issue.md +4 -1
  53. package/claude/skills/git-pr/SKILL.md +13 -13
  54. package/claude/skills/git-pr/references/evidence.md +2 -2
  55. package/claude/skills/git-pr/references/final-command.md +38 -14
  56. package/claude/skills/git-pr/references/labels.md +5 -1
  57. package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
  58. package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
  59. package/claude/skills/git-pr/references/task-number.md +1 -1
  60. package/claude/skills/git-ship/SKILL.md +15 -14
  61. package/claude/skills/git-split/SKILL.md +3 -3
  62. package/claude/skills/git-stage/SKILL.md +1 -1
  63. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  64. package/claude/skills/review-address/SKILL.md +1 -1
  65. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  66. package/claude/skills/review-craft/references/ui.md +1 -1
  67. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  68. package/claude/skills/review-pr/SKILL.md +3 -3
  69. package/claude/skills/review-pr/references/body-variants.md +2 -2
  70. package/claude/skills/review-pr/references/key-changes.md +3 -3
  71. package/claude/skills/review-pr/references/post.md +4 -2
  72. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  73. package/claude/skills/review-ui/SKILL.md +8 -6
  74. package/claude/skills/review-ui/references/driving.md +8 -8
  75. package/claude/skills/review-ui/references/post.md +53 -17
  76. package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
  77. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  78. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
  79. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
  80. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
  81. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  82. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
  83. package/claude/skills/role-planner/REQUIREMENT.md +2 -1
  84. package/claude/skills/role-planner/SKILL.md +13 -2
  85. package/claude/skills/role-reviewer/SKILL.md +1 -1
  86. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  87. package/claude/skills/role-worker/SKILL.md +9 -1
  88. package/claude/skills/session-map/SKILL.md +1 -1
  89. package/claude/skills/session-resume/REQUIREMENT.md +2 -2
  90. package/claude/skills/session-worktree/SKILL.md +5 -5
  91. package/claude/skills/target-setup/SKILL.md +10 -4
  92. package/claude/skills/target-setup/references/detect.md +20 -3
  93. package/claude/skills/task-board/REQUIREMENT.md +7 -12
  94. package/claude/skills/task-board/SKILL.md +5 -17
  95. package/claude/skills/task-board/references/tasks.md +2 -2
  96. package/claude/skills/task-complete/REQUIREMENT.md +47 -0
  97. package/claude/skills/task-complete/SKILL.md +86 -0
  98. package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
  99. package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
  100. package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
  101. package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
  102. package/claude/skills/teach-workspace/SKILL.md +8 -4
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  104. package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
  105. package/claude/skills/teach-workspace/references/promotion.md +3 -3
  106. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  107. package/claude/skills/test-craft/SKILL.md +15 -3
  108. package/claude/skills/ui-checklist/SKILL.md +1 -1
  109. package/claude/skills/ux-audit/SKILL.md +1 -1
  110. package/claude/skills/ux-measure/SKILL.md +1 -1
  111. package/docs/agents/canvas.md +6 -2
  112. package/docs/agents/commands.md +4 -3
  113. package/docs/agents/context-audit-checks.md +4 -4
  114. package/docs/agents/gov-detect.md +47 -0
  115. package/docs/agents/index.md +7 -4
  116. package/docs/agents/indexes.md +1 -1
  117. package/docs/agents/install-and-sync.md +10 -11
  118. package/docs/agents/key-changes.md +5 -5
  119. package/docs/agents/label-coverage.md +11 -0
  120. package/docs/agents/migrate.md +1 -1
  121. package/docs/agents/pr-evidence-layout.md +70 -0
  122. package/docs/agents/pr-evidence.md +8 -9
  123. package/docs/agents/pr-frames.md +7 -2
  124. package/docs/agents/pr-lift.md +132 -0
  125. package/docs/agents/records-push.md +2 -2
  126. package/docs/agents/records-stale.md +12 -47
  127. package/docs/agents/records-validate.md +3 -7
  128. package/docs/agents/records.md +5 -27
  129. package/docs/agents/rule-citations.md +1 -1
  130. package/docs/agents/sandbox.md +4 -4
  131. package/docs/agents/scripting.md +2 -1
  132. package/docs/agents/skills-audit.md +1 -1
  133. package/docs/agents/tasks-plans.md +2 -2
  134. package/docs/agents/tasks.md +2 -2
  135. package/docs/target/migrations.md +10 -2
  136. package/docs/target/projects.md +4 -4
  137. package/docs/target/sync.md +1 -1
  138. package/docs/workflow/operating-model.md +2 -2
  139. package/docs/workflow/parallel-features.md +7 -7
  140. package/docs/workflow/skill-map-on-request.md +9 -9
  141. package/docs/workflow/skill-map.md +12 -11
  142. package/docs/workflow/visual-design-workflow.md +2 -2
  143. package/governance/detect.toml +201 -0
  144. package/governance/renames.toml +0 -11
  145. package/governance/rules/canon/context.md +13 -0
  146. package/governance/rules/claude/skill.md +17 -0
  147. package/governance/rules/code/000-code.md +1 -13
  148. package/governance/rules/code/010-testing.md +2 -30
  149. package/governance/rules/ui/400-ui.md +1 -1
  150. package/governance/rules/ui/480-canvas.md +13 -0
  151. package/governance/stacks/base.toml +2 -4
  152. package/package.json +4 -4
  153. package/scripts/core/install-check.sh +3 -3
  154. package/scripts/core/regen-hero.ts +0 -6
  155. package/scripts/lib/ui.sh +0 -168
  156. package/src/audits/catalog.ts +0 -1
  157. package/src/canvas/arrange.ts +66 -0
  158. package/src/canvas/capture.ts +20 -6
  159. package/src/canvas/content.ts +39 -0
  160. package/src/canvas/fit.ts +238 -0
  161. package/src/canvas/tokens.ts +28 -10
  162. package/src/capture/frame-values.ts +2 -32
  163. package/src/claude/cases/misc.ts +1 -1
  164. package/src/claude/cases/setup.ts +4 -0
  165. package/src/claude/cases/workflow.ts +6 -1
  166. package/src/claude/seeds.ts +1 -1
  167. package/src/claude/skills-audit.ts +2 -2
  168. package/src/cli/help.ts +3 -1
  169. package/src/cli/ui.ts +2 -3
  170. package/src/commands/canvas.ts +179 -1
  171. package/src/commands/claude.ts +1 -1
  172. package/src/commands/gov.ts +146 -16
  173. package/src/commands/labels.ts +326 -7
  174. package/src/commands/migrate.ts +6 -1
  175. package/src/commands/pr.ts +377 -2
  176. package/src/commands/records.ts +13 -399
  177. package/src/commands/sandbox.ts +3 -6
  178. package/src/commands/tasks.ts +3 -3
  179. package/src/context/audit.ts +5 -5
  180. package/src/context/folders.ts +1 -1
  181. package/src/design/budget.ts +1 -1
  182. package/src/design/components.ts +1 -1
  183. package/src/design/document.ts +1 -1
  184. package/src/design/regen.ts +1 -1
  185. package/src/design/render.ts +1 -1
  186. package/src/feedback/body.ts +26 -5
  187. package/src/gate/measures.ts +59 -45
  188. package/src/gate/sequencer.ts +56 -1
  189. package/src/gate/stages.ts +4 -11
  190. package/src/gov/detect.ts +301 -0
  191. package/src/gov/install.ts +2 -3
  192. package/src/gov/list.ts +23 -2
  193. package/src/gov/stacks.ts +10 -1
  194. package/src/hooks/post-merge.ts +1 -1
  195. package/src/labels/apply.ts +39 -0
  196. package/src/labels/format.ts +3 -3
  197. package/src/labels/phase.ts +3 -3
  198. package/src/labels/sections.ts +43 -0
  199. package/src/migrate/skill-names.ts +20 -1
  200. package/src/pr/evidence.ts +218 -45
  201. package/src/pr/frames.ts +23 -3
  202. package/src/pr/lift.ts +418 -0
  203. package/src/pr/paths.ts +4 -4
  204. package/src/pr/rest.ts +3 -0
  205. package/src/pr/review-scope.ts +76 -0
  206. package/src/records/backup.ts +16 -6
  207. package/src/records/stale-canonical.ts +31 -1
  208. package/src/records/validate.ts +2 -188
  209. package/src/sandbox/census.ts +1 -1
  210. package/src/sandbox/coverage.ts +2 -6
  211. package/src/sandbox/equivalence.ts +19 -51
  212. package/src/sandbox/expect.ts +4 -4
  213. package/src/sandbox/provision.ts +13 -36
  214. package/src/sandbox/scenario.ts +26 -48
  215. package/src/sandbox/tree.ts +2 -6
  216. package/src/standards/read.ts +14 -1
  217. package/src/sync/git.ts +1 -1
  218. package/src/tasks/archive.ts +5 -5
  219. package/src/tasks/branch.ts +3 -3
  220. package/src/tasks/label.ts +1 -1
  221. package/src/tasks/record.ts +2 -2
  222. package/src/tasks/validate.ts +1 -1
  223. package/standards/index.md +0 -13
  224. package/standards/standard.md +1 -7
  225. package/tooling/base/reference.md +1 -1
  226. package/tooling/base/seeds/.github/pull_request_template.md +2 -2
  227. package/tooling/claude/reference.md +1 -1
  228. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
  229. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
  230. package/tooling/claude/seeds/.claude/settings.json +9 -0
  231. package/governance/rules/standards/context.md +0 -14
  232. package/governance/rules/standards/design.md +0 -12
  233. package/governance/rules/standards/readme.md +0 -12
  234. package/governance/rules/standards/rule.md +0 -13
  235. package/governance/rules/standards/skill.md +0 -21
  236. package/governance/rules/standards/standard.md +0 -13
  237. package/scripts/config.sh +0 -11
  238. package/scripts/lib/gov.sh +0 -14
  239. package/scripts/lib/sandbox-fixtures.sh +0 -191
  240. package/scripts/lib/sandbox-git.sh +0 -125
  241. package/scripts/lib/sandbox-path.sh +0 -50
  242. package/scripts/sandbox-hook.sh +0 -99
  243. package/src/gov/standard-rules.ts +0 -179
  244. package/src/records/migrate.ts +0 -78
  245. package/src/records/stale.ts +0 -243
  246. package/src/sandbox/hooks.ts +0 -128
  247. /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
  248. /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
  249. /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
  250. /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
@@ -0,0 +1,66 @@
1
+ import { type Box, FRAME_GAP, type Frame } from '@/canvas/content'
2
+
3
+ export interface Placement {
4
+ readonly name: string
5
+ readonly from: Box
6
+ readonly to: Box
7
+ }
8
+
9
+ function boxOf(frame: Frame): Box {
10
+ return {
11
+ x: frame.x,
12
+ y: frame.y,
13
+ width: frame.width,
14
+ height: frame.height,
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Boxes whose vertical spans overlap share a row, so a row the operator dragged
20
+ * stays a row. Reading by overlap rather than by an equal `y` lets a frame a
21
+ * few pixels off its neighbor still join it.
22
+ */
23
+ function rowsOf(frames: readonly Frame[]): Frame[][] {
24
+ const sorted = [...frames].sort((a, b) => a.y - b.y || a.x - b.x)
25
+ const rows: Frame[][] = []
26
+ let bottom = Number.NEGATIVE_INFINITY
27
+ for (const frame of sorted) {
28
+ const row = rows.at(-1)
29
+ if (row && frame.y < bottom) {
30
+ row.push(frame)
31
+ bottom = Math.max(bottom, frame.y + frame.height)
32
+ } else {
33
+ rows.push([frame])
34
+ bottom = frame.y + frame.height
35
+ }
36
+ }
37
+ return rows.map((row) =>
38
+ row.sort((a, b) => a.x - b.x || a.name.localeCompare(b.name)),
39
+ )
40
+ }
41
+
42
+ /**
43
+ * Re-lays a page's frames: each row left to right on `FRAME_GAP` from the
44
+ * page's leftmost edge, rows stacked on their tallest box plus the gap from
45
+ * the page's top edge. Returns only the frames whose position changes.
46
+ */
47
+ export function planArrange(frames: readonly Frame[]): Placement[] {
48
+ if (frames.length === 0) return []
49
+ const left = Math.min(...frames.map((frame) => frame.x))
50
+ let top = Math.min(...frames.map((frame) => frame.y))
51
+
52
+ const moves: Placement[] = []
53
+ for (const row of rowsOf(frames)) {
54
+ let x = left
55
+ for (const frame of row) {
56
+ const from = boxOf(frame)
57
+ const to: Box = { ...from, x, y: top }
58
+ if (from.x !== to.x || from.y !== to.y) {
59
+ moves.push({ name: frame.name, from, to })
60
+ }
61
+ x += frame.width + FRAME_GAP
62
+ }
63
+ top += Math.max(...row.map((frame) => frame.height)) + FRAME_GAP
64
+ }
65
+ return moves
66
+ }
@@ -157,7 +157,7 @@ export async function captureCanvas(
157
157
  })
158
158
  }
159
159
 
160
- type ServeRefused = {
160
+ export type ServeRefused = {
161
161
  readonly ok: false
162
162
  readonly reason: 'no-server' | 'capture-failed'
163
163
  readonly detail: string
@@ -168,20 +168,34 @@ async function serveFor<T>(
168
168
  deps: CaptureDeps,
169
169
  run: (base: string, capture: CaptureEngine) => Promise<T>,
170
170
  ): Promise<T | ServeRefused> {
171
- const start = deps.start ?? startCanvas
171
+ return serveCanvas(root, deps.start, async (base) => {
172
+ const capture =
173
+ deps.capture ?? (await import('@/capture/render')).captureSources
174
+ return run(base, capture)
175
+ })
176
+ }
177
+
178
+ /**
179
+ * Runs a call against a canvas served on a free port for its length, so a verb
180
+ * that renders frames reads the same address the board shows.
181
+ */
182
+ export async function serveCanvas<T>(
183
+ root: string,
184
+ startOverride: CaptureDeps['start'],
185
+ run: (base: string) => Promise<T>,
186
+ ): Promise<T | ServeRefused> {
187
+ const start = startOverride ?? startCanvas
172
188
  const server = start(root, { port: 0, shell: new Response('') })
173
189
  if (!server.ok) {
174
190
  return {
175
191
  ok: false,
176
192
  reason: 'no-server',
177
- detail: `${server.detail}, so run canon canvas serve and capture again`,
193
+ detail: `${server.detail}, so run canon canvas serve and try again`,
178
194
  }
179
195
  }
180
196
 
181
197
  try {
182
- const capture =
183
- deps.capture ?? (await import('@/capture/render')).captureSources
184
- return await run(server.url, capture)
198
+ return await run(server.url)
185
199
  } catch (error) {
186
200
  return {
187
201
  ok: false,
@@ -578,6 +578,45 @@ export function restoreBox(
578
578
  })
579
579
  }
580
580
 
581
+ export type PruneOutcome =
582
+ | { readonly ok: true; readonly dropped: readonly string[] }
583
+ | ContentRefused
584
+
585
+ /**
586
+ * Lists the layout entries whose frame file is gone, and removes them under
587
+ * the layout lock unless `write` is false. `readPage` never reports them, so
588
+ * this is the one place an entry outliving its frame is seen.
589
+ */
590
+ export function pruneLayout(
591
+ root: string,
592
+ page: string,
593
+ write: boolean,
594
+ ): PruneOutcome {
595
+ if (!isValidName(page)) {
596
+ return refuse('invalid-name', `${page} is not a valid page name`)
597
+ }
598
+ const dir = pagePath(root, page)
599
+ if (!isDirectory(dir)) return refuse('no-page', `page ${page} does not exist`)
600
+
601
+ const prune = (): PruneOutcome => {
602
+ const { boxes, malformed } = readLayout(dir)
603
+ if (malformed) {
604
+ return refuse('malformed-layout', `${page}/${LAYOUT_FILE} does not parse`)
605
+ }
606
+ const present = new Set(frameNames(dir))
607
+ const dropped = [...boxes.keys()].filter((name) => !present.has(name))
608
+ if (write && dropped.length > 0) {
609
+ writeLayout(
610
+ dir,
611
+ new Map([...boxes].filter(([name]) => present.has(name))),
612
+ )
613
+ }
614
+ return { ok: true, dropped }
615
+ }
616
+
617
+ return write ? withFileLock(join(dir, LAYOUT_FILE), prune) : prune()
618
+ }
619
+
581
620
  export interface Selection {
582
621
  readonly page: string
583
622
  readonly frame: string
@@ -0,0 +1,238 @@
1
+ import {
2
+ type Box,
3
+ type ContentRefused,
4
+ readPage,
5
+ resizeFrame,
6
+ } from '@/canvas/content'
7
+ import {
8
+ type CaptureDeps,
9
+ resolveCaptureTargets,
10
+ type ServeRefused,
11
+ serveCanvas,
12
+ } from '@/canvas/capture'
13
+ import { resolveFrameTokens, servedFamilies } from '@/canvas/tokens'
14
+
15
+ /** What the capture browser read off one frame at its box width. */
16
+ export interface Measured {
17
+ readonly page: string
18
+ readonly frame: string
19
+ readonly box: Box
20
+ readonly contentHeight: number
21
+ readonly contentWidth: number
22
+ /**
23
+ * The lowest edge of any element in the body moved when the viewport
24
+ * height changed, so a viewport unit is sizing something and the content
25
+ * measure is not the height a viewer draws.
26
+ */
27
+ readonly viewportBound: boolean
28
+ /** The first family of the body text, unquoted. */
29
+ readonly family: string
30
+ }
31
+
32
+ export interface FitRow {
33
+ readonly page: string
34
+ readonly frame: string
35
+ /** The box height now. */
36
+ readonly height: number
37
+ /** The content's height, which is what the box should be. */
38
+ readonly fitted: number
39
+ /** The content is wider than the box, which fit reports and never widens. */
40
+ readonly wide: boolean
41
+ /** The first family is not one the canvas serves, so the height may not hold. */
42
+ readonly font: boolean
43
+ /**
44
+ * The document height depends on the viewport, as a height in `vh` does, so
45
+ * the content measure is not the height a viewer draws and nothing is written.
46
+ */
47
+ readonly viewport: boolean
48
+ readonly family: string
49
+ }
50
+
51
+ /**
52
+ * Turns measurements into rows. The box width is the frame's declared render
53
+ * width, so an overflow in it is a finding and never a resize. `served` is the
54
+ * set of families the served sheet carries a face for.
55
+ *
56
+ * A frame whose elements moved with the viewport height is sized by a unit
57
+ * such as `vh`, so the row keeps its height and reports `viewport`. Scroll
58
+ * height cannot show it alone, since a single `100vh` section reads as short
59
+ * content drawn in a taller viewport.
60
+ */
61
+ export function planFit(
62
+ measured: readonly Measured[],
63
+ served: ReadonlySet<string>,
64
+ ): FitRow[] {
65
+ return measured.map((item) => {
66
+ const content = Math.max(1, Math.ceil(item.contentHeight))
67
+ const viewport = item.viewportBound
68
+ return {
69
+ page: item.page,
70
+ frame: item.frame,
71
+ height: item.box.height,
72
+ fitted: viewport ? item.box.height : content,
73
+ wide: item.contentWidth > item.box.width,
74
+ font: !served.has(item.family),
75
+ viewport,
76
+ family: item.family,
77
+ }
78
+ })
79
+ }
80
+
81
+ /** Whether a row asks anything of the operator or the session. */
82
+ export function isOwed(row: FitRow): boolean {
83
+ return row.fitted !== row.height || row.wide || row.font || row.viewport
84
+ }
85
+
86
+ export type FitOutcome =
87
+ | {
88
+ readonly ok: true
89
+ readonly rows: readonly FitRow[]
90
+ /** Whether heights were written, which is false under a check. */
91
+ readonly written: boolean
92
+ }
93
+ | ContentRefused
94
+ | ServeRefused
95
+
96
+ export interface FitDeps extends Pick<CaptureDeps, 'start'> {
97
+ /** What a test stands in for, which the command never passes. */
98
+ readonly measure?: (targets: readonly MeasureTarget[]) => Promise<Measured[]>
99
+ }
100
+
101
+ export interface MeasureTarget {
102
+ readonly page: string
103
+ readonly frame: string
104
+ readonly url: string
105
+ readonly box: Box
106
+ }
107
+
108
+ /**
109
+ * Loads each frame at its box width through the served address, which is what
110
+ * injects the tokens and the faces, and reads the document once fonts and the
111
+ * load event have settled, the moment a capture waits for. The content is read
112
+ * at a one pixel tall viewport, since a document shorter than the viewport
113
+ * reports the viewport and a frame could then never shrink. The lowest element
114
+ * edge is then read at the box height and at twice it, and an edge that moved
115
+ * is a height set in viewport units.
116
+ */
117
+ async function measureInBrowser(
118
+ targets: readonly MeasureTarget[],
119
+ ): Promise<Measured[]> {
120
+ const { chromium } = await import('playwright-core')
121
+ const browser = await chromium.launch()
122
+ try {
123
+ const measured: Measured[] = []
124
+ for (const target of targets) {
125
+ const page = await browser.newPage({
126
+ viewport: { width: target.box.width, height: 1 },
127
+ })
128
+ try {
129
+ await page.goto(target.url, { waitUntil: 'load' })
130
+ const read = await page.evaluate(async () => {
131
+ await document.fonts.ready
132
+ const root = document.documentElement
133
+ const family = getComputedStyle(document.body).fontFamily
134
+ return {
135
+ contentHeight: root.scrollHeight,
136
+ contentWidth: root.scrollWidth,
137
+ family,
138
+ }
139
+ })
140
+ const lowestEdge = () =>
141
+ page.evaluate(() => {
142
+ let bottom = 0
143
+ for (const element of document.body.querySelectorAll('*')) {
144
+ bottom = Math.max(
145
+ bottom,
146
+ element.getBoundingClientRect().bottom + window.scrollY,
147
+ )
148
+ }
149
+ return Math.ceil(bottom)
150
+ })
151
+ const heights = [target.box.height, target.box.height * 2]
152
+ const edges: number[] = []
153
+ for (const height of heights) {
154
+ await page.setViewportSize({ width: target.box.width, height })
155
+ edges.push(await lowestEdge())
156
+ }
157
+ measured.push({
158
+ page: target.page,
159
+ frame: target.frame,
160
+ box: target.box,
161
+ contentHeight: read.contentHeight,
162
+ contentWidth: read.contentWidth,
163
+ viewportBound: edges[0] !== edges[1],
164
+ family: firstFamily(read.family),
165
+ })
166
+ } finally {
167
+ await page.close()
168
+ }
169
+ }
170
+ return measured
171
+ } finally {
172
+ await browser.close()
173
+ }
174
+ }
175
+
176
+ /** The first entry of a computed `font-family` list, without its quotes. */
177
+ export function firstFamily(list: string): string {
178
+ const first = list.split(',')[0] ?? ''
179
+ return first.trim().replace(/^["']|["']$/g, '')
180
+ }
181
+
182
+ /**
183
+ * Measures the frames `spec` names and, unless `write` is false, sets each
184
+ * box's height to its content through `resizeFrame`, the writer the operator's
185
+ * drags share. Serves the canvas for the call, so it needs no running board.
186
+ */
187
+ export async function fitCanvas(
188
+ root: string,
189
+ spec: string,
190
+ write: boolean,
191
+ deps: FitDeps = {},
192
+ ): Promise<FitOutcome> {
193
+ const early = resolveCaptureTargets(root, spec, '')
194
+ if (!early.ok) return early
195
+
196
+ const served = await serveCanvas(root, deps.start, async (base) => {
197
+ const resolved = resolveCaptureTargets(root, spec, base)
198
+ if (!resolved.ok) return resolved
199
+ const frames = readPage(root, resolved.targets[0]?.page ?? '')?.frames ?? []
200
+
201
+ const targets: MeasureTarget[] = []
202
+ for (const item of resolved.targets) {
203
+ const frame = frames.find((candidate) => candidate.name === item.frame)
204
+ if (!frame) continue
205
+ targets.push({
206
+ page: item.page,
207
+ frame: item.frame,
208
+ url: item.url,
209
+ box: {
210
+ x: frame.x,
211
+ y: frame.y,
212
+ width: frame.width,
213
+ height: frame.height,
214
+ },
215
+ })
216
+ }
217
+ const measure = deps.measure ?? measureInBrowser
218
+ return { ok: true as const, targets, measured: await measure(targets) }
219
+ })
220
+ if (!served.ok) return served
221
+
222
+ const rows = planFit(
223
+ served.measured,
224
+ servedFamilies(resolveFrameTokens(root).css),
225
+ )
226
+ if (!write) return { ok: true, rows, written: false }
227
+
228
+ for (const row of rows.filter((item) => item.fitted !== item.height)) {
229
+ const target = served.targets.find((item) => item.frame === row.frame)
230
+ if (!target) continue
231
+ const outcome = resizeFrame(root, row.page, row.frame, {
232
+ ...target.box,
233
+ height: row.fitted,
234
+ })
235
+ if (!outcome.ok) return outcome
236
+ }
237
+ return { ok: true, rows, written: true }
238
+ }
@@ -10,7 +10,6 @@ import {
10
10
  import {
11
11
  type DroppedFace,
12
12
  inlineProjectFaces,
13
- namedFamilies,
14
13
  projectSheets,
15
14
  } from '@/design/project-fonts'
16
15
  import { isOwnCheckout } from '@/roots/project'
@@ -31,7 +30,7 @@ export type FrameTokens =
31
30
  /** Project faces left out of the sheet, present only when one was. */
32
31
  readonly dropped?: readonly DroppedFace[]
33
32
  }
34
- | { readonly source: 'none'; readonly css: ''; readonly notice: string }
33
+ | { readonly source: 'none'; readonly css: string; readonly notice: string }
35
34
 
36
35
  export interface TokenOptions {
37
36
  /** Overrides the checkout test, which reads this package's own name. */
@@ -124,6 +123,21 @@ export function tokenGroups(css: string): TokenGroup[] {
124
123
  })
125
124
  }
126
125
 
126
+ /**
127
+ * The families a served sheet declares a face for, so a caller reads what the
128
+ * frames carry from the same sheet the server builds rather than from a list
129
+ * of its own.
130
+ */
131
+ export function servedFamilies(css: string): Set<string> {
132
+ const families = new Set<string>()
133
+ for (const [, family] of css.matchAll(
134
+ /@font-face\s*{[^}]*?font-family:\s*["']?([^"';}]+)/g,
135
+ )) {
136
+ if (family) families.add(family.trim())
137
+ }
138
+ return families
139
+ }
140
+
127
141
  /** The installed files in cascade order: the base first, then each override. */
128
142
  function installedFiles(root: string): string[] {
129
143
  const base = join(DESIGN_INSTALL_DIR, BASE_FILE)
@@ -134,14 +148,14 @@ function installedFiles(root: string): string[] {
134
148
  }
135
149
 
136
150
  /**
137
- * The vendored faces the installed sheets name and no project rule already
138
- * declares, so a target renders the base's families on a machine that never
139
- * installed them, and a project face of the same family wins outright.
151
+ * The vendored faces no project rule already declares, so a frame naming
152
+ * `Geist Variable` renders the same face in the capture and in any browser
153
+ * whether or not a sheet names it, and a project face of the same family wins
154
+ * outright.
140
155
  */
141
- function toolkitFaces(css: string, declared: ReadonlySet<string>): FontFace[] {
142
- const named = namedFamilies(css)
156
+ function toolkitFaces(declared: ReadonlySet<string>): FontFace[] {
143
157
  return [...FONT_FACES, ...HAND_DRAWN_FONT_FACES].filter(
144
- (face) => named.has(face.family) && !declared.has(face.family),
158
+ (face) => !declared.has(face.family),
145
159
  )
146
160
  }
147
161
 
@@ -190,7 +204,7 @@ export function resolveFrameTokens(
190
204
  const files = installedFiles(root)
191
205
  if (files.length > 0) {
192
206
  const { css, dropped, declared } = installedSheets(root, files)
193
- const faces = toolkitFaces(css, declared)
207
+ const faces = toolkitFaces(declared)
194
208
  return {
195
209
  source: 'installed',
196
210
  css: faces.length > 0 ? `${fontFaceBlock(faces)}\n${css}` : css,
@@ -199,5 +213,9 @@ export function resolveFrameTokens(
199
213
  }
200
214
  }
201
215
 
202
- return { source: 'none', css: '', notice: NONE_NOTICE }
216
+ return {
217
+ source: 'none',
218
+ css: fontFaceBlock(toolkitFaces(new Set())),
219
+ notice: NONE_NOTICE,
220
+ }
203
221
  }
@@ -31,15 +31,9 @@ const GLOB_WIDTH = 44
31
31
  /** A refusal the script prints as `regen-hero: <message>` and exits 1 on. */
32
32
  export class FrameError extends Error {}
33
33
 
34
- export interface StandardRow {
35
- readonly name: string
36
- readonly appliesTo?: readonly string[]
37
- }
38
-
39
34
  export interface FrameCatalogs {
40
35
  readonly skills: readonly string[]
41
36
  readonly gov: GovCatalog
42
- readonly standards: readonly StandardRow[]
43
37
  readonly toolingStacks: readonly StackSummary[]
44
38
  readonly commandCount: number
45
39
  readonly tokenCss: string
@@ -63,7 +57,7 @@ const escape = (value: string): string =>
63
57
  /**
64
58
  * Even spacing across the sorted catalog rather than its first N, so a column
65
59
  * whose names share a prefix does not read as a narrower catalog than the one
66
- * that ships. Rules and standards sample. Skills do not, because the even step
60
+ * that ships. Rules sample. Skills do not, because the even step
67
61
  * lands on the entries closest to commodity and on none of the workflow the
68
62
  * toolkit exists to carry.
69
63
  */
@@ -186,7 +180,7 @@ function faviconUri(markSvg: string, tokenCss: string): string {
186
180
  * rather than shipping a frame claiming the domain has nothing in it.
187
181
  */
188
182
  export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
189
- const { gov, toolingStacks, standards, skills } = catalogs
183
+ const { gov, toolingStacks, skills } = catalogs
190
184
  const markSvg = catalogs.markSvg.trim()
191
185
  if (!markSvg) {
192
186
  throw new FrameError(
@@ -207,12 +201,10 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
207
201
  const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
208
202
  const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
209
203
  const deliveredRules = deliveredEntries.map((entry) => slug(entry.name))
210
- const standardNames = standards.map((entry) => entry.name)
211
204
 
212
205
  for (const [label, list] of [
213
206
  ['skills', skills],
214
207
  ['rules', rules],
215
- ['standards', standardNames],
216
208
  ['tooling stacks', toolingStacks],
217
209
  ['gov stacks', gov.stacks],
218
210
  ['stack-delivered rules', deliveredRules],
@@ -251,23 +243,6 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
251
243
  })
252
244
  .join('\n')
253
245
 
254
- // A standard either governs an artifact by path or is opened by name when a
255
- // session decides it needs it. Naming the second is the contrast, since
256
- // nothing else tells a reader the corpus installs into no project.
257
- const standardRows = sample(standards)
258
- .map((entry) => {
259
- const applies = entry.appliesTo ?? []
260
- const governs =
261
- applies.length > 0
262
- ? clip(applies.join(' '), GLOB_WIDTH)
263
- : 'read by name'
264
- return frameRow(
265
- `<span class="name">${escape(pad(entry.name, 18))}</span>` +
266
- `<span class="muted">${escape(governs)}</span>`,
267
- )
268
- })
269
- .join('\n')
270
-
271
246
  // A tooling stack is counted rather than listed, because what it lays down is
272
247
  // dev dependencies, run scripts, and ignore groups rather than named entries.
273
248
  // The inheritance is the part worth showing, since it is why a stack carrying
@@ -286,7 +261,6 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
286
261
  for (const [label, rows] of [
287
262
  ['governance stack rows', govStackRows],
288
263
  ['governance rule rows', govRuleRows],
289
- ['standard rows', standardRows],
290
264
  ['tooling stack rows', toolingStackRows],
291
265
  ] as const) {
292
266
  if (rows === '') {
@@ -300,11 +274,9 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
300
274
  TOKENS: tokenCss,
301
275
  GOV_STACK_ROWS: govStackRows,
302
276
  GOV_RULE_ROWS: govRuleRows,
303
- STANDARD_ROWS: standardRows,
304
277
  TOOLING_STACK_ROWS: toolingStackRows,
305
278
  SKILL_COUNT: String(skills.length),
306
279
  RULE_COUNT: String(rules.length),
307
- STANDARD_COUNT: String(standardNames.length),
308
280
  GOV_STACK_COUNT: String(gov.stacks.length),
309
281
  TOOLING_STACK_COUNT: String(toolingStacks.length),
310
282
  COMMAND_COUNT: String(catalogs.commandCount),
@@ -312,10 +284,8 @@ export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
312
284
  FAVICON: faviconUri(markSvg, tokenCss),
313
285
  SKILL_ENTRIES: markup(featured(skills, FEATURED_SKILLS)),
314
286
  RULE_ENTRIES: markup(sample(deliveredRules)),
315
- STANDARD_ENTRIES: markup(sample(standardNames)),
316
287
  SKILL_MORE: remaining(skills),
317
288
  RULE_MORE: remaining(rules),
318
- STANDARD_MORE: remaining(standardNames),
319
289
  }
320
290
 
321
291
  const light = lightTokens(tokenCss)
@@ -25,7 +25,7 @@ export const MISC_CASES: readonly SkillCase[] = [
25
25
  {
26
26
  prompt:
27
27
  'Draw the signup screen as a frame on the canvas, then restyle the element I just selected there.',
28
- expect: 'canvas',
28
+ expect: 'canvas-board',
29
29
  },
30
30
  {
31
31
  prompt:
@@ -40,6 +40,10 @@ export const SETUP_CASES: readonly SkillCase[] = [
40
40
  "Before I run this sync, tell me exactly what it's going to overwrite.",
41
41
  expect: 'canon-cli',
42
42
  },
43
+ {
44
+ prompt: 'Which canon command tells me if my markdown breaks the rules?',
45
+ expect: 'canon-cli',
46
+ },
43
47
  {
44
48
  prompt:
45
49
  'Something about the toolkit itself is broken, write it up and send it back to the maintainers.',
@@ -31,7 +31,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
31
31
  },
32
32
  {
33
33
  prompt:
34
- 'Fold what we decided this session into the context entries and mark off what shipped on the task board.',
34
+ 'Fold what we decided this session into the context entries before this branch goes out.',
35
35
  expect: 'context-fold',
36
36
  },
37
37
  {
@@ -127,6 +127,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
127
127
  prompt: 'Open a new entry on the task board for this piece of work.',
128
128
  expect: 'task-board',
129
129
  },
130
+ {
131
+ prompt:
132
+ 'This branch finished the pagination work, so tick off whichever board outcomes it actually delivered.',
133
+ expect: 'task-complete',
134
+ },
130
135
  {
131
136
  prompt: 'Open a learning workspace so I can study this topic properly.',
132
137
  expect: 'teach-workspace',
@@ -17,7 +17,7 @@ const HOOKS = 'hooks'
17
17
  * stem in an older layout, which is what `@/sync/layout` pairs a target against
18
18
  * to find a superseded artifact.
19
19
  */
20
- export const SUBDIRS: readonly string[] = [HOOKS, 'context', 'memory', 'tasks']
20
+ export const SUBDIRS: readonly string[] = [HOOKS, 'context', 'tasks']
21
21
 
22
22
  export type SeedScope = 'claude' | 'root'
23
23
 
@@ -7,7 +7,7 @@ import { bodyLines, maskDisplayed } from '@/markdown/scan'
7
7
  /** Returned when a skill folder carries no `REQUIREMENT.md`, the gating check. */
8
8
  export const EXIT_MISSING_REQUIREMENT = 2
9
9
 
10
- /** The description ceiling stated in the frontmatter rules of `standards/skill.md`. */
10
+ /** The description ceiling stated in the frontmatter rules of `claude/skills/create-skill/references/skill.md`. */
11
11
  export const DESCRIPTION_LIMIT = 1024
12
12
 
13
13
  /** The two headings the requirement template declares, matched at any level. */
@@ -160,7 +160,7 @@ interface SkillText {
160
160
  }
161
161
 
162
162
  /**
163
- * Measures both skill corpora against the rules `standards/skill.md` states
163
+ * Measures both skill corpora against the rules `claude/skills/create-skill/references/skill.md` states
164
164
  * mechanically. A corpus the project does not carry is skipped rather than
165
165
  * reported, so a target holding only `.claude/skills/` reads as in scope.
166
166
  *
package/src/cli/help.ts CHANGED
@@ -10,6 +10,8 @@ type HelpStream = { isTTY?: boolean }
10
10
 
11
11
  // The grouping mirrors the README's domain table and has no other source, so a
12
12
  // command added to the program needs a row here or help-option.test.ts fails.
13
+ // A row here also needs a `canon <verb>` line in claude/skills/canon-cli, or a
14
+ // name in TOOLKIT_ONLY in canon-cli-skill.test.ts, which reads this list.
13
15
  export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
14
16
  {
15
17
  title: 'Project',
@@ -25,7 +27,7 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
25
27
  title: 'Domains',
26
28
  rows: [
27
29
  ['gov [cmd]', 'Governance rules (install, sync, list)'],
28
- ['standards [cmd]', 'Standards (install, sync, list, <name>)'],
30
+ ['standards [cmd]', 'Standards (list, <name>)'],
29
31
  ['tooling [cmd]', 'Tooling stacks (sync, ref, create)'],
30
32
  ['claude [cmd]', 'Claude workflow (init, sync, setup)'],
31
33
  ['wiki [cmd]', 'Wiki pages (init)'],
package/src/cli/ui.ts CHANGED
@@ -154,9 +154,8 @@ export function isNonInteractive(): boolean {
154
154
  }
155
155
 
156
156
  /**
157
- * `nonInteractiveDefault` opts a prompt into the `select_option` behavior from
158
- * `scripts/lib/ui.sh`, where `CANON_NON_INTERACTIVE=1` resolves to the first
159
- * option instead of failing. Callers that would rather fail loudly than pick
157
+ * `nonInteractiveDefault` opts a prompt into resolving to its first option
158
+ * under `CANON_NON_INTERACTIVE=1` instead of failing. Callers that would rather fail loudly than pick
160
159
  * for the user leave it off.
161
160
  */
162
161
  export async function select<Value>(opts: {