@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/REQUIREMENT.md +1 -2
- package/claude/skills/api-design/SKILL.md +2 -3
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
- package/claude/skills/canon-feedback/SKILL.md +10 -28
- package/claude/skills/canon-feedback/references/template.md +53 -0
- package/claude/skills/canvas-board/SKILL.md +4 -1
- package/claude/skills/canvas-board/references/canvas.md +26 -3
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +0 -1
- package/claude/skills/create-rule/SKILL.md +1 -1
- package/claude/skills/create-rule/references/rule.md +3 -1
- package/claude/skills/create-skill/references/skill.md +1 -1
- package/claude/skills/git-issue/references/issue.md +4 -1
- package/claude/skills/review-craft/references/ui.md +1 -1
- package/claude/skills/review-ui/SKILL.md +7 -5
- package/claude/skills/review-ui/references/driving.md +8 -8
- package/claude/skills/review-ui/references/post.md +52 -16
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +9 -0
- package/claude/skills/target-setup/SKILL.md +10 -4
- package/claude/skills/target-setup/references/detect.md +19 -2
- package/claude/skills/test-craft/REQUIREMENT.md +2 -3
- package/claude/skills/test-craft/SKILL.md +15 -3
- package/docs/agents/canvas.md +5 -1
- package/docs/agents/commands.md +1 -1
- package/docs/agents/gov-detect.md +47 -0
- package/docs/agents/index.md +4 -1
- package/docs/agents/install-and-sync.md +4 -0
- package/docs/agents/pr-evidence-layout.md +70 -0
- package/docs/agents/pr-evidence.md +8 -9
- package/docs/agents/pr-frames.md +7 -2
- package/docs/agents/pr-lift.md +132 -0
- package/docs/agents/scripting.md +2 -1
- package/docs/target/projects.md +3 -3
- package/governance/detect.toml +201 -0
- package/governance/rules/code/000-code.md +1 -13
- package/governance/rules/code/010-testing.md +2 -30
- package/governance/rules/ui/400-ui.md +1 -1
- package/governance/rules/ui/480-canvas.md +13 -0
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +0 -6
- package/scripts/lib/ui.sh +0 -9
- package/src/canvas/arrange.ts +66 -0
- package/src/canvas/capture.ts +20 -6
- package/src/canvas/content.ts +39 -0
- package/src/canvas/fit.ts +238 -0
- package/src/canvas/tokens.ts +28 -10
- package/src/capture/frame-values.ts +2 -32
- package/src/commands/canvas.ts +179 -1
- package/src/commands/gov.ts +144 -1
- package/src/commands/pr.ts +376 -1
- package/src/feedback/body.ts +26 -5
- package/src/gate/measures.ts +58 -44
- package/src/gate/stages.ts +3 -4
- package/src/gov/detect.ts +301 -0
- package/src/gov/list.ts +23 -2
- package/src/gov/stacks.ts +9 -0
- package/src/pr/evidence.ts +218 -45
- package/src/pr/frames.ts +23 -3
- package/src/pr/lift.ts +418 -0
- package/src/pr/rest.ts +3 -0
- package/src/pr/review-scope.ts +76 -0
- 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:
|
|
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:
|
|
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
|
|
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
|
@@ -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
|
+
}
|
package/src/canvas/capture.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
193
|
+
detail: `${server.detail}, so run canon canvas serve and try again`,
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
try {
|
|
182
|
-
|
|
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,
|
package/src/canvas/content.ts
CHANGED
|
@@ -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
|