@erclx/canon 5.18.0 → 5.20.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/README.md +1 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/create-skill/SKILL.md +1 -0
- 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/install-and-sync.md +17 -3
- package/docs/agents/rule-citations.md +4 -4
- package/docs/agents/sandbox.md +8 -7
- package/docs/agents/teach.md +7 -1
- package/docs/workflow/operating-model.md +9 -9
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map.md +1 -0
- package/governance/renames.toml +24 -0
- 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 +2 -1
- package/src/canvas/capture.ts +228 -15
- package/src/claude/cases/misc.ts +5 -0
- 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 +73 -4
- package/src/commands/teach.ts +112 -4
- package/src/gate/stages.ts +7 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/standard-rules.ts +179 -0
- package/src/sandbox/equivalence.ts +43 -14
- package/src/sandbox/stub-remote.ts +140 -0
- package/src/serve/live.ts +113 -0
- package/src/serve/static.ts +60 -2
- package/src/slides/convert/export.ts +323 -0
- package/src/slides/convert/shapes.ts +764 -0
- package/src/slides/convert/svg.ts +38 -0
- package/src/slides/convert/walk.ts +444 -0
- package/src/teach/browser/reload.ts +9 -0
- package/src/teach/nav.ts +7 -6
- package/src/teach/workspace.ts +2 -1
- package/src/teach/write-if-changed.ts +17 -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 +8 -13
- package/standards/teach.md +5 -0
- package/standards/wiki.md +4 -0
- package/standards/wireframes.md +5 -0
- 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
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?",
|
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
|
package/src/commands/gov.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
|
|
|
12
12
|
import { creationRel, SCRATCH } from '@/record-root'
|
|
13
13
|
import { createGovAdapter } from '@/gov/adapter'
|
|
14
14
|
import { regenConsumedRules } from '@/gov/consumed'
|
|
15
|
+
import { regenStandardRules } from '@/gov/standard-rules'
|
|
15
16
|
import { installRules, lookupRules } from '@/gov/install'
|
|
16
17
|
import { buildGovCatalog, describeRule, describeStack } from '@/gov/list'
|
|
17
18
|
import { buildRulesPayload, listRuleFiles } from '@/gov/payload'
|
|
@@ -190,15 +191,19 @@ export function register(program: Command): void {
|
|
|
190
191
|
|
|
191
192
|
gov
|
|
192
193
|
.command('regen')
|
|
193
|
-
.description(
|
|
194
|
+
.description(
|
|
195
|
+
"Rebuild the standard-pointer rules and a repository's own .claude/rules/",
|
|
196
|
+
)
|
|
194
197
|
.helpOption('-h, --help', 'Show this help message')
|
|
195
198
|
.option('--root <path>', 'Repository root to regenerate', PROJECT_ROOT)
|
|
196
199
|
.addHelpText(
|
|
197
200
|
'after',
|
|
198
201
|
[
|
|
199
202
|
'',
|
|
200
|
-
'
|
|
201
|
-
'
|
|
203
|
+
'First writes governance/rules/standards/ from the paths: and rule:',
|
|
204
|
+
'fields in each standard under standards/. Then reads',
|
|
205
|
+
'internal/governance.toml and installs the stack it names, plus any',
|
|
206
|
+
'rules under internal/rules/. Unlike install and sync, this runs',
|
|
202
207
|
'against the toolkit root, whose .claude/rules/ is produced output.',
|
|
203
208
|
'',
|
|
204
209
|
].join('\n'),
|
|
@@ -1114,7 +1119,17 @@ async function runRegen(
|
|
|
1114
1119
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
1115
1120
|
}
|
|
1116
1121
|
|
|
1117
|
-
const
|
|
1122
|
+
const root = resolve(opts.root ?? PROJECT_ROOT)
|
|
1123
|
+
|
|
1124
|
+
// The consumed copy installs from `governance/rules/`, so the band this
|
|
1125
|
+
// writes has to be fresh before that copy is built from it.
|
|
1126
|
+
const generated = await regenStandardRules(root)
|
|
1127
|
+
if (!generated.ok) {
|
|
1128
|
+
process.stderr.write(`Standard-rules regen failed: ${generated.reason}\n`)
|
|
1129
|
+
return 1
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const result = await regenConsumedRules(root)
|
|
1118
1133
|
|
|
1119
1134
|
if (!result.ok) {
|
|
1120
1135
|
process.stderr.write(`Consumed-rules regen failed: ${result.reason}\n`)
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -346,6 +346,7 @@ function runCoverage(options: CoverageOptions): void {
|
|
|
346
346
|
interface EquivalenceOptions {
|
|
347
347
|
readonly base: string
|
|
348
348
|
readonly includeAnchor?: boolean
|
|
349
|
+
readonly stubRemote?: boolean
|
|
349
350
|
readonly out?: string
|
|
350
351
|
readonly masks: boolean
|
|
351
352
|
readonly json?: boolean
|
|
@@ -372,6 +373,7 @@ async function runEquivalence(
|
|
|
372
373
|
targets,
|
|
373
374
|
base: options.base,
|
|
374
375
|
includeAnchor: options.includeAnchor === true,
|
|
376
|
+
stubRemote: options.stubRemote === true,
|
|
375
377
|
useMasks: options.masks,
|
|
376
378
|
out: options.out,
|
|
377
379
|
})
|
|
@@ -568,6 +570,10 @@ export function register(program: Command): void {
|
|
|
568
570
|
'--include-anchor',
|
|
569
571
|
'Also run the arms that push to the shared GitHub anchor, one at a time (attended)',
|
|
570
572
|
)
|
|
573
|
+
.option(
|
|
574
|
+
'--stub-remote',
|
|
575
|
+
'Run the anchor arms against a local bare repository and a stub gh, writing nothing to the shared anchor',
|
|
576
|
+
)
|
|
571
577
|
.option('--out <dir>', 'Keep the provisioned trees and logs in this folder')
|
|
572
578
|
.option(
|
|
573
579
|
'--no-masks',
|
|
@@ -583,6 +589,7 @@ export function register(program: Command): void {
|
|
|
583
589
|
' canon sandbox equivalence claude:plan-feature --base origin/main',
|
|
584
590
|
'',
|
|
585
591
|
'Provisions offline only and never runs sandbox/run.sh or a claude binary.',
|
|
592
|
+
'--stub-remote also runs the anchor arms, and wins over --include-anchor.',
|
|
586
593
|
'Exit codes: 0 when every compared arm is identical or red the same way',
|
|
587
594
|
'on both sides, 1 on any difference, exit mismatch, or enumeration error.',
|
|
588
595
|
].join('\n'),
|
package/src/commands/slides.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
1
|
+
import { existsSync, statSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
+
import { INSTALL_BROWSER } from '@/browser/engine'
|
|
4
5
|
import { creationRel, SCRATCH } from '@/record-root'
|
|
5
6
|
import { LAYOUTS } from '@/slides/layouts'
|
|
6
7
|
import { openDeck } from '@/slides/open'
|
|
@@ -17,8 +18,14 @@ export function register(program: Command): void {
|
|
|
17
18
|
slides
|
|
18
19
|
.command('render')
|
|
19
20
|
.helpOption('-h, --help', 'Show this help message')
|
|
20
|
-
.description(
|
|
21
|
-
|
|
21
|
+
.description(
|
|
22
|
+
'Render a SLIDES.md source, or a folder of HTML slides, into a PowerPoint deck',
|
|
23
|
+
)
|
|
24
|
+
.option(
|
|
25
|
+
'-s, --source <path>',
|
|
26
|
+
'Source SLIDES.md file, or a folder of .html slides read in filename order',
|
|
27
|
+
'.claude/SLIDES.md',
|
|
28
|
+
)
|
|
22
29
|
.option(
|
|
23
30
|
'-o, --out <path>',
|
|
24
31
|
'Output directory',
|
|
@@ -44,8 +51,15 @@ export function register(program: Command): void {
|
|
|
44
51
|
if (!existsSync(sourcePath)) {
|
|
45
52
|
fail(`${opts.source} not found`)
|
|
46
53
|
}
|
|
47
|
-
const variant = parseVariant(opts.variant)
|
|
48
54
|
const mirror = resolveMirror(opts.mirror)
|
|
55
|
+
if (statSync(sourcePath).isDirectory()) {
|
|
56
|
+
if (opts.variant !== undefined) {
|
|
57
|
+
fail('--variant applies to a SLIDES.md source, not a folder')
|
|
58
|
+
}
|
|
59
|
+
await renderHtmlFolder(sourcePath, outDir, mirror, opts.open)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
const variant = parseVariant(opts.variant)
|
|
49
63
|
const { GREEN, GREY, NC, RED } = palette(process.stderr)
|
|
50
64
|
intro('Render slides')
|
|
51
65
|
const result = await renderSlidesDoc(sourcePath, outDir, {
|
|
@@ -103,6 +117,61 @@ export function register(program: Command): void {
|
|
|
103
117
|
})
|
|
104
118
|
}
|
|
105
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The converter loads on demand, so no other command resolves pptxgenjs,
|
|
122
|
+
* jszip, or the token module at startup. A fallback or a refused link reports
|
|
123
|
+
* one line each and keeps the exit at 0, as an unrecognized layout does on the
|
|
124
|
+
* markdown path, since the deck is written either way.
|
|
125
|
+
*/
|
|
126
|
+
async function renderHtmlFolder(
|
|
127
|
+
sourceDir: string,
|
|
128
|
+
outDir: string,
|
|
129
|
+
mirror: string | undefined,
|
|
130
|
+
shouldOpen: boolean | undefined,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const { exportHtmlDeck } = await import('@/slides/convert/export')
|
|
133
|
+
const { GREEN, GREY, NC, RED } = palette(process.stderr)
|
|
134
|
+
const result = await exportHtmlDeck(sourceDir, outDir, {
|
|
135
|
+
root: process.cwd(),
|
|
136
|
+
mirror,
|
|
137
|
+
})
|
|
138
|
+
if (result.status === 'refused') {
|
|
139
|
+
fail(
|
|
140
|
+
result.reason === 'browser-missing'
|
|
141
|
+
? `Chromium is not installed. Run ${INSTALL_BROWSER}`
|
|
142
|
+
: result.message,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
intro('Render slides')
|
|
146
|
+
process.stderr.write(
|
|
147
|
+
`${GREY}│${NC} ${GREEN}✓${NC} ${result.slideCount} slides\n${GREY}│${NC} ${GREEN}✓${NC} ${result.pptxPath}\n`,
|
|
148
|
+
)
|
|
149
|
+
for (const { slide, selector, properties } of result.fallbacks) {
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`${GREY}│${NC} ${RED}✗${NC} slide ${slide} ${selector} drawn as a picture for ${properties.join(', ')}\n`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
for (const { slide, selector, href, reason } of result.refusedLinks) {
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`${GREY}│${NC} ${RED}✗${NC} slide ${slide} ${selector} link "${href}" left out: ${reason}\n`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
if (result.mirrorPath) {
|
|
160
|
+
process.stderr.write(
|
|
161
|
+
`${GREY}│${NC} ${GREEN}✓${NC} mirrored to ${result.mirrorPath}\n`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
if (shouldOpen) {
|
|
165
|
+
const target = result.mirrorPath ?? result.pptxPath
|
|
166
|
+
const opened = await openDeck(target)
|
|
167
|
+
const mark = opened ? `${GREEN}✓${NC}` : `${RED}✗${NC}`
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
`${GREY}│${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
outro()
|
|
173
|
+
}
|
|
174
|
+
|
|
106
175
|
function parseVariant(value: string | undefined): Variant | undefined {
|
|
107
176
|
if (value === undefined) return undefined
|
|
108
177
|
if (value === 'light' || value === 'dark') return value
|