@erclx/canon 5.19.0 → 5.21.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/auto-ship/references/verb-absent.md +1 -1
- package/claude/skills/create-skill/SKILL.md +1 -0
- package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/REQUIREMENT.md +1 -1
- package/claude/skills/draft-ready/REQUIREMENT.md +2 -1
- package/claude/skills/draft-ready/SKILL.md +6 -4
- package/claude/skills/markdown-propose/REQUIREMENT.md +3 -3
- package/claude/skills/markdown-propose/SKILL.md +1 -1
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/record-screencast/REQUIREMENT.md +2 -1
- package/claude/skills/record-screencast/SKILL.md +2 -2
- package/claude/skills/role-worker/REQUIREMENT.md +3 -1
- package/claude/skills/role-worker/SKILL.md +5 -3
- package/claude/skills/session-resume/SKILL.md +1 -1
- package/claude/skills/task-board/REQUIREMENT.md +1 -0
- package/claude/skills/task-board/SKILL.md +6 -0
- package/claude/skills/task-board/references/decline.md +2 -0
- package/claude/skills/video-craft/REQUIREMENT.md +48 -0
- package/claude/skills/video-craft/SKILL.md +80 -0
- package/claude/skills/video-craft/references/adopted.md +66 -0
- package/docs/agents/canvas.md +4 -0
- package/docs/agents/commands.md +1 -0
- package/docs/agents/demo.md +1 -1
- package/docs/agents/index.md +1 -0
- package/docs/agents/install-and-sync.md +17 -3
- package/docs/agents/markdown-audit.md +1 -3
- package/docs/agents/rule-citations.md +4 -4
- package/docs/agents/sandbox.md +8 -7
- package/docs/agents/slides.md +120 -0
- package/docs/agents/tasks-validate.md +1 -1
- package/docs/agents/tasks.md +1 -1
- package/docs/target/migrations.md +1 -1
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/operating-model.md +9 -9
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +12 -13
- package/governance/renames.toml +24 -0
- package/governance/rules/lib/330-testing-py.md +1 -1
- package/governance/rules/lib/336-testing-php.md +1 -1
- package/governance/rules/{canon/640-architecture.md → standards/architecture.md} +3 -2
- package/governance/rules/standards/context.md +15 -0
- package/governance/rules/standards/design.md +13 -0
- package/governance/rules/standards/diagrams.md +17 -0
- package/governance/rules/standards/groundwork.md +13 -0
- package/governance/rules/standards/intake.md +12 -0
- package/governance/rules/standards/markdown.md +15 -0
- package/governance/rules/standards/memory.md +12 -0
- package/governance/rules/standards/mermaid.md +15 -0
- package/governance/rules/{canon/658-plan.md → standards/plan.md} +5 -9
- package/governance/rules/standards/readme.md +12 -0
- package/governance/rules/standards/ready.md +13 -0
- package/governance/rules/standards/requirements.md +13 -0
- package/governance/rules/standards/rule.md +13 -0
- package/governance/rules/standards/session.md +13 -0
- package/governance/rules/{claude/570-skill.md → standards/skill.md} +6 -10
- package/governance/rules/standards/standard.md +13 -0
- package/governance/rules/standards/tasks.md +15 -0
- package/governance/rules/{canon/661-teach.md → standards/teach.md} +3 -2
- package/governance/rules/standards/wiki.md +13 -0
- package/governance/rules/standards/wireframes.md +14 -0
- package/governance/rules/writing/800-prose.md +1 -1
- package/governance/stacks/base.toml +4 -2
- package/package.json +1 -1
- package/src/canvas/capture.ts +228 -15
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/cases/workflow.ts +4 -20
- package/src/claude/routing.ts +1 -1
- package/src/commands/canvas.ts +76 -2
- package/src/commands/gov.ts +19 -4
- package/src/commands/sandbox.ts +7 -0
- package/src/commands/slides.ts +13 -3
- package/src/commands/tasks.ts +46 -2
- package/src/gate/stages.ts +7 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/standard-rules.ts +179 -0
- package/src/migrate/skill-names.ts +0 -7
- package/src/sandbox/equivalence.ts +43 -14
- package/src/sandbox/stub-remote.ts +140 -0
- package/src/slides/convert/chart.ts +165 -0
- package/src/slides/convert/deck.ts +164 -0
- package/src/slides/convert/export.ts +217 -7
- package/src/slides/convert/master.ts +195 -0
- package/src/slides/convert/shapes.ts +1 -1
- package/src/slides/convert/walk.ts +187 -2
- package/src/tasks/record.ts +11 -1
- package/src/tasks/validate.ts +14 -0
- package/standards/architecture.md +5 -0
- package/standards/context.md +6 -0
- package/standards/design.md +3 -0
- package/standards/diagrams.md +8 -0
- package/standards/groundwork-spikes.md +66 -0
- package/standards/groundwork.md +5 -25
- package/standards/index.md +6 -2
- package/standards/intake.md +2 -0
- package/standards/markdown.md +6 -0
- package/standards/memory.md +2 -0
- package/standards/mermaid.md +6 -0
- package/standards/plan-lifecycle.md +36 -0
- package/standards/plan.md +13 -11
- package/standards/readme.md +2 -0
- package/standards/ready.md +4 -0
- package/standards/requirements.md +3 -0
- package/standards/rule.md +6 -1
- package/standards/session.md +4 -0
- package/standards/skill-paths.md +50 -0
- package/standards/skill.md +15 -23
- package/standards/standard.md +7 -1
- package/standards/tasks-decline.md +38 -0
- package/standards/tasks.md +9 -14
- package/standards/teach.md +5 -0
- package/standards/wiki.md +4 -0
- package/standards/wireframes.md +5 -0
- package/tooling/astro/configs/card-src/pages/og-card.astro +2 -2
- package/claude/skills/docs-fold/REQUIREMENT.md +0 -25
- package/claude/skills/docs-fold/SKILL.md +0 -9
- package/claude/skills/standards-audit/REQUIREMENT.md +0 -33
- package/claude/skills/standards-audit/SKILL.md +0 -99
- package/governance/rules/canon/610-context.md +0 -17
- package/governance/rules/canon/620-wireframes.md +0 -13
- package/governance/rules/canon/630-requirements.md +0 -12
- package/governance/rules/canon/650-design.md +0 -12
- package/governance/rules/canon/655-tasks.md +0 -13
- package/governance/rules/canon/656-groundwork.md +0 -11
- package/governance/rules/canon/657-intake.md +0 -11
- package/governance/rules/canon/659-memory.md +0 -11
- package/governance/rules/canon/660-diagrams.md +0 -19
- package/governance/rules/canon/662-session.md +0 -15
- package/governance/rules/canon/663-ready.md +0 -15
- package/governance/rules/claude/590-rule-authoring.md +0 -12
- package/governance/rules/writing/801-markdown.md +0 -14
- package/governance/rules/writing/802-mermaid.md +0 -14
- package/governance/rules/writing/880-readme.md +0 -11
- package/governance/rules/writing/891-standard-authoring.md +0 -12
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/requirements.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Shape and content rules for canon/REQUIREMENTS.md
|
|
4
|
+
paths:
|
|
5
|
+
- 'canon/REQUIREMENTS.md'
|
|
6
|
+
- '.claude/REQUIREMENTS.md'
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Requirements standards
|
|
10
|
+
|
|
11
|
+
## Authority
|
|
12
|
+
|
|
13
|
+
- Follow the requirements standard for shape and content rules for canon/REQUIREMENTS.md. It is the single source. Read it with `canon standards requirements`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/rule.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Rule frontmatter, body shape, and voice for .claude/rules files
|
|
4
|
+
paths:
|
|
5
|
+
- '.claude/rules/**/*.md'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Governance rule standards
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
- Follow the rule standard for rule frontmatter, body shape, and voice for .claude/rules files. It is the single source. Read it with `canon standards rule`.
|
|
13
|
+
- Read it before writing or editing a rule. Do not work the shape from memory.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/session.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it
|
|
4
|
+
paths:
|
|
5
|
+
- '.canon/tasks/session-*.md'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Session map standards
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
- Follow the session standard for filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it. It is the single source. Read it with `canon standards session`.
|
|
13
|
+
- Follow this rule rather than the tasks rule for a `session-` file. A handoff is not a task and carries neither `## Outcomes` nor `## Findings`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
# Generated by `canon gov regen` from standards/skill.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Skill structure and authoring rules for Claude Code
|
|
3
4
|
paths:
|
|
4
5
|
- '.claude/skills/**/SKILL.md'
|
|
5
6
|
- 'claude/skills/**/SKILL.md'
|
|
@@ -7,19 +8,14 @@ paths:
|
|
|
7
8
|
- 'claude/skills/**/REQUIREMENT.md'
|
|
8
9
|
---
|
|
9
10
|
|
|
10
|
-
#
|
|
11
|
+
# Claude skill standards
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## Authority
|
|
13
14
|
|
|
15
|
+
- Follow the skill standard for skill structure and authoring rules for Claude Code. It is the single source. Read it with `canon standards skill`.
|
|
14
16
|
- Confirm `canon:create-skill`'s two creation-time questions are answered before a new `SKILL.md` lands, whether drafted by hand, by another skill, or by `canon:create-skill` itself. Carry the third question into the sibling `REQUIREMENT.md`'s `Must not` section as a review criterion rather than a gate.
|
|
15
17
|
- Report it rather than proceeding silently when `canon:create-skill` does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
|
|
16
|
-
|
|
17
|
-
## After editing
|
|
18
|
-
|
|
19
18
|
- Re-read a skill body this session edited before invoking that skill again in the same session
|
|
20
19
|
- Do not read a resolved file path in a held body as evidence the body is current
|
|
21
|
-
|
|
22
|
-
## Authority
|
|
23
|
-
|
|
24
|
-
- Follow the skill standard for skill structure, frontmatter fields, and invocation rules. It is the single source. Read it with `canon standards skill`.
|
|
25
20
|
- Follow the skill requirement standard for the shape a `REQUIREMENT.md` states. Read it with `canon standards skill-requirement`.
|
|
21
|
+
- Follow the skill path standard for which root a path in a body resolves against and how a body cites a standard. Read it with `canon standards skill-paths`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/standard.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Shape and content rules for authoring a standard
|
|
4
|
+
paths:
|
|
5
|
+
- 'standards/**/*.md'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Standard standards
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
- Follow the standard standard for shape and content rules for authoring a standard. It is the single source. Read it with `canon standards standard`.
|
|
13
|
+
- Read it before writing or editing a standard. Do not work the shape from memory.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/tasks.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Folder layout, filename convention, file format, origin lines, and archiving for task files under .canon/tasks/
|
|
4
|
+
paths:
|
|
5
|
+
- '.canon/tasks/**'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Tasks standards
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
- Follow the tasks standard for folder layout, filename convention, file format, origin lines, and archiving for task files under .canon/tasks/. It is the single source. Read it with `canon standards tasks`.
|
|
13
|
+
- Follow the board standard for `priority.md` and `backlog.md`, their readiness groups, and row order. Read it with `canon standards board`.
|
|
14
|
+
- Never hand-edit `.canon/tasks/index.md`. A hook regenerates it from sibling frontmatter.
|
|
15
|
+
- Follow the task decline standard for a task decided against. Read it with `canon standards tasks-decline`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
# Generated by `canon gov regen` from standards/teach.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
|
|
3
4
|
paths:
|
|
4
5
|
- '.canon/teach/**'
|
|
5
6
|
---
|
|
@@ -8,6 +9,6 @@ paths:
|
|
|
8
9
|
|
|
9
10
|
## Authority
|
|
10
11
|
|
|
11
|
-
- Follow the teach standard for
|
|
12
|
+
- Follow the teach standard for workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace. It is the single source. Read it with `canon standards teach`.
|
|
12
13
|
- Follow the glossary standard for the glossary every workspace carries at its root. It is the single source for the entry shape, the ordering, and which terms the file carries. Read it with `canon standards glossary`.
|
|
13
14
|
- Report it rather than proceeding silently when that file does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/wiki.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Folder split, frontmatter, naming convention, and sourcing for a wiki reference page
|
|
4
|
+
paths:
|
|
5
|
+
- 'wiki/**/*.md'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Wiki standards
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
- Follow the wiki standard for folder split, frontmatter, naming convention, and sourcing for a wiki reference page. It is the single source. Read it with `canon standards wiki`.
|
|
13
|
+
- Read it before drafting a page. Do not work the placement criterion or the naming convention from memory.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Generated by `canon gov regen` from standards/wireframes.md. Edit that frontmatter, not this file.
|
|
3
|
+
description: Shape and content rules for canon/wireframes/<surface>.md files
|
|
4
|
+
paths:
|
|
5
|
+
- 'canon/wireframes/**'
|
|
6
|
+
- '.claude/wireframes/**'
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Wireframe standards
|
|
10
|
+
|
|
11
|
+
## Authority
|
|
12
|
+
|
|
13
|
+
- Follow the wireframes standard for shape and content rules for canon/wireframes/<surface>.md files. It is the single source. Read it with `canon standards wireframes`.
|
|
14
|
+
- Read it before adding or revising a surface.
|
|
@@ -12,4 +12,4 @@ paths:
|
|
|
12
12
|
- Load it before drafting a passage, not after revising one.
|
|
13
13
|
- Report it rather than proceeding silently when the skill does not resolve.
|
|
14
14
|
- Do not work these rules from memory.
|
|
15
|
-
- Punctuation, formatting, and file references are a separate topic.
|
|
15
|
+
- Punctuation, formatting, and file references are a separate topic. The markdown rule routes them.
|
|
@@ -5,5 +5,7 @@ extends = ""
|
|
|
5
5
|
# session conduct and `.claude/` authoring rules, `tooling` the dev setup and
|
|
6
6
|
# CI workflow rules, and `writing` the prose and document-shape rules. The
|
|
7
7
|
# `code` group is left to the stacks that write code, so a docs or writing
|
|
8
|
-
# project loads no rule it can never apply.
|
|
9
|
-
|
|
8
|
+
# project loads no rule it can never apply. `standards` holds one rule per
|
|
9
|
+
# standard routing its paths to it, generated by `canon gov regen` from each
|
|
10
|
+
# standard's frontmatter rather than authored.
|
|
11
|
+
rules = ["canon", "claude", "standards", "tooling", "writing", "120-bash"]
|
package/package.json
CHANGED
package/src/canvas/capture.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
2
3
|
import type { CaptureResult } from '@/capture/render'
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type ContentRefused,
|
|
6
|
+
canvasDir,
|
|
7
|
+
type Frame,
|
|
8
|
+
readPage,
|
|
9
|
+
} from '@/canvas/content'
|
|
4
10
|
import {
|
|
5
11
|
type CanvasOptions,
|
|
6
12
|
type CanvasOutcome,
|
|
@@ -134,6 +140,34 @@ export async function captureCanvas(
|
|
|
134
140
|
const early = resolveCaptureTargets(root, spec, '')
|
|
135
141
|
if (!early.ok) return early
|
|
136
142
|
|
|
143
|
+
return serveFor(root, deps, async (base, capture) => {
|
|
144
|
+
const resolved = resolveCaptureTargets(root, spec, base, out)
|
|
145
|
+
if (!resolved.ok) return resolved
|
|
146
|
+
|
|
147
|
+
const captures: { target: CaptureTarget; result: CaptureResult }[] = []
|
|
148
|
+
for (const target of resolved.targets) {
|
|
149
|
+
const [result] = await capture(target.url, {
|
|
150
|
+
selector: CAPTURE_SELECTOR,
|
|
151
|
+
width: target.width,
|
|
152
|
+
outDir: target.pngPath,
|
|
153
|
+
})
|
|
154
|
+
if (result) captures.push({ target, result })
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, captures }
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type ServeRefused = {
|
|
161
|
+
readonly ok: false
|
|
162
|
+
readonly reason: 'no-server' | 'capture-failed'
|
|
163
|
+
readonly detail: string
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function serveFor<T>(
|
|
167
|
+
root: string,
|
|
168
|
+
deps: CaptureDeps,
|
|
169
|
+
run: (base: string, capture: CaptureEngine) => Promise<T>,
|
|
170
|
+
): Promise<T | ServeRefused> {
|
|
137
171
|
const start = deps.start ?? startCanvas
|
|
138
172
|
const server = start(root, { port: 0, shell: new Response('') })
|
|
139
173
|
if (!server.ok) {
|
|
@@ -145,21 +179,9 @@ export async function captureCanvas(
|
|
|
145
179
|
}
|
|
146
180
|
|
|
147
181
|
try {
|
|
148
|
-
const resolved = resolveCaptureTargets(root, spec, server.url, out)
|
|
149
|
-
if (!resolved.ok) return resolved
|
|
150
|
-
|
|
151
182
|
const capture =
|
|
152
183
|
deps.capture ?? (await import('@/capture/render')).captureSources
|
|
153
|
-
|
|
154
|
-
for (const target of resolved.targets) {
|
|
155
|
-
const [result] = await capture(target.url, {
|
|
156
|
-
selector: CAPTURE_SELECTOR,
|
|
157
|
-
width: target.width,
|
|
158
|
-
outDir: target.pngPath,
|
|
159
|
-
})
|
|
160
|
-
if (result) captures.push({ target, result })
|
|
161
|
-
}
|
|
162
|
-
return { ok: true, captures }
|
|
184
|
+
return await run(server.url, capture)
|
|
163
185
|
} catch (error) {
|
|
164
186
|
return {
|
|
165
187
|
ok: false,
|
|
@@ -170,3 +192,194 @@ export async function captureCanvas(
|
|
|
170
192
|
await server.stop()
|
|
171
193
|
}
|
|
172
194
|
}
|
|
195
|
+
|
|
196
|
+
/** The element a composite is captured by, holding every frame and no chrome. */
|
|
197
|
+
export const COMPOSITE_SELECTOR = '#canvas-composite'
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The container holds no text, so its family exists only for the engine's
|
|
201
|
+
* font probe, which rejects a family the machine lacks. The browser default is
|
|
202
|
+
* Times New Roman, which a Linux runner often lacks, and `system-ui` is the one
|
|
203
|
+
* every platform maps. Each frame keeps its own fonts inside its iframe.
|
|
204
|
+
*/
|
|
205
|
+
const COMPOSITE_FONT = 'system-ui'
|
|
206
|
+
|
|
207
|
+
export interface Placement {
|
|
208
|
+
readonly frame: string
|
|
209
|
+
readonly url: string
|
|
210
|
+
readonly left: number
|
|
211
|
+
readonly top: number
|
|
212
|
+
readonly width: number
|
|
213
|
+
readonly height: number
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface Composite {
|
|
217
|
+
readonly placements: readonly Placement[]
|
|
218
|
+
readonly width: number
|
|
219
|
+
readonly height: number
|
|
220
|
+
readonly html: string
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One document showing every frame of a page at its layout position. Offsets
|
|
225
|
+
* run from the top left of the union of the boxes, so a negative coordinate
|
|
226
|
+
* never clips and a lone frame fills the container exactly. Each iframe takes
|
|
227
|
+
* its box, so a document taller than its frame clips as it does on the board,
|
|
228
|
+
* and frames paint in the order given, which is the board's.
|
|
229
|
+
*/
|
|
230
|
+
export function buildComposite(
|
|
231
|
+
page: string,
|
|
232
|
+
frames: readonly Frame[],
|
|
233
|
+
base: string,
|
|
234
|
+
): Composite {
|
|
235
|
+
const left = Math.min(...frames.map((frame) => frame.x))
|
|
236
|
+
const top = Math.min(...frames.map((frame) => frame.y))
|
|
237
|
+
const right = Math.max(...frames.map((frame) => frame.x + frame.width))
|
|
238
|
+
const bottom = Math.max(...frames.map((frame) => frame.y + frame.height))
|
|
239
|
+
const width = right - left
|
|
240
|
+
const height = bottom - top
|
|
241
|
+
|
|
242
|
+
const placements = frames.map((frame) => ({
|
|
243
|
+
frame: frame.name,
|
|
244
|
+
url: frameUrl(base, page, frame),
|
|
245
|
+
left: frame.x - left,
|
|
246
|
+
top: frame.y - top,
|
|
247
|
+
width: frame.width,
|
|
248
|
+
height: frame.height,
|
|
249
|
+
}))
|
|
250
|
+
|
|
251
|
+
const iframes = placements.map(
|
|
252
|
+
(p) =>
|
|
253
|
+
`<iframe title="${escapeAttribute(p.frame)}" src="${escapeAttribute(p.url)}" style="left:${p.left}px;top:${p.top}px;width:${p.width}px;height:${p.height}px"></iframe>`,
|
|
254
|
+
)
|
|
255
|
+
const html = [
|
|
256
|
+
'<!doctype html>',
|
|
257
|
+
'<html><head><meta charset="utf-8">',
|
|
258
|
+
`<style>html,body{margin:0;background:transparent}#canvas-composite{position:relative;font-family:${COMPOSITE_FONT}}#canvas-composite iframe{position:absolute;display:block;border:0;background:transparent}</style>`,
|
|
259
|
+
'</head><body>',
|
|
260
|
+
`<div id="canvas-composite" style="width:${width}px;height:${height}px">`,
|
|
261
|
+
...iframes,
|
|
262
|
+
'</div></body></html>',
|
|
263
|
+
'',
|
|
264
|
+
].join('\n')
|
|
265
|
+
|
|
266
|
+
return { placements, width, height, html }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function escapeAttribute(value: string): string {
|
|
270
|
+
return value.replaceAll('&', '&').replaceAll('"', '"')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Where the generated document is served from, inside the canvas folder so it
|
|
275
|
+
* shares the frames' origin. Chromium skips painting a cross-origin iframe
|
|
276
|
+
* outside the viewport, and the engine's viewport is 720 pixels tall, so a
|
|
277
|
+
* document on another origin captured every frame below that line blank. A dot
|
|
278
|
+
* folder is no page name, so the board never lists it.
|
|
279
|
+
*/
|
|
280
|
+
const COMPOSITE_FOLDER = '.composite'
|
|
281
|
+
|
|
282
|
+
export interface CompositeTarget {
|
|
283
|
+
readonly page: string
|
|
284
|
+
readonly composite: Composite
|
|
285
|
+
readonly htmlPath: string
|
|
286
|
+
/** The served address of the document at `htmlPath`. */
|
|
287
|
+
readonly url: string
|
|
288
|
+
readonly pngPath: string
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export type CompositeTargetOutcome =
|
|
292
|
+
| { readonly ok: true; readonly target: CompositeTarget }
|
|
293
|
+
| ContentRefused
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Turns `<page>` into one composite. `out` is the PNG, and without it the PNG
|
|
297
|
+
* lands at `<page>.png` beside the per-frame folder of the same name, so the
|
|
298
|
+
* two captures of one page never collide.
|
|
299
|
+
*/
|
|
300
|
+
export function resolveCompositeTarget(
|
|
301
|
+
root: string,
|
|
302
|
+
spec: string,
|
|
303
|
+
base: string,
|
|
304
|
+
out?: string,
|
|
305
|
+
): CompositeTargetOutcome {
|
|
306
|
+
if (spec.includes('/')) {
|
|
307
|
+
return refuse(
|
|
308
|
+
'invalid-name',
|
|
309
|
+
`--composite takes a page, not ${spec}, so capture <page> or drop the flag`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const page = readPage(root, spec)
|
|
314
|
+
if (!page) return refuse('no-page', `page ${spec} does not exist`)
|
|
315
|
+
if (page.frames.length === 0) {
|
|
316
|
+
return refuse('no-frame', `page ${spec} has no frames`)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const file = `${spec}.html`
|
|
320
|
+
return {
|
|
321
|
+
ok: true,
|
|
322
|
+
target: {
|
|
323
|
+
page: spec,
|
|
324
|
+
composite: buildComposite(spec, page.frames, base),
|
|
325
|
+
htmlPath: join(canvasDir(root), COMPOSITE_FOLDER, file),
|
|
326
|
+
url: `${base}${FRAMES_PREFIX.slice(1)}${COMPOSITE_FOLDER}/${encodeURIComponent(file)}`,
|
|
327
|
+
pngPath:
|
|
328
|
+
out ?? join(recordDir(root, 'tmp', 'canvas-capture'), `${spec}.png`),
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export type CompositeOutcome =
|
|
334
|
+
| {
|
|
335
|
+
readonly ok: true
|
|
336
|
+
readonly composite: {
|
|
337
|
+
readonly target: CompositeTarget
|
|
338
|
+
readonly result: CaptureResult
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
| ContentRefused
|
|
342
|
+
| ServeRefused
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Captures a page as one PNG through the served canvas, the way `captureCanvas`
|
|
346
|
+
* captures each frame. The viewport is the container's width, so no frame's
|
|
347
|
+
* media query resolves against a window narrower than the frame itself. The
|
|
348
|
+
* page load the engine waits on waits on every iframe.
|
|
349
|
+
*/
|
|
350
|
+
export async function captureComposite(
|
|
351
|
+
root: string,
|
|
352
|
+
spec: string,
|
|
353
|
+
out?: string,
|
|
354
|
+
deps: CaptureDeps = {},
|
|
355
|
+
): Promise<CompositeOutcome> {
|
|
356
|
+
const early = resolveCompositeTarget(root, spec, '')
|
|
357
|
+
if (!early.ok) return early
|
|
358
|
+
|
|
359
|
+
return serveFor(root, deps, async (base, capture) => {
|
|
360
|
+
const resolved = resolveCompositeTarget(root, spec, base, out)
|
|
361
|
+
if (!resolved.ok) return resolved
|
|
362
|
+
|
|
363
|
+
const { target } = resolved
|
|
364
|
+
mkdirSync(dirname(target.htmlPath), { recursive: true })
|
|
365
|
+
writeFileSync(target.htmlPath, target.composite.html)
|
|
366
|
+
let result: CaptureResult | undefined
|
|
367
|
+
try {
|
|
368
|
+
;[result] = await capture(target.url, {
|
|
369
|
+
selector: COMPOSITE_SELECTOR,
|
|
370
|
+
width: target.composite.width,
|
|
371
|
+
outDir: target.pngPath,
|
|
372
|
+
})
|
|
373
|
+
} finally {
|
|
374
|
+
rmSync(target.htmlPath, { force: true })
|
|
375
|
+
}
|
|
376
|
+
if (!result) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
reason: 'capture-failed',
|
|
380
|
+
detail: `the engine rendered nothing for ${spec}`,
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { ok: true, composite: { target, result } }
|
|
384
|
+
})
|
|
385
|
+
}
|
package/src/claude/cases/misc.ts
CHANGED
|
@@ -84,6 +84,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
84
84
|
'Find me the strongest evidence on whether rent control reduces housing supply. Where should I look?',
|
|
85
85
|
expect: 'search-craft',
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
prompt:
|
|
89
|
+
'The composed demo video looks generic and the zoom drags. What should the captions say, and how fast should the push-in be?',
|
|
90
|
+
expect: 'video-craft',
|
|
91
|
+
},
|
|
87
92
|
{
|
|
88
93
|
prompt:
|
|
89
94
|
"I've got the plan approved and it touches the parser, the command, and the docs. How should I build it so I don't end up with one giant diff?",
|
|
@@ -38,12 +38,6 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
38
38
|
'Fold what we decided this session into the context entries and mark off what shipped on the task board.',
|
|
39
39
|
expect: 'context-fold',
|
|
40
40
|
},
|
|
41
|
-
{
|
|
42
|
-
prompt:
|
|
43
|
-
'I typed the old fold skill name and it says it was renamed, where did it go?',
|
|
44
|
-
// The pointer answers under its old name on purpose. canon-keep-retired
|
|
45
|
-
expect: 'docs-fold',
|
|
46
|
-
},
|
|
47
41
|
{
|
|
48
42
|
prompt:
|
|
49
43
|
'Sketch out a plan for adding this new capability before we touch any code.',
|
|
@@ -143,16 +137,10 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
143
137
|
prompt: 'Turn this topic into a slide deck I can render.',
|
|
144
138
|
expect: 'draft-slides',
|
|
145
139
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
},
|
|
151
|
-
// Rot across documents nobody changed is what separates this from
|
|
152
|
-
// `standards-audit`, which is diff-scoped and reports a stated rule broken on
|
|
153
|
-
// a given line. It is also not `target-check`, which asks whether a document
|
|
154
|
-
// exists and holds its sections rather than whether it still describes the
|
|
155
|
-
// tree.
|
|
140
|
+
// Rot across documents nobody changed is what this reads, rather than a
|
|
141
|
+
// stated rule broken on a given line. It is also not `target-check`, which
|
|
142
|
+
// asks whether a document exists and holds its sections rather than whether
|
|
143
|
+
// it still describes the tree.
|
|
156
144
|
{
|
|
157
145
|
prompt:
|
|
158
146
|
'Which of our docs have gone stale, grown too long, or ended up in the wrong file?',
|
|
@@ -230,10 +218,6 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
230
218
|
"Go through what's still unanswered in the intake folder and decide.",
|
|
231
219
|
expect: 'plan-intake-answer',
|
|
232
220
|
},
|
|
233
|
-
{
|
|
234
|
-
prompt: 'Check this markdown against the house style rules.',
|
|
235
|
-
expect: 'standards-audit',
|
|
236
|
-
},
|
|
237
221
|
{
|
|
238
222
|
prompt: 'Find the rough, unfinished-feeling spots in this interface.',
|
|
239
223
|
expect: 'ux-audit',
|
package/src/claude/routing.ts
CHANGED
|
@@ -196,7 +196,7 @@ function frontmatter(text: string): string {
|
|
|
196
196
|
*
|
|
197
197
|
* A glob opening `**` reaches every folder in the tree, so it answers that a
|
|
198
198
|
* file type is governed and never that a named path is. Counting one would
|
|
199
|
-
* report every markdown path covered by
|
|
199
|
+
* report every markdown path covered by the markdown rule and leave the column
|
|
200
200
|
* saying nothing a reader could act on.
|
|
201
201
|
*/
|
|
202
202
|
function isAnchored(glob: string): boolean {
|
package/src/commands/canvas.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
readSelection,
|
|
14
14
|
renamePage,
|
|
15
15
|
} from '@/canvas/content'
|
|
16
|
-
import { captureCanvas } from '@/canvas/capture'
|
|
16
|
+
import { captureCanvas, captureComposite } from '@/canvas/capture'
|
|
17
17
|
import {
|
|
18
18
|
type EditRefused,
|
|
19
19
|
editFrameAtIndex,
|
|
@@ -468,6 +468,10 @@ export function register(program: Command): void {
|
|
|
468
468
|
.option(
|
|
469
469
|
'-o, --out <path>',
|
|
470
470
|
'The PNG for a frame, or the folder for a page; defaults to session scratch',
|
|
471
|
+
)
|
|
472
|
+
.option(
|
|
473
|
+
'--composite',
|
|
474
|
+
'Capture a page as one PNG with every frame at its layout position; --out is then the PNG',
|
|
471
475
|
),
|
|
472
476
|
)
|
|
473
477
|
.addHelpText(
|
|
@@ -478,18 +482,25 @@ export function register(program: Command): void {
|
|
|
478
482
|
'the injected tokens are in the PNG and each frame renders at its own',
|
|
479
483
|
'width. The raw file on disk is never captured.',
|
|
480
484
|
'',
|
|
485
|
+
'With --composite, each frame is clipped to its box as on the board and',
|
|
486
|
+
'shows in its default theme, with no captions or selection outline.',
|
|
487
|
+
'',
|
|
481
488
|
'Exit codes:',
|
|
482
489
|
' 0 every frame rendered',
|
|
483
490
|
' 1 refused or a frame failed, with the reason on stderr or in the JSON record',
|
|
484
491
|
'',
|
|
485
492
|
].join('\n'),
|
|
486
493
|
)
|
|
487
|
-
.action(async (target: string, opts:
|
|
494
|
+
.action(async (target: string, opts: CaptureOptions) => {
|
|
488
495
|
const resolved = await resolveRoot(opts)
|
|
489
496
|
if (!resolved.ok) {
|
|
490
497
|
process.exitCode = refuse(resolved, opts.json ?? false)
|
|
491
498
|
return
|
|
492
499
|
}
|
|
500
|
+
if (opts.composite) {
|
|
501
|
+
process.exitCode = await runComposite(resolved.root, target, opts)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
493
504
|
let outcome: Awaited<ReturnType<typeof captureCanvas>>
|
|
494
505
|
try {
|
|
495
506
|
outcome = await captureCanvas(
|
|
@@ -543,6 +554,69 @@ export function register(program: Command): void {
|
|
|
543
554
|
})
|
|
544
555
|
}
|
|
545
556
|
|
|
557
|
+
interface CaptureOptions extends RootOptions {
|
|
558
|
+
readonly out?: string
|
|
559
|
+
readonly composite?: boolean
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function runComposite(
|
|
563
|
+
root: string,
|
|
564
|
+
target: string,
|
|
565
|
+
opts: CaptureOptions,
|
|
566
|
+
): Promise<number> {
|
|
567
|
+
const emitJson = opts.json ?? false
|
|
568
|
+
let outcome: Awaited<ReturnType<typeof captureComposite>>
|
|
569
|
+
try {
|
|
570
|
+
outcome = await captureComposite(
|
|
571
|
+
root,
|
|
572
|
+
target,
|
|
573
|
+
opts.out ? resolve(process.cwd(), opts.out) : undefined,
|
|
574
|
+
)
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
577
|
+
return refuse({ ok: false, reason: 'capture-failed', detail }, emitJson)
|
|
578
|
+
}
|
|
579
|
+
if (!outcome.ok) return refuse(outcome, emitJson)
|
|
580
|
+
|
|
581
|
+
const { target: item, result } = outcome.composite
|
|
582
|
+
if (result.status === 'failed') {
|
|
583
|
+
if (emitJson) {
|
|
584
|
+
writeJson({
|
|
585
|
+
ok: false,
|
|
586
|
+
composite: {
|
|
587
|
+
page: item.page,
|
|
588
|
+
status: result.status,
|
|
589
|
+
reason: result.reason,
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
}
|
|
593
|
+
intro(BANNER)
|
|
594
|
+
logError(`${item.page}: ${result.reason}`)
|
|
595
|
+
outro()
|
|
596
|
+
return 1
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (emitJson) {
|
|
600
|
+
writeJson({
|
|
601
|
+
ok: true,
|
|
602
|
+
composite: {
|
|
603
|
+
page: item.page,
|
|
604
|
+
status: result.status,
|
|
605
|
+
path: result.pngPath,
|
|
606
|
+
width: result.width,
|
|
607
|
+
height: result.height,
|
|
608
|
+
},
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
intro(BANNER)
|
|
612
|
+
logAdd(
|
|
613
|
+
`${item.page} composite of ${plural(item.composite.placements.length, 'frame')}`,
|
|
614
|
+
)
|
|
615
|
+
logInfo(`${displayPath(result.pngPath)} ${result.width}x${result.height}`)
|
|
616
|
+
outro()
|
|
617
|
+
return 0
|
|
618
|
+
}
|
|
619
|
+
|
|
546
620
|
/** NaN for a value that is not a number, which the content module refuses. */
|
|
547
621
|
function parsePosition(raw: string | undefined, fallback: number): number {
|
|
548
622
|
if (raw === undefined) return fallback
|