@erclx/canon 5.33.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 (64) 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/canon-feedback/REQUIREMENT.md +1 -0
  6. package/claude/skills/canon-feedback/SKILL.md +10 -28
  7. package/claude/skills/canon-feedback/references/template.md +53 -0
  8. package/claude/skills/canvas-board/SKILL.md +4 -1
  9. package/claude/skills/canvas-board/references/canvas.md +26 -3
  10. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  11. package/claude/skills/code-craft/SKILL.md +0 -1
  12. package/claude/skills/create-rule/SKILL.md +1 -1
  13. package/claude/skills/create-rule/references/rule.md +3 -1
  14. package/claude/skills/create-skill/references/skill.md +1 -1
  15. package/claude/skills/git-issue/references/issue.md +4 -1
  16. package/claude/skills/review-craft/references/ui.md +1 -1
  17. package/claude/skills/review-ui/SKILL.md +7 -5
  18. package/claude/skills/review-ui/references/driving.md +8 -8
  19. package/claude/skills/review-ui/references/post.md +52 -16
  20. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +9 -0
  21. package/claude/skills/target-setup/SKILL.md +10 -4
  22. package/claude/skills/target-setup/references/detect.md +19 -2
  23. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  24. package/claude/skills/test-craft/SKILL.md +15 -3
  25. package/docs/agents/canvas.md +5 -1
  26. package/docs/agents/commands.md +1 -1
  27. package/docs/agents/gov-detect.md +47 -0
  28. package/docs/agents/index.md +4 -1
  29. package/docs/agents/install-and-sync.md +4 -0
  30. package/docs/agents/pr-evidence-layout.md +70 -0
  31. package/docs/agents/pr-evidence.md +8 -9
  32. package/docs/agents/pr-frames.md +7 -2
  33. package/docs/agents/pr-lift.md +132 -0
  34. package/docs/agents/scripting.md +2 -1
  35. package/docs/target/projects.md +3 -3
  36. package/governance/detect.toml +201 -0
  37. package/governance/rules/code/000-code.md +1 -13
  38. package/governance/rules/code/010-testing.md +2 -30
  39. package/governance/rules/ui/400-ui.md +1 -1
  40. package/governance/rules/ui/480-canvas.md +13 -0
  41. package/package.json +1 -1
  42. package/scripts/core/regen-hero.ts +0 -6
  43. package/scripts/lib/ui.sh +0 -9
  44. package/src/canvas/arrange.ts +66 -0
  45. package/src/canvas/capture.ts +20 -6
  46. package/src/canvas/content.ts +39 -0
  47. package/src/canvas/fit.ts +238 -0
  48. package/src/canvas/tokens.ts +28 -10
  49. package/src/capture/frame-values.ts +2 -32
  50. package/src/commands/canvas.ts +179 -1
  51. package/src/commands/gov.ts +144 -1
  52. package/src/commands/pr.ts +376 -1
  53. package/src/feedback/body.ts +26 -5
  54. package/src/gate/measures.ts +58 -44
  55. package/src/gate/stages.ts +3 -4
  56. package/src/gov/detect.ts +301 -0
  57. package/src/gov/list.ts +23 -2
  58. package/src/gov/stacks.ts +9 -0
  59. package/src/pr/evidence.ts +218 -45
  60. package/src/pr/frames.ts +23 -3
  61. package/src/pr/lift.ts +418 -0
  62. package/src/pr/rest.ts +3 -0
  63. package/src/pr/review-scope.ts +76 -0
  64. package/src/sandbox/equivalence.ts +14 -7
