@erclx/canon 4.6.1 → 4.8.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/canon-feedback-file/SKILL.md +3 -3
- package/claude/skills/canon-feedback-triage/SKILL.md +1 -1
- package/claude/skills/canon-rollout/SKILL.md +1 -1
- package/claude/skills/canon-screencast/SKILL.md +5 -5
- package/claude/skills/canon-slides-draft/SKILL.md +1 -1
- package/claude/skills/claude-address-review/SKILL.md +4 -4
- package/claude/skills/claude-autoship/SKILL.md +17 -17
- package/claude/skills/claude-design-extract/SKILL.md +2 -2
- package/claude/skills/claude-diagram/SKILL.md +13 -13
- package/claude/skills/claude-docs/SKILL.md +23 -23
- package/claude/skills/claude-feature/SKILL.md +6 -6
- package/claude/skills/claude-groundwork/SKILL.md +16 -16
- package/claude/skills/claude-intake/SKILL.md +8 -8
- package/claude/skills/claude-intake-answer/SKILL.md +1 -1
- package/claude/skills/claude-markdown-propose/REQUIREMENT.md +2 -2
- package/claude/skills/claude-markdown-propose/SKILL.md +8 -8
- package/claude/skills/claude-markdown-propose/references/format.md +3 -3
- package/claude/skills/claude-memory-capture/SKILL.md +13 -13
- package/claude/skills/claude-memory-review/SKILL.md +28 -28
- package/claude/skills/claude-memory-review/references/receipt-format.md +1 -1
- package/claude/skills/claude-orchestrate/SKILL.md +9 -9
- package/claude/skills/claude-orchestrate/references/orchestrator-dispatch.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-parked.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-poll.md +1 -1
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +1 -1
- package/claude/skills/claude-orchestrate/scripts/poll.sh +1 -1
- package/claude/skills/claude-planner/REQUIREMENT.md +1 -1
- package/claude/skills/claude-planner/SKILL.md +6 -6
- package/claude/skills/claude-pr-review/SKILL.md +5 -5
- package/claude/skills/claude-review/SKILL.md +4 -4
- package/claude/skills/claude-seed-sync/SKILL.md +2 -2
- package/claude/skills/claude-tasks/SKILL.md +16 -16
- package/claude/skills/claude-teach/SKILL.md +9 -9
- package/claude/skills/claude-ui-test/SKILL.md +3 -3
- package/claude/skills/claude-ux-audit/SKILL.md +5 -5
- package/claude/skills/claude-ux-measure/SKILL.md +3 -3
- package/claude/skills/claude-worker/SKILL.md +2 -2
- package/claude/skills/claude-worktree/SKILL.md +3 -3
- package/claude/skills/git-issue/SKILL.md +4 -4
- package/claude/skills/git-pr/SKILL.md +6 -6
- package/claude/skills/git-ship/SKILL.md +3 -3
- package/claude/skills/git-split/REQUIREMENT.md +1 -1
- package/claude/skills/git-split/SKILL.md +20 -20
- package/claude/skills/migration-superseded/SKILL.md +4 -4
- package/claude/skills/session-map/SKILL.md +2 -2
- package/claude/skills/session-resume/SKILL.md +8 -8
- package/docs/agents/capture.md +17 -11
- package/docs/agents/commands.md +8 -5
- package/docs/agents/demo.md +2 -2
- package/docs/agents/driver.md +2 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/indexes.md +1 -1
- package/docs/agents/install-and-sync.md +1 -1
- package/docs/agents/intake.md +1 -1
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/records.md +12 -9
- package/docs/agents/restated.md +2 -2
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/state-scoped-risk.md +1 -1
- package/docs/agents/tasks.md +6 -6
- package/docs/agents/teach.md +4 -4
- package/docs/ai-workflow.md +26 -19
- package/docs/operating-model.md +3 -3
- package/docs/target-projects.md +22 -2
- package/docs/visual-design-workflow.md +1 -1
- package/governance/rules/claude/555-tasks.md +3 -3
- package/governance/rules/claude/556-groundwork.md +2 -2
- package/governance/rules/claude/557-intake.md +2 -2
- package/governance/rules/claude/558-plan.md +3 -3
- package/governance/rules/claude/559-memory.md +2 -2
- package/governance/rules/claude/560-diagrams.md +3 -3
- package/governance/rules/claude/561-teach.md +2 -2
- package/governance/rules/claude/562-session.md +2 -2
- package/governance/rules/core/035-tasks.md +2 -2
- package/governance/rules/core/045-memory.md +3 -3
- package/governance/rules/core/055-scratch.md +2 -1
- package/governance/rules/core/085-worktrees.md +1 -1
- package/package.json +1 -2
- package/scripts/core/check-ignore-parity.sh +24 -51
- package/scripts/core/install-check.sh +4 -4
- package/scripts/core/regen-hero.sh +6 -3
- package/scripts/core/snapshot.sh +1 -1
- package/scripts/manage-sandbox.sh +1 -1
- package/scripts/tooling/verify.sh +2 -2
- package/snippets/claude/decision-memo.md +1 -1
- package/snippets/claude/feature-recap.md +1 -1
- package/src/audits/catalog.ts +1 -1
- package/src/capture/render.ts +166 -0
- package/src/capture/sources.ts +42 -0
- package/src/capture/stamp.ts +52 -0
- package/src/claude/cases/all.ts +1 -1
- package/src/claude/seeds.ts +13 -2
- package/src/claude/skills-rank.ts +1 -1
- package/src/cli.ts +5 -5
- package/src/commands/capture.ts +52 -41
- package/src/commands/demo.ts +1 -1
- package/src/commands/design.ts +1 -1
- package/src/commands/feedback.ts +5 -5
- package/src/commands/gov.ts +17 -6
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +173 -2
- package/src/commands/pr.ts +1 -1
- package/src/commands/records.ts +1 -1
- package/src/commands/serve.ts +2 -2
- package/src/commands/slides.ts +1 -1
- package/src/commands/tasks.ts +5 -5
- package/src/commands/teach.ts +1 -1
- package/src/context/audit.ts +1 -1
- package/src/demo/compile.ts +1 -1
- package/src/demo/drive.ts +5 -8
- package/src/driver/drive.ts +7 -8
- package/src/gate/measures.ts +1 -1
- package/src/gate/stages.ts +1 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/restated.ts +3 -3
- package/src/inventory/walk.ts +3 -3
- package/src/migrate/apply.ts +67 -1
- package/src/migrate/records.ts +276 -0
- package/src/paths.ts +1 -1
- package/src/pr/paths.ts +1 -1
- package/src/record-root.ts +77 -27
- package/src/records/backup.ts +71 -17
- package/src/records/validate.ts +1 -1
- package/src/sync/layout.ts +11 -2
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +20 -3
- package/standards/diagrams.md +4 -4
- package/standards/glossary.md +1 -1
- package/standards/groundwork.md +3 -3
- package/standards/index.md +2 -2
- package/standards/intake.md +3 -3
- package/standards/memory.md +2 -2
- package/standards/plan.md +3 -3
- package/standards/session.md +2 -2
- package/standards/standard.md +1 -1
- package/standards/tasks.md +9 -9
- package/standards/teach.md +2 -2
- package/standards/versioning.md +1 -1
- package/tooling/base/configs/.husky/post-merge +1 -1
- package/tooling/base/reference.md +1 -1
- package/tooling/claude/manifest.toml +13 -3
- package/tooling/claude/reference.md +28 -15
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +8 -8
package/src/commands/capture.ts
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { relative, resolve } from 'node:path'
|
|
3
3
|
import type { Command } from 'commander'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
INSTALL_BROWSER,
|
|
6
|
+
isBrowserMissing,
|
|
7
|
+
isEngineMissing,
|
|
8
|
+
} from '@/browser/engine'
|
|
9
|
+
import {
|
|
10
|
+
frameError,
|
|
11
|
+
intro,
|
|
12
|
+
logError,
|
|
13
|
+
logInfo,
|
|
14
|
+
logWarn,
|
|
15
|
+
outro,
|
|
16
|
+
pipeOutput,
|
|
17
|
+
} from '@/ui'
|
|
5
18
|
|
|
6
19
|
const DEFAULT_SOURCE = 'assets'
|
|
7
|
-
const DEFAULT_SELECTOR = '.window'
|
|
8
20
|
|
|
9
21
|
/**
|
|
10
|
-
* Both aliases are type queries rather than imports, so
|
|
11
|
-
*
|
|
22
|
+
* Both aliases are type queries rather than imports, so naming the render
|
|
23
|
+
* module's types adds no static import of it. The only reference that survives
|
|
24
|
+
* to runtime is the `import()` inside the action.
|
|
12
25
|
*/
|
|
13
26
|
type Renderer = typeof import('@/capture/render')
|
|
14
27
|
type CaptureResult = Awaited<ReturnType<Renderer['captureSources']>>[number]
|
|
15
28
|
|
|
16
29
|
/**
|
|
17
|
-
* Holds wiring only. Every browser reference sits behind
|
|
18
|
-
* because `src/cli.ts` imports this module at startup and the
|
|
19
|
-
*
|
|
30
|
+
* Holds wiring only. Every browser reference sits behind a dynamic import,
|
|
31
|
+
* because `src/cli.ts` imports this module at startup and resolving the engine
|
|
32
|
+
* there would put a browser launch in front of every other command. That is the
|
|
33
|
+
* same reason `src/commands/demo.ts`, `src/commands/inventory.ts`, and
|
|
34
|
+
* `src/commands/driver.ts` state for themselves.
|
|
20
35
|
*/
|
|
21
36
|
export function register(program: Command): void {
|
|
22
37
|
program
|
|
@@ -24,20 +39,27 @@ export function register(program: Command): void {
|
|
|
24
39
|
.description('Render HTML capture sources to PNG')
|
|
25
40
|
.argument('[source]', 'HTML file or a directory of them', DEFAULT_SOURCE)
|
|
26
41
|
.option('-o, --out <dir>', 'Output directory, defaults beside the source')
|
|
27
|
-
.option('-s, --selector <selector>', 'Element to capture'
|
|
42
|
+
.option('-s, --selector <selector>', 'Element to capture')
|
|
28
43
|
.action(
|
|
29
44
|
async (
|
|
30
45
|
source: string,
|
|
31
|
-
opts: { out?: string; selector
|
|
46
|
+
opts: { out?: string; selector?: string },
|
|
32
47
|
): Promise<void> => {
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Refused rather than defaulted, and refused ahead of every other
|
|
50
|
+
* check, so the message names the invocation rather than whatever the
|
|
51
|
+
* working directory happens to hold. The element a capture crops to is
|
|
52
|
+
* a property of the page's own markup, and the class this command used
|
|
53
|
+
* to assume is declared by two committed sources in one repository.
|
|
54
|
+
*/
|
|
55
|
+
if (!opts.selector) {
|
|
35
56
|
frameError(
|
|
36
|
-
'
|
|
57
|
+
'--selector names the element to capture and has no default. See canon capture --help.',
|
|
37
58
|
)
|
|
38
59
|
process.exitCode = 1
|
|
39
60
|
return
|
|
40
61
|
}
|
|
62
|
+
const selector = opts.selector
|
|
41
63
|
|
|
42
64
|
const sourcePath = resolve(process.cwd(), source)
|
|
43
65
|
if (!existsSync(sourcePath)) {
|
|
@@ -49,8 +71,9 @@ export function register(program: Command): void {
|
|
|
49
71
|
intro('Capture')
|
|
50
72
|
let results: CaptureResult[]
|
|
51
73
|
try {
|
|
74
|
+
const renderer = await import('@/capture/render')
|
|
52
75
|
results = await renderer.captureSources(sourcePath, {
|
|
53
|
-
selector
|
|
76
|
+
selector,
|
|
54
77
|
outDir: opts.out ? resolve(process.cwd(), opts.out) : undefined,
|
|
55
78
|
})
|
|
56
79
|
} catch (error) {
|
|
@@ -85,16 +108,28 @@ export function register(program: Command): void {
|
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
/**
|
|
88
|
-
* Closes an open frame around a failure
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
111
|
+
* Closes an open frame around a failure that stopped the whole run rather than
|
|
112
|
+
* one source. Two of them are setup states rather than defects and each names
|
|
113
|
+
* the step that clears it: an engine package that never resolved, and a browser
|
|
114
|
+
* binary that was never downloaded. Every other failure passes through intact,
|
|
115
|
+
* since the engine's own message is readable and summarizing it loses what it
|
|
116
|
+
* said.
|
|
92
117
|
*/
|
|
93
118
|
function reportInFrame(error: unknown): void {
|
|
119
|
+
if (isEngineMissing(error)) {
|
|
120
|
+
logError('the browser engine is not installed in this project')
|
|
121
|
+
logWarn(`Install it with: ${INSTALL_BROWSER}`)
|
|
122
|
+
outro()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
const message = error instanceof Error ? error.message : String(error)
|
|
95
127
|
const [first, ...rest] = message.split('\n')
|
|
96
128
|
logError(first ?? 'capture failed')
|
|
97
129
|
if (rest.length) pipeOutput(rest.join('\n'))
|
|
130
|
+
if (isBrowserMissing(error)) {
|
|
131
|
+
logWarn(`Install the browser binary with: ${INSTALL_BROWSER}`)
|
|
132
|
+
}
|
|
98
133
|
outro()
|
|
99
134
|
}
|
|
100
135
|
|
|
@@ -107,27 +142,3 @@ function displayPath(path: string): string {
|
|
|
107
142
|
const fromCwd = relative(process.cwd(), path)
|
|
108
143
|
return fromCwd.startsWith('..') ? path : fromCwd
|
|
109
144
|
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Reports absence only when the module or its engine cannot be resolved, which
|
|
113
|
-
* is the published-package case. Any other import failure is a defect inside
|
|
114
|
-
* the render module and propagates, rather than being reported as a feature
|
|
115
|
-
* the package left out.
|
|
116
|
-
*/
|
|
117
|
-
async function loadRenderer(): Promise<Renderer | undefined> {
|
|
118
|
-
try {
|
|
119
|
-
return await import('@/capture/render')
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (isModuleNotFound(error)) return undefined
|
|
122
|
-
throw error
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function isModuleNotFound(error: unknown): boolean {
|
|
127
|
-
return (
|
|
128
|
-
typeof error === 'object' &&
|
|
129
|
-
error !== null &&
|
|
130
|
-
'code' in error &&
|
|
131
|
-
error.code === 'ERR_MODULE_NOT_FOUND'
|
|
132
|
-
)
|
|
133
|
-
}
|
package/src/commands/demo.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function register(program: Command): void {
|
|
|
61
61
|
' 1 refused, with the reason on stderr',
|
|
62
62
|
'',
|
|
63
63
|
'Examples:',
|
|
64
|
-
' canon demo compile .
|
|
64
|
+
' canon demo compile .canon/tmp/screencast/inline-edit.md',
|
|
65
65
|
' canon demo compile draft.md --out demos --force',
|
|
66
66
|
'',
|
|
67
67
|
].join('\n'),
|
package/src/commands/design.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function register(program: Command): void {
|
|
|
17
17
|
.option(
|
|
18
18
|
'-o, --out <path>',
|
|
19
19
|
'Output directory',
|
|
20
|
-
creationRel('review', 'design'),
|
|
20
|
+
creationRel(process.cwd(), 'review', 'design'),
|
|
21
21
|
)
|
|
22
22
|
.action((opts: { source: string; out: string }) => {
|
|
23
23
|
const sourcePath = resolve(process.cwd(), opts.source)
|
package/src/commands/feedback.ts
CHANGED
|
@@ -40,10 +40,10 @@ function isToolkitSource(): boolean {
|
|
|
40
40
|
* single ignore entry and the single backed-folder entry it already had.
|
|
41
41
|
*/
|
|
42
42
|
function writeLocal(body: string): string {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
const relativeDir = creationRel('review', 'feedback')
|
|
43
|
+
// Resolved against the same root the write joins onto, so a checkout that has
|
|
44
|
+
// not migrated its records writes this beside the ones already there rather
|
|
45
|
+
// than opening a second root nothing reads.
|
|
46
|
+
const relativeDir = creationRel(PROJECT_ROOT, 'review', 'feedback')
|
|
47
47
|
const reviewDir = join(PROJECT_ROOT, relativeDir)
|
|
48
48
|
mkdirSync(reviewDir, { recursive: true })
|
|
49
49
|
const filename = `feedback-${deriveSlug(body)}-${timestamp()}.md`
|
|
@@ -57,7 +57,7 @@ export function register(program: Command): void {
|
|
|
57
57
|
program
|
|
58
58
|
.command('feedback')
|
|
59
59
|
.description(
|
|
60
|
-
'Write toolkit feedback from stdin to .
|
|
60
|
+
'Write toolkit feedback from stdin to .canon/review/feedback/, or open a GitHub issue with --github',
|
|
61
61
|
)
|
|
62
62
|
.option(
|
|
63
63
|
'--github',
|
package/src/commands/gov.ts
CHANGED
|
@@ -66,7 +66,17 @@ import {
|
|
|
66
66
|
select,
|
|
67
67
|
} from '@/ui'
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Where the rules payload lands, resolved per target rather than once at import.
|
|
71
|
+
*
|
|
72
|
+
* The record root a project uses is a fact about that project, and this command
|
|
73
|
+
* writes into a target the caller named, so a value computed here against this
|
|
74
|
+
* process's own tree would point every target at whichever root the toolkit
|
|
75
|
+
* happens to hold.
|
|
76
|
+
*/
|
|
77
|
+
function payloadRel(root: string): string {
|
|
78
|
+
return creationRel(root, SCRATCH, 'gov', 'rules.md')
|
|
79
|
+
}
|
|
70
80
|
const RULES_REL = join('.claude', 'rules')
|
|
71
81
|
|
|
72
82
|
interface InstallOptions {
|
|
@@ -170,7 +180,7 @@ export function register(program: Command): void {
|
|
|
170
180
|
|
|
171
181
|
gov
|
|
172
182
|
.command('build')
|
|
173
|
-
.description('Concatenate installed rules into .
|
|
183
|
+
.description('Concatenate installed rules into .canon/tmp/gov/rules.md')
|
|
174
184
|
.argument('[target]', 'Target directory', '.')
|
|
175
185
|
.helpOption('-h, --help', 'Show this help message')
|
|
176
186
|
.action(async (target: string) => {
|
|
@@ -1222,6 +1232,7 @@ async function runBuild(target: string): Promise<number> {
|
|
|
1222
1232
|
intro('canon gov build')
|
|
1223
1233
|
|
|
1224
1234
|
const resolved = resolve(target)
|
|
1235
|
+
const payload = payloadRel(resolved)
|
|
1225
1236
|
const rulesDir = join(resolved, RULES_REL)
|
|
1226
1237
|
|
|
1227
1238
|
if (!existsSync(rulesDir)) {
|
|
@@ -1237,7 +1248,7 @@ async function runBuild(target: string): Promise<number> {
|
|
|
1237
1248
|
}
|
|
1238
1249
|
|
|
1239
1250
|
const shouldBuild = await select({
|
|
1240
|
-
message: `Build ${files.length} rules to ${
|
|
1251
|
+
message: `Build ${files.length} rules to ${payload}?`,
|
|
1241
1252
|
options: [
|
|
1242
1253
|
{ value: true, label: 'Yes' },
|
|
1243
1254
|
{ value: false, label: 'No' },
|
|
@@ -1252,15 +1263,15 @@ async function runBuild(target: string): Promise<number> {
|
|
|
1252
1263
|
}
|
|
1253
1264
|
|
|
1254
1265
|
logStep('Building rules payload')
|
|
1255
|
-
const output = join(resolved,
|
|
1266
|
+
const output = join(resolved, payload)
|
|
1256
1267
|
await mkdir(dirname(output), { recursive: true })
|
|
1257
1268
|
await writeFile(output, buildRulesPayload(files))
|
|
1258
|
-
logAdd(
|
|
1269
|
+
logAdd(payload)
|
|
1259
1270
|
|
|
1260
1271
|
const { GREEN, NC } = palette(process.stderr)
|
|
1261
1272
|
outro()
|
|
1262
1273
|
process.stderr.write(
|
|
1263
|
-
`${GREEN}✓ Rules built (${files.length} rules → ${
|
|
1274
|
+
`${GREEN}✓ Rules built (${files.length} rules → ${payload})${NC}\n`,
|
|
1264
1275
|
)
|
|
1265
1276
|
return 0
|
|
1266
1277
|
}
|
package/src/commands/intake.ts
CHANGED
|
@@ -47,7 +47,7 @@ interface AnswerCommandOptions {
|
|
|
47
47
|
export function register(program: Command): void {
|
|
48
48
|
const intake = program
|
|
49
49
|
.command('intake')
|
|
50
|
-
.description('Read and answer intake folders in .
|
|
50
|
+
.description('Read and answer intake folders in .canon/intake/')
|
|
51
51
|
.helpOption('-h, --help', 'Show this help message')
|
|
52
52
|
|
|
53
53
|
intake
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
1
3
|
import type { Command } from 'commander'
|
|
2
4
|
import { listRepositoryFiles } from '@/git-files'
|
|
3
|
-
import { applyRename, readSources } from '@/migrate/apply'
|
|
5
|
+
import { applyRecordsMove, applyRename, readSources } from '@/migrate/apply'
|
|
4
6
|
import { isToolkitOwned, planRename, type RenamePlan } from '@/migrate/plan'
|
|
7
|
+
import {
|
|
8
|
+
ignoresDestination,
|
|
9
|
+
planRecordsMove,
|
|
10
|
+
type RecordsPlan,
|
|
11
|
+
} from '@/migrate/records'
|
|
5
12
|
import { logError, logInfo, logStep, logWarn, pipeOutput, plural } from '@/ui'
|
|
6
13
|
|
|
7
14
|
interface RenameOptions {
|
|
@@ -128,12 +135,176 @@ function toRecord(
|
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
interface RecordsOptions {
|
|
139
|
+
readonly json?: boolean
|
|
140
|
+
readonly write?: boolean
|
|
141
|
+
readonly root?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Moves a project's session records to `.canon/` and repoints what cites them.
|
|
146
|
+
*
|
|
147
|
+
* The ignore gate runs before anything is planned. Every folder being moved is
|
|
148
|
+
* ignored where it stands, so landing them under a root the project does not
|
|
149
|
+
* ignore publishes the memory pen into the next commit, and reporting a plan
|
|
150
|
+
* the caller cannot safely apply is worse than refusing to draw one.
|
|
151
|
+
*/
|
|
152
|
+
async function runRecords(opts: RecordsOptions): Promise<number> {
|
|
153
|
+
const root = opts.root ?? process.cwd()
|
|
154
|
+
|
|
155
|
+
const gitignore = readGitignore(root)
|
|
156
|
+
if (gitignore === undefined) {
|
|
157
|
+
logError(
|
|
158
|
+
`No .gitignore at ${root}, so the records cannot be moved somewhere they stay ignored.`,
|
|
159
|
+
)
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!ignoresDestination(gitignore)) {
|
|
164
|
+
logError(
|
|
165
|
+
'This project does not ignore .canon/, and every record folder being moved is ignored where it stands.',
|
|
166
|
+
)
|
|
167
|
+
logError(
|
|
168
|
+
'Run canon tooling sync --write to take the ignore entry, then run this again.',
|
|
169
|
+
)
|
|
170
|
+
return 1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const files = await listRepositoryFiles(root)
|
|
174
|
+
if (files === undefined) {
|
|
175
|
+
logError(`Could not list files under ${root}. Is it a git repository?`)
|
|
176
|
+
return 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const plan = planRecordsMove(root, await readSources(root, files))
|
|
180
|
+
|
|
181
|
+
// stdout, so the record pipes clean. `pipeOutput` frames to stderr, which is
|
|
182
|
+
// where this command's report belongs and where a JSON record does not.
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
process.stdout.write(
|
|
185
|
+
`${JSON.stringify(toRecordsRecord(plan, opts.write))}\n`,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
reportRecords(plan)
|
|
190
|
+
|
|
191
|
+
if (plan.collisions.length > 0) {
|
|
192
|
+
logError(
|
|
193
|
+
`${plural(plan.collisions.length, 'destination')} already exist under .canon/. Merging two record folders is not a call this verb takes.`,
|
|
194
|
+
)
|
|
195
|
+
for (const path of plan.collisions) logError(` ${path}`)
|
|
196
|
+
return 1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (plan.moves.length === 0 && plan.entries.length === 0) return 0
|
|
200
|
+
|
|
201
|
+
if (!opts.write) {
|
|
202
|
+
logWarn('Nothing was written. Pass --write to apply this plan.')
|
|
203
|
+
return 2
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const applied = await applyRecordsMove(root, plan)
|
|
207
|
+
logStep(
|
|
208
|
+
`Rewrote ${plural(applied.written, 'file')} and moved ${plural(applied.moved, 'folder')}.`,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// `failed` carries a citation the write loop could not land as well as a
|
|
212
|
+
// folder the rename loop stopped on, so the line names a path rather than a
|
|
213
|
+
// folder. Reading a rejected write back as a folder that would not move sends
|
|
214
|
+
// the reader looking at the wrong half of the verb.
|
|
215
|
+
if (applied.failed.length > 0) {
|
|
216
|
+
logError(`Could not write ${plural(applied.failed.length, 'path')}.`)
|
|
217
|
+
for (const path of applied.failed) logError(` ${path}`)
|
|
218
|
+
return 1
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readGitignore(root: string): string | undefined {
|
|
225
|
+
try {
|
|
226
|
+
return readFileSync(join(root, '.gitignore'), 'utf8')
|
|
227
|
+
} catch {
|
|
228
|
+
return undefined
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reportRecords(plan: RecordsPlan): void {
|
|
233
|
+
logInfo(`${plural(plan.moves.length, 'folder')} to move.`)
|
|
234
|
+
for (const move of plan.moves) logInfo(` ${move.from} -> ${move.to}`)
|
|
235
|
+
logInfo(
|
|
236
|
+
`${plural(plan.entries.length, 'file')} to change, ${plural(plan.rewritten, 'citation')} to rewrite.`,
|
|
237
|
+
)
|
|
238
|
+
logInfo(`${plural(plan.kept, 'citation')} marked to keep the old root.`)
|
|
239
|
+
|
|
240
|
+
if (plan.excluded.length > 0) {
|
|
241
|
+
logInfo(`${plural(plan.excluded.length, 'file')} excluded from the sweep.`)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function toRecordsRecord(
|
|
246
|
+
plan: RecordsPlan,
|
|
247
|
+
wrote: boolean | undefined,
|
|
248
|
+
): unknown {
|
|
249
|
+
return {
|
|
250
|
+
ok: plan.collisions.length === 0,
|
|
251
|
+
wrote: wrote === true,
|
|
252
|
+
moves: plan.moves,
|
|
253
|
+
collisions: plan.collisions,
|
|
254
|
+
files: plan.entries.length,
|
|
255
|
+
rewritten: plan.rewritten,
|
|
256
|
+
kept: plan.kept,
|
|
257
|
+
excluded: plan.excluded.length,
|
|
258
|
+
paths: plan.entries.map((entry) => ({
|
|
259
|
+
path: entry.path,
|
|
260
|
+
rewritten: entry.rewritten,
|
|
261
|
+
})),
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
131
265
|
export function register(program: Command): void {
|
|
132
266
|
const migrate = program
|
|
133
267
|
.command('migrate')
|
|
134
|
-
.description('Move a project off
|
|
268
|
+
.description('Move a project off a retired name or layout')
|
|
135
269
|
.helpOption('-h, --help', 'Show this help message')
|
|
136
270
|
|
|
271
|
+
migrate
|
|
272
|
+
.command('records')
|
|
273
|
+
.description('Move the gitignored session records to .canon/')
|
|
274
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
275
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
276
|
+
.option('--write', 'Apply the plan rather than reporting it')
|
|
277
|
+
.option(
|
|
278
|
+
'--root <path>',
|
|
279
|
+
'Project root, defaulting to the working directory',
|
|
280
|
+
)
|
|
281
|
+
.addHelpText(
|
|
282
|
+
'after',
|
|
283
|
+
[
|
|
284
|
+
'',
|
|
285
|
+
'Exit codes:',
|
|
286
|
+
' 0 nothing to move, or --write applied the whole plan',
|
|
287
|
+
' 1 refused, or a move failed',
|
|
288
|
+
' 2 a plan exists and --write was not passed',
|
|
289
|
+
'',
|
|
290
|
+
'The project has to ignore .canon/ before anything moves. Every folder',
|
|
291
|
+
'this relocates is ignored where it stands, and landing one under a root',
|
|
292
|
+
'the project tracks publishes the memory pen into the next commit.',
|
|
293
|
+
'',
|
|
294
|
+
'A line carrying canon-keep-record-root, or the line below it, keeps the',
|
|
295
|
+
'old root. Prose that dates a decision needs it; a live path does not.',
|
|
296
|
+
'',
|
|
297
|
+
'Examples:',
|
|
298
|
+
' canon migrate records',
|
|
299
|
+
' canon migrate records --write',
|
|
300
|
+
' canon migrate records --json',
|
|
301
|
+
'',
|
|
302
|
+
].join('\n'),
|
|
303
|
+
)
|
|
304
|
+
.action(async (opts: RecordsOptions) => {
|
|
305
|
+
process.exitCode = await runRecords(opts)
|
|
306
|
+
})
|
|
307
|
+
|
|
137
308
|
migrate
|
|
138
309
|
.command('rename')
|
|
139
310
|
.description('Rewrite every unprotected aitk token to canon')
|
package/src/commands/pr.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function register(program: Command): void {
|
|
|
130
130
|
'Examples:',
|
|
131
131
|
' canon pr key-changes',
|
|
132
132
|
' canon pr key-changes 1265 --json',
|
|
133
|
-
' canon pr key-changes --body .
|
|
133
|
+
' canon pr key-changes --body .canon/tmp/body.md --base origin/main',
|
|
134
134
|
'',
|
|
135
135
|
].join('\n'),
|
|
136
136
|
)
|
package/src/commands/records.ts
CHANGED
|
@@ -226,7 +226,7 @@ function backupHelp(verb: 'push' | 'pull'): string {
|
|
|
226
226
|
'',
|
|
227
227
|
...direction,
|
|
228
228
|
'',
|
|
229
|
-
'The history lives in a second git directory at .
|
|
229
|
+
'The history lives in a second git directory at .canon/.records.git, pointed at a',
|
|
230
230
|
'private repository a person creates once. The verbs refuse with the setup command',
|
|
231
231
|
'when it is absent, and refuse when its origin is also a remote of this project.',
|
|
232
232
|
'',
|
package/src/commands/serve.ts
CHANGED
|
@@ -36,8 +36,8 @@ export function register(program: Command): void {
|
|
|
36
36
|
'starts it in the background and reads the record off stdout.',
|
|
37
37
|
'',
|
|
38
38
|
'Examples:',
|
|
39
|
-
' canon serve .
|
|
40
|
-
' canon serve .
|
|
39
|
+
' canon serve .canon/teach',
|
|
40
|
+
' canon serve .canon/teach --entry 03-fde-system-design/index.html',
|
|
41
41
|
' canon serve dist --port 4000 --json',
|
|
42
42
|
'',
|
|
43
43
|
].join('\n'),
|
package/src/commands/slides.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function register(program: Command): void {
|
|
|
20
20
|
.option(
|
|
21
21
|
'-o, --out <path>',
|
|
22
22
|
'Output directory',
|
|
23
|
-
creationRel('review', 'slides'),
|
|
23
|
+
creationRel(process.cwd(), 'review', 'slides'),
|
|
24
24
|
)
|
|
25
25
|
.option('-v, --variant <variant>', 'Override variant (light or dark)')
|
|
26
26
|
.option(
|
package/src/commands/tasks.ts
CHANGED
|
@@ -76,7 +76,7 @@ interface OutcomeCommandOptions {
|
|
|
76
76
|
export function register(program: Command): void {
|
|
77
77
|
const tasks = program
|
|
78
78
|
.command('tasks')
|
|
79
|
-
.description('Manage the task board in .
|
|
79
|
+
.description('Manage the task board in .canon/tasks/')
|
|
80
80
|
.helpOption('-h, --help', 'Show this help message')
|
|
81
81
|
|
|
82
82
|
tasks
|
|
@@ -157,8 +157,8 @@ export function register(program: Command): void {
|
|
|
157
157
|
'',
|
|
158
158
|
'Locations:',
|
|
159
159
|
' unstated the task carries no Plan: line',
|
|
160
|
-
' live the target resolves inside .
|
|
161
|
-
' archived the target resolves inside .
|
|
160
|
+
' live the target resolves inside .canon/plans/',
|
|
161
|
+
' archived the target resolves inside .canon/plans/archive/',
|
|
162
162
|
' outside the target resolves somewhere else',
|
|
163
163
|
'',
|
|
164
164
|
'Exit codes:',
|
|
@@ -201,11 +201,11 @@ export function register(program: Command): void {
|
|
|
201
201
|
'exit code, which a shell function wrapping canon can flatten to zero.',
|
|
202
202
|
'',
|
|
203
203
|
'A relative path resolves against the project root first and against',
|
|
204
|
-
'.
|
|
204
|
+
'.canon/tasks/ second, so the ../plans/ link a board row writes works.',
|
|
205
205
|
'',
|
|
206
206
|
'Examples:',
|
|
207
207
|
' canon tasks plan-answers dispatch-answer-gate',
|
|
208
|
-
' canon tasks plan-answers .
|
|
208
|
+
' canon tasks plan-answers .canon/plans/feature-dispatch-answer-gate.md',
|
|
209
209
|
' canon tasks plan-answers ../plans/feature-dispatch-answer-gate.md',
|
|
210
210
|
' canon tasks plan-answers dispatch-answer-gate --json',
|
|
211
211
|
'',
|
package/src/commands/teach.ts
CHANGED
|
@@ -76,7 +76,7 @@ interface GlossaryCommandOptions {
|
|
|
76
76
|
export function register(program: Command): void {
|
|
77
77
|
const teach = program
|
|
78
78
|
.command('teach')
|
|
79
|
-
.description('Manage learning workspaces in .
|
|
79
|
+
.description('Manage learning workspaces in .canon/teach/')
|
|
80
80
|
.helpOption('-h, --help', 'Show this help message')
|
|
81
81
|
|
|
82
82
|
teach
|
package/src/context/audit.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { isStubSeed } from '@/seed-marker'
|
|
|
19
19
|
* Every reason `canon context audit` refuses for.
|
|
20
20
|
*
|
|
21
21
|
* `no-folders` is the one ordinary absence: a project that never adopted
|
|
22
|
-
* `.claude/context/`, `.
|
|
22
|
+
* `.claude/context/`, `.canon/diagrams/`, or `.claude/wireframes/` names no
|
|
23
23
|
* corpus this audit can measure, the same state `no-skills` reads for the
|
|
24
24
|
* skill corpora. The other four are a malformed invocation or a checkout git
|
|
25
25
|
* cannot read, which stay a break rather than an absence.
|
package/src/demo/compile.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Beat, Draft } from '@/demo/beats'
|
|
|
5
5
|
* separate artifacts on purpose: a beat carries no target, no wait condition,
|
|
6
6
|
* and no timing, and putting those four fields on every beat would destroy the
|
|
7
7
|
* property the draft was designed around. See
|
|
8
|
-
* `.
|
|
8
|
+
* `.canon/groundwork/38-demo-recorder/06-decision.md`.
|
|
9
9
|
*
|
|
10
10
|
* A compiled plan is committed rather than scratch, because the timing below is
|
|
11
11
|
* a starting point the operator tunes and the draft cannot reproduce a tuned
|
package/src/demo/drive.ts
CHANGED
|
@@ -20,15 +20,12 @@ declare global {
|
|
|
20
20
|
* the demo feature adds lives here, and `src/commands/demo.ts` reaches it
|
|
21
21
|
* through a dynamic import so no other command resolves the engine at startup.
|
|
22
22
|
*
|
|
23
|
-
* Unlike `@/capture/render`, this module ships. The capture command is excluded
|
|
24
|
-
* from the published package because it regenerates images committed to this
|
|
25
|
-
* repository, and that reason does not transfer to a command whose whole
|
|
26
|
-
* purpose is running in someone else's project.
|
|
27
|
-
*
|
|
28
23
|
* It imports `playwright-core` rather than `@playwright/test`, which stays a
|
|
29
|
-
* development dependency
|
|
30
|
-
* every target's dependency tree, and a target needs the driver
|
|
31
|
-
* test runner and an assertion library.
|
|
24
|
+
* development dependency the published tarball never carries. Shipping puts the
|
|
25
|
+
* import in every target's dependency tree, and a target needs the driver
|
|
26
|
+
* rather than a test runner and an assertion library. Every browser module here
|
|
27
|
+
* takes the same import for that reason, `@/capture/render` included since it
|
|
28
|
+
* started shipping too. Both are pinned to one version rather
|
|
32
29
|
* than a range, because `bunx playwright install chromium` fetches the browser
|
|
33
30
|
* revision the installed engine expects and a float would leave a target
|
|
34
31
|
* resolving a binary its engine cannot launch.
|
package/src/driver/drive.ts
CHANGED
|
@@ -23,19 +23,18 @@ import type {
|
|
|
23
23
|
* `src/commands/driver.ts` reaches it through a dynamic import so no other
|
|
24
24
|
* command resolves the engine at startup.
|
|
25
25
|
*
|
|
26
|
-
* Like `@/demo/drive
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* two surfaces move independently.
|
|
26
|
+
* Like `@/demo/drive`, `@/inventory/walk`, and `@/capture/render`, this module
|
|
27
|
+
* ships. A command whose whole purpose is measuring someone else's page cannot
|
|
28
|
+
* stay toolkit-only, and nothing here reaches into `@/capture/`, so the two
|
|
29
|
+
* surfaces move independently.
|
|
31
30
|
*
|
|
32
31
|
* What separates it from `canon capture` is the axis it adds. A render answers
|
|
33
32
|
* about a page as it loads, and every defect that exists only after a menu
|
|
34
33
|
* opens, an answer is chosen, or the page scrolls is invisible to one. Probes
|
|
35
34
|
* therefore run after a step rather than on arrival, and a run reaches the load
|
|
36
|
-
* state by opening with a `wait` step of its own. That matters most where
|
|
37
|
-
* capture
|
|
38
|
-
*
|
|
35
|
+
* state by opening with a `wait` step of its own. That matters most where no
|
|
36
|
+
* capture runs against the page, since this command is then the only thing
|
|
37
|
+
* measuring it at all.
|
|
39
38
|
*/
|
|
40
39
|
|
|
41
40
|
/** Each probe keyed by the name a caller writes in the run. */
|
package/src/gate/measures.ts
CHANGED
|
@@ -100,7 +100,7 @@ export const SANDBOX_UNDECLARED_CEILING = 47
|
|
|
100
100
|
export const AUDITS_BASELINE = '.claude/canon/baseline.json'
|
|
101
101
|
|
|
102
102
|
export const HERO_STAMP_FAILURE =
|
|
103
|
-
'The hero set disagrees with the stamp written when the image was captured. Run canon capture assets/hero.html and commit all three files together.'
|
|
103
|
+
'The hero set disagrees with the stamp written when the image was captured. Run canon capture assets/hero.html --selector .window and commit all three files together.'
|
|
104
104
|
|
|
105
105
|
function parseJson(payload: string): unknown {
|
|
106
106
|
try {
|
package/src/gate/stages.ts
CHANGED
|
@@ -189,7 +189,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
189
189
|
kind: 'drift',
|
|
190
190
|
pathspec: 'assets/hero.html',
|
|
191
191
|
failure:
|
|
192
|
-
'Hero counts drifted. Run bun run check, then canon capture assets/hero.html, and commit assets/hero.html with assets/hero.png and assets/hero.stamp.',
|
|
192
|
+
'Hero counts drifted. Run bun run check, then canon capture assets/hero.html --selector .window, and commit assets/hero.html with assets/hero.png and assets/hero.stamp.',
|
|
193
193
|
},
|
|
194
194
|
{ kind: 'measure', measure: heroStamp },
|
|
195
195
|
],
|
package/src/gov/citations.ts
CHANGED
|
@@ -357,7 +357,7 @@ function globMatches(root: string, glob: string): boolean {
|
|
|
357
357
|
/**
|
|
358
358
|
* Which of `paths` git ignores, or nothing when git could not answer.
|
|
359
359
|
*
|
|
360
|
-
* Session scratch is the class this reaches. `.
|
|
360
|
+
* Session scratch is the class this reaches. `.canon/tasks/index.md` is real
|
|
361
361
|
* in a live project, absent from a fresh clone and from every linked worktree,
|
|
362
362
|
* and a rule naming it is right either way. Resolving against the filesystem
|
|
363
363
|
* alone would make the verdict depend on which tree the check ran in.
|