@@ -0,0 +1,201 @@
1
+ # Evidence that marks a project as wanting an opt-in rule. Each table is keyed by
2
+ # a rule basename, the same key `renames.toml` and the stacks use, so a rename
3
+ # sweep reaches this file too. `canon gov detect` reads it and proposes every
4
+ # rule whose evidence a target holds, and `target-setup` puts those proposals to
5
+ # the operator before anything installs.
6
+ #
7
+ # `deps` are package names read from `package.json` (`dependencies` and
8
+ # `devDependencies`), `pyproject.toml`, `go.mod`, and `composer.json`, at the
9
+ # root and in every folder to depth two. `files` are globs matched against the
10
+ # target's git-listed paths to depth two, and against each folder on those
11
+ # paths, so `.github/workflows` matches a folder holding a workflow. A glob with
12
+ # no wildcard is also tested on disk, which reaches a gitignored path such as
13
+ # `.canon/canvas`.
14
+ #
15
+ # Every rule no stack reaches needs a table here, which the `unreferenced-rules`
16
+ # gate stage checks, along with every key naming a live rule. A table for a rule
17
+ # a stack already reaches is harmless, since the verb leaves out every rule the
18
+ # caller's stack resolves.
19
+
20
+ ["skill"]
21
+ deps = []
22
+ files = [".claude/skills", "claude/skills"]
23
+
24
+ ["700-ci-workflow"]
25
+ deps = []
26
+ files = [".github/workflows"]
27
+
28
+ ["765-spelling"]
29
+ deps = ["cspell"]
30
+ files = [".cspell", "cspell.json", ".cspell.json", "cspell.config.*"]
31
+
32
+ ["775-dependencies"]
33
+ deps = []
34
+ files = ["**/package.json", "**/pyproject.toml", "**/go.mod", "**/composer.json"]
35
+
36
+ ["780-config-comments"]
37
+ deps = []
38
+ files = [".editorconfig", "**/Dockerfile*", "**/*.config.*", "**/.env.example"]
39
+
40
+ ["790-code-comments"]
41
+ deps = []
42
+ files = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.sh", "**/*.py"]
43
+
44
+ ["130-go"]
45
+ deps = []
46
+ files = ["**/go.mod"]
47
+
48
+ ["140-php"]
49
+ deps = []
50
+ files = ["**/composer.json"]
51
+
52
+ ["200-react"]
53
+ deps = ["react"]
54
+ files = []
55
+
56
+ ["210-astro"]
57
+ deps = ["astro"]
58
+ files = ["**/astro.config.*"]
59
+
60
+ ["220-fastapi"]
61
+ deps = ["fastapi"]
62
+ files = []
63
+
64
+ ["230-nextjs"]
65
+ deps = ["next"]
66
+ files = ["**/next.config.*"]
67
+
68
+ ["250-tailwind"]
69
+ deps = ["tailwindcss", "@tailwindcss/vite", "@tailwindcss/postcss"]
70
+ files = ["**/tailwind.config.*"]
71
+
72
+ ["260-shadcn"]
73
+ deps = []
74
+ files = ["**/components.json"]
75
+
76
+ ["305-e2e-reliability"]
77
+ deps = ["@playwright/test", "playwright", "cypress"]
78
+ files = ["**/e2e"]
79
+
80
+ ["306-test-scope"]
81
+ deps = ["@playwright/test", "playwright", "cypress"]
82
+ files = ["**/e2e"]
83
+
84
+ ["310-zod"]
85
+ deps = ["zod"]
86
+ files = []
87
+
88
+ ["320-tanstack-query"]
89
+ deps = [
90
+ "@tanstack/react-query",
91
+ "@tanstack/vue-query",
92
+ "@tanstack/solid-query",
93
+ "@tanstack/svelte-query",
94
+ "@tanstack/query-core",
95
+ ]
96
+ files = []
97
+
98
+ ["335-testing-go"]
99
+ deps = []
100
+ files = ["**/go.mod"]
101
+
102
+ ["336-testing-php"]
103
+ deps = ["phpunit/phpunit", "pestphp/pest"]
104
+ files = ["**/phpunit.xml", "**/phpunit.xml.dist"]
105
+
106
+ ["340-pydantic"]
107
+ deps = ["pydantic"]
108
+ files = []
109
+
110
+ ["350-security-web"]
111
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
112
+ files = ["index.html", "*/index.html"]
113
+
114
+ ["360-security-server"]
115
+ deps = [
116
+ "express",
117
+ "fastify",
118
+ "hono",
119
+ "koa",
120
+ "elysia",
121
+ "@nestjs/core",
122
+ "fastapi",
123
+ "django",
124
+ "flask",
125
+ "starlette",
126
+ "laravel/framework",
127
+ "symfony/http-kernel",
128
+ "slim/slim",
129
+ "github.com/gin-gonic/gin",
130
+ "github.com/labstack/echo/v4",
131
+ "github.com/go-chi/chi/v5",
132
+ "github.com/gofiber/fiber/v2",
133
+ ]
134
+ files = []
135
+
136
+ ["370-database"]
137
+ deps = [
138
+ "prisma",
139
+ "@prisma/client",
140
+ "drizzle-orm",
141
+ "typeorm",
142
+ "sequelize",
143
+ "knex",
144
+ "kysely",
145
+ "mongoose",
146
+ "pg",
147
+ "mysql2",
148
+ "better-sqlite3",
149
+ "sqlalchemy",
150
+ "alembic",
151
+ "psycopg",
152
+ "psycopg2",
153
+ "psycopg2-binary",
154
+ "asyncpg",
155
+ "django",
156
+ "doctrine/orm",
157
+ "illuminate/database",
158
+ "gorm.io/gorm",
159
+ "github.com/jackc/pgx/v5",
160
+ ]
161
+ files = ["**/migrations", "**/schema.prisma", "**/*.sql"]
162
+
163
+ ["400-ui"]
164
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
165
+ files = ["index.html", "*/index.html"]
166
+
167
+ ["410-a11y"]
168
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
169
+ files = ["index.html", "*/index.html"]
170
+
171
+ ["420-forms"]
172
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
173
+ files = ["index.html", "*/index.html"]
174
+
175
+ ["430-ux-completeness"]
176
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
177
+ files = ["index.html", "*/index.html"]
178
+
179
+ ["440-surface-capture"]
180
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
181
+ files = ["index.html", "*/index.html"]
182
+
183
+ ["450-link-behavior"]
184
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
185
+ files = ["index.html", "*/index.html"]
186
+
187
+ ["460-design-taste"]
188
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
189
+ files = ["canon/DESIGN.md", "index.html", "*/index.html"]
190
+
191
+ ["465-interface-casing"]
192
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
193
+ files = ["index.html", "*/index.html"]
194
+
195
+ ["470-motion"]
196
+ deps = ["react", "vue", "svelte", "astro", "next", "solid-js", "preact"]
197
+ files = ["index.html", "*/index.html"]
198
+
199
+ ["480-canvas"]
200
+ deps = []
201
+ files = [".canon/canvas"]
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Code placement, shape, dependency, error, and naming choices a model does not make by default
2
+ description: Routes code placement and shape decisions to the skills that carry them
3
3
  ---
4
4
 
5
5
  # Code standards
@@ -7,19 +7,7 @@ description: Code placement, shape, dependency, error, and naming choices a mode
7
7
  ## Placement
8
8
 
9
9
  - Load the `canon:codebase-layout` skill before placing a new file, and report it rather than proceeding silently when it does not resolve.
10
- - Prioritize native platform capabilities over third-party libraries.
11
- - Ensure data and configuration reside in designated Single Source of Truth locations.
12
10
 
13
11
  ## Shape
14
12
 
15
13
  - Load the `canon:code-craft` skill before deciding the shape of a new function, class, or module, and report it rather than proceeding silently when it does not resolve.
16
-
17
- ## Errors
18
-
19
- - Return structured error types for recoverable failures.
20
- - Never expose internal implementation details in error messages.
21
-
22
- ## Naming
23
-
24
- - Prefix booleans with `is`, `has`, `should`, or `can`: `isLoading`, `hasAccess`.
25
- - Prefix event handlers with `handle`: `handleClick`, `handleSubmit`.
@@ -1,37 +1,9 @@
1
1
  ---
2
- description: Enforce universal testing standards and best practices
2
+ description: Routes a test edit to the skill that carries the testing floors
3
3
  ---
4
4
 
5
5
  # Testing standards
6
6
 
7
7
  ## Before writing a test
8
8
 
9
- - Load the `canon:test-craft` skill before choosing the layer for a new test, and report it rather than proceeding silently when the skill does not resolve.
10
-
11
- ## Test structure
12
-
13
- - Structure tests using the Arrange, Act, Assert (AAA) pattern.
14
- - Each test should verify a single behavior.
15
- - Do not use conditional logic or loops within test bodies.
16
-
17
- ## Test focus
18
-
19
- - Do not test private functions or internal state directly.
20
- - Do not target arbitrary coverage percentages.
21
-
22
- ## Organization and isolation
23
-
24
- - Group related tests using the framework's nesting mechanism.
25
- - Keep nesting shallow (max 2 levels).
26
- - Ensure tests are independent with no shared side effects.
27
- - Clean up side effects and restore state after each test.
28
-
29
- ## Test data and async
30
-
31
- - Use factory functions or builders for test data over inline object literals.
32
- - Always await async operations.
33
- - Do not fire-and-forget promises in tests.
34
-
35
- ## Verification
36
-
37
- - Do not use snapshot testing for verification.
9
+ - Load the `canon:test-craft` skill before writing or changing a test, and report it rather than proceeding silently when the skill does not resolve.
@@ -11,7 +11,7 @@ paths:
11
11
 
12
12
  ## Punctuation
13
13
 
14
- - Do not use em dashes (`—`). Use a comma or period, or restructure the sentence.
14
+ - Do not use em dashes (`—`) or semicolons (`;`). Use a comma or period, or restructure the sentence.
15
15
  - Do not add punctuation to button labels or short action strings.
16
16
 
17
17
  ## Button copy
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Route edits to canvas frames under .canon/canvas/ to the canvas-board skill for the house style, and keep frame text and box height readable
3
+ paths:
4
+ - '.canon/canvas/**'
5
+ ---
6
+
7
+ # Canvas standards
8
+
9
+ ## Frames
10
+
11
+ - Load the `canon:canvas-board` skill before drawing or editing a frame. It carries the house style for an explanation frame and the content format. Report it rather than proceeding silently when the skill does not resolve.
12
+ - Write explanation frame text in sentence case. A mockup of the project's interface follows its design document instead.
13
+ - Size each explanation frame's box to show all its content with no scrollbar.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.33.0",
4
+ "version": "5.34.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -50,7 +50,6 @@ import { listSkills } from '../../src/claude/skills-list'
50
50
  import { countCommands } from '../../src/counts/catalogs'
51
51
  import { buildDesignCss } from '../../src/design/css'
52
52
  import { buildGovCatalog } from '../../src/gov/list'
53
- import { listStandardEntries } from '../../src/standards/list'
54
53
  import { buildStackSummaries } from '../../src/tooling/list'
55
54
 
56
55
  const root = new URL('../..', import.meta.url).pathname
@@ -87,11 +86,6 @@ function run(): void {
87
86
  const { values, frames } = buildFrameValues({
88
87
  skills: listSkills(root).map((entry) => entry.name),
89
88
  gov: buildGovCatalog(root),
90
- // The toolkit root only: a project-local standard would put a row in the
91
- // hero the toolkit does not ship.
92
- standards: listStandardEntries(root).filter((entry) =>
93
- entry.source.startsWith('standards/'),
94
- ),
95
89
  toolingStacks: buildStackSummaries(root),
96
90
  commandCount: countCommands(root) ?? 0,
97
91
  tokenCss,
package/scripts/lib/ui.sh CHANGED
@@ -96,12 +96,3 @@ close_timeline() {
96
96
  set_palette 2
97
97
  echo -e "${GREY}└${NC}" >&2
98
98
  }
99
-
100
- guard_root() {
101
- local target="$1"
102
- local target_abs
103
- target_abs=$(cd "$target" && pwd)
104
- if [ "$target_abs" = "$PROJECT_ROOT" ]; then
105
- log_error "Cannot run against toolkit root. Files here are the source of truth."
106
- fi
107
- }
@@ -